redis
下载
下载redis
安装
安装gcc环境
yum -y install gcc automake autoconf libtool make
# 若出现/var/run/yum.pid,则执行rm -f /var/run/yum.pid
安装redis
# 解压
tar zxvf redis-4.0.1.tar.gz
cd redis-4.0.1
make 或 make MALLOC=libc
# 指定目录安装
make PREFIX=/usr/local/redis install
# 查看
cd /usr/local/redis
# 进入bin目录
cd /usr/local/redis/bin
# 启动服务,可以修改配置文件,设置daemonize yes
./redis-server
# 再解压文件夹中copy配置文件到安装目录中,与bin目录同级即可,启动服务时加上该文件,配置即可生效
./redis-server redis.conf文件的位置
# 启动客户端 可加参数-h:服务器IP -p:端口 -a:指定认证密码
./redis-cli
存储类型
string
常用命令
# 设置值
set key value
# 获取值
get key
# 解决分布式锁
setnx key value
# 值+1
incr key
# 值+num
incrby key num
# 值-1
decr key
# 值-num
decrby key num
应用场景
[1]保存单个字符串或json数据
[2]存储图片
[3]计数器(微博粉丝数)
hash
常用命令
hset(hmset) key field value
hget(hmget) key field
hgetall key
hkeys key
hlen key
hdel key
hexists key field
# 也可用 del key 的方式直接删除
hdel key field
应用场景
一般用来存储对象,虽然字符串也能解决,但有如下问题:
[1]查询问题/修改问题(如:修改某个字段时需要查询出来再转换,再修改)
[2]分开存的话对内存造成大量浪费(如:user:id、user:name等)
list
常用命令
lpush key value
rpush key value
# 插入已存在列表头部,如果列表不存在,则操作无效
lpushx key value
rpushx key value
# 列表长度
llen key
# 通过索引获取列表中的元素
lindex key index
# 获取指定范围内的元素(0,-1)为全部 多用于分页功能
lrange key start stop
lpop key
rpop key
# 移除并获取第一个元素,如没有则等待
blpop key timeout
brpop key timeout
# 不在区间的将删除
ltrim key start stop
lset key index value
# 将value插入world前或后
linsert key before|after world value
#高级命令
rpopLpush source destination
# 如:source列表有元素 a,b,c,destination列表有元素x,y,z,执行 RPOPLPUSH source destination 之后,source 列表包含元素 a,b,destination列表包含元素 c,x,y,z,并且元素c会被返回给客户端
# 使用场景:需要实名认证之后才能访问特定网页;物流,完成之后才能商品评价
应用场景
大数据集合删减
列表数据显示
粉丝列表、留言评论
热点新闻
分页
set
常用命令
sadd key mem1 [mem2 ......]
# 获取成员数
scard key
# 返回所有成员
smembers key
# 判断是否是成员
sismember key mem
# 返回一个或多个随机数
srandmember key [count]
# 移除一个或多个元素
srem key mem1 [mem2]
# 返回并移除一个或多个元素
spop key [count]
# 将mem从source移到destination
smove source destination mem
# 差集(以左侧为准)
sdiff key1 key2
# 差集(以左侧为准)并存储到nkey
sdiffstore nkey key1 key2
# 交集
sinter key1 key2
# 并集
sunion key1 key2
应用场景
交集、并集、差集
抽奖
共同关注、二度好友
利用唯一性,用户是否存在
zset
常用命令
# 添加
zadd key score1 mem1 [score2 mem2]
# 获取有序集合的成员数
zcard key
# 计算某分数区间内的成员个数
zcount key min max
# 返回有序集合中指定成员的索引
zrank key mem
# 通过索引区间返回指定区间内的成员[低到高]
zrange key start stop [withscores]
# 通过索引区间返回指定区间内的成员[高到低]
zrevrange key start stop [withscores]
# 删除集合
del key
# 移除一个或多个元素
zrem key mem [mem2 ...]
# 移除有序集合中给定的排名区间的所有成员
zremrangebyrank key start stop
# 移除有序集合中给定的分数区间的所有成员
zremrangebyscore key min max
应用场景
排行榜
发布订阅
常用命令
# 订阅频道
subscribe channel [channel2 ...]
# 订阅一个或多个符合给定模式的频道
psubscribe pattern [pattern2 ...]
# 发布消息
publish channel massage
# 取消订阅频道
unsubscribe channel [channel2 ...]
# 取消订阅一个或多个符合给定模式的频道
punsubscribe pattern [pattern2 ...]
应用场景
构建实时的消息系统
微信公众号
博客网站
多数据库
# 将一个库中的key移动到另一个库中
move key 数据库号
# 清除当前数据库所有key
flushdb
# 清除整个redis的所有数据库的所有key
flushall
redis事务
常用命令
# 开启事务
multi
# ... 中间执行操作命令,放入队列,但并不是立即执行
# 可取消事务
discard
# 执行事务队列
exec
# 如果中间命令错误(如给字符串加1),不影响其它命令的执行;若出现了报告错误(如:没有的命令,类似于java编译性错误),则取消事务
# 监视一个或多个key,如果在事务过程中,该key发生改变,则中断事务
watch key [key2 ...]
# 取消watch命令对所有key的监视,如果在执行watch命令之后,exec命令或discard命令先被执行的话,就不需要再执行unwatch命令了
unwatch
应用场景
一组命令必须同时执行,或者不执行;保证一组命令再执行过程中不被其它命令插入
商品秒杀(活动)
转账活动
redis持久化
持久化机制
RDB
redis默认机制–还原数据快,适用于灾备;内存需求高
# seconds(秒) changes(改变次数)
save <seconds> <changes>
AOF
# 启动aof持久化方式
appendonly yes
# 三种方式
# 收到写命令就执行
appendfsync always
# 每秒执行一次
appendfsync everysec
# 完全依赖os
appendfsync no
# aof重写参数设置:
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
redis缓存与数据库一致
实时同步
缓存中无->数据库中查询并保存到缓存;
更新缓存时:先更新数据库->将缓存设置过期(建议不要直接更新缓存内容)
缓存穿透
在高并发情况下,查询一个数据库和缓存都不存在的值时;如果缓存服务不对这个不存在的值进行缓存,导致缓存不会被命中,而大量请求直接落到数据库上。这种情况就叫缓存穿透。这种情况就是缓存和数据库都没有响应的值。
解决办法
缓存的key要按一定规则生成,然后集中拦截所有请求缓存的Key,如果请求的Key不满足既定的生成规则,就拒绝访问;对于NULL值,也直接缓存,但是缓存NULL的时间不能太长,否则NULL数据长时间得不到更新,也不能太短,否则达不到防止缓存击穿的效果
缓存雪崩
在高并发情况下,大量的缓存key在同一时间一起失效,导致大量的请求在缓存中获取不到数据,而直接访问数据库。
解决办法
尽量设置Key失效时间点均匀分布,即不同的Key设置不同的过期时间;如果缓存数据库采用的是分布式部署,将热点数据均匀分布在不同的缓存数据库中;设置热点数据永远不过期
缓存击穿
在高并发情况下,对一个特定的值进行查询,但是这个时候缓存正好过期了,缓存没有命中,而导致大量请求直接访问数据库,如活动系统里面查询活动信息。这种情况就是缓存没有对应的值,数据库有对应的值。
解决办法
设置热点Key对应的数据永远不过期;读取数据库数据进行缓存时,增加互斥锁
总结
缓存穿透、缓存击穿和缓存雪崩都是缓存失效导致大量请求直接访问数据库而出现的情况。不同的是缓存穿透是数据库和缓存都不存在相关数据;而缓存击穿和缓存雪崩是缓存和数据库都存在相应数据,只是缓存失效了而已。
@Cacheable 查询时使用,注意Long转换为String类型,否则抛异常
@CachePut 更新时使用,使用此注解一定会从DB上查询
@CacheEvict 删除时使用
@Caching 组合用法
异步队列
使用kafka等消息中间件处理消息生产和消费
使用阿里的同步工具-canal
使用UDF自定义函数的方式
利用触发器进行缓存同步(c/c++编程语言)
redis主从复制
创建一个从服务器三种方式
- 配置文件:在从服务器的配置文件中加入
slaveof <masterip> <masterport>
- 启动命令:redis-server启动命令后加入
slaveof <masterip> <masterport>
- 客户端命令:Redis服务器启动后,直接通过客户端执行命令
slaveof <masterip> <masterport>
,则该Redis实例成为从节点
# 从变主
slaveof no one
redis cluster集群
0-16383个槽
关于redis自己总结
过期策略
定时删除
优点:保证内存被尽快释放;
缺点:若过期key很多,删除这些key会占用很多的CPU时间,在CPU时间紧张的情况下,CPU不能把所有的时间用来做要紧的事儿,还需要去花时间删除这些key定时器的创建耗时,若为每一个设置过期时间的key创建一个定时器(将会有大量的定时器产生),性能影响严重
懒汉式删除
优点:删除操作只发生在通过key取值的时候发生,而且只删除当前key,所以对CPU时间的占用是比较少的,而且此时的删除是已经到了非做不可的地步(如果此时还不删除的话,我们就会获取到了已经过期的key了);
缺点:若大量的key在超出超时时间后,很久一段时间内,都没有被获取过,那么可能发生内存泄露(无用的垃圾占用了大量的内存)
定期删除
优点:通过限制删除操作的时长和频率,来减少删除操作对CPU时间的占用;
缺点:在内存友好方面,不如"定时删除"(会造成一定的内存占用,但是没有懒汉式那么占用内存),在CPU时间友好方面,不如"懒汉式删除"(会定期的去进行比较和删除操作,cpu方面不如懒汉式,但是比定时好);
难点:合理设置删除操作的执行时长(每次删除执行多长时间)和执行频率(每隔多长时间做一次删除)(这个要根据服务器运行情况来定了),每次执行时间太长,或者执行频率太高对cpu都是一种压力。每次进行定期删除操作执行之后,需要记录遍历循环到了哪个标志位,以便下一次定期时间来时,从上次位置开始进行循环遍历
-----------------202310008-------------------
Redis的使用场景
缓存:穿透、击穿、雪崩、双写一致、持久化、数据过期、淘汰策略
分布式锁:setnx、redisson
如果发生了缓存穿透、击穿、雪崩,该如何解决?
缓存穿透
缓存穿透:查询一个不存在的数据,mysql查询不到数据也不会直接写入缓存,就会导致每次请求都查数据库
- 解决方案一:缓存空数据,查询返回的数据为空,仍把这个空结果进行缓存,
{key:1,value:null}
优点:简单
缺点:消耗内存,可能会发生不一致的问题
- 解决方案二:布隆过滤器
优点:内存占用较少,没有多余key
缺点:实现复杂,存在误判
bitmap(位图):相当于是一个以(bit)位为单位的数组,数组中每个单元只能存储二进制数0或1
布隆过滤器作用:布隆过滤器可以用于检索一个元素是否在一个集合中。
布隆过滤器之误判:
误判率:数组越小误判率就越大,数组越大误判率就越小,但是同时带来了更多的内存消耗。
/**测试误判率
*/
private static int getData(RBloomFilter<String> bloomFilter, int size){
int count = 0; //记录误判的数据条数
for(int x = size; x < size* 2 ; x++) {
if(bloomFilter.contains("add" + x)){
count++;
}
}
return count;
}
/**初始化数据
*/
private static void initData(RBloomFilter<String> bloomFilter,int size){
//第一个参数:布隆过滤器存储的元素个数,第一个参数:误判率
bloomFilter.tryInit(size,e.05);
//在布隆过滤器初始化数据
for(int x =0;x < size; x++) {
bloomFilter.add ("add" + x) ;
}
System.out.println("初始化完成...");
}
缓存击穿
缓存击穿:给某一个key设置了过期时间,当key过期的时候,恰好这时间点对这个key有大量的并发请求过来,这些并发的请求可能会瞬间把DB压垮
- 解决方案一:互斥锁(分布式锁)
当缓存失效时,不立即去load db,先使用如Redis的 setnx去设置一个互斥锁,当操作成功返回时再进行load db的操作并回设缓存,否则重试get缓存的方法。
特点:互斥锁,强一致,性能差
- 解决方案二:逻辑过期
①:在设置key的时候,设置一个过期时间字段一块存入缓存中,不给当前key设置过期时间
②:当查询的时候,从redis取出数据后判断时间是否过期
③:如果过期则开通另外一个线程进行数据同步,当前线程正常返回数据,这个数据不是最新的
特点:逻辑过期,高可用,性能优,不能保证数据绝对一致
缓存雪崩
缓存雪崩:是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案:
- 给不同的Key的TTL添加随机值
- 利用Redis集群提高服务的可用性:哨兵模式、集群模式
- 给缓存业务添加降级限流策略:ngxin或spring cloud gateway
- 给业务添加多级缓存:Guava或Caffeine
----------20231009----------
redis做为缓存,mysql的数据如何与redis进行同步(双写一致性)
双写一致性:当修改了数据库的数据也要同时更新缓存的数据,缓存和数据库的数据要保持一致
延时双删
- 读操作:缓存命中,直接返回;缓存未命中查询数据库,写入缓存,设定超时时间
- 写操作:延迟双删(有脏数据的风险)
分布式锁(强一致)
共享锁:读锁readLock,加锁之后,其他线程可以共享读操作
排他锁:独占锁也叫writeLock,加锁之后,阻塞其他线程读写操作
异步通知保证数据的最终一致性(异步通知)
使用MQ中间件,更新数据之后,通知缓存删除。
基于Canal的异步通知(异步通知)
利用canal中间件,不需要修改业务代码,伪装为mysql的一个从节点,canal通过读取binlog数据更新缓存,二进制日志(BINLOG)记录了所有的DDL(数据定义语言)语句和DML(数据操纵语言)语句,但不包括数据查询(SELECT、SHOW)语询句。
redis做为缓存,数据的持久化是怎么做的?
在Redis中提供了两种数据持久化的方式:1、RDB 2、AOF。
RDB
RDB全称Redis Database Backup file (Redis数据备份文件),也被叫做Redis数据快照。简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后,从磁盘读取快照文件,恢复数据
主动备份
[root@localhost ~]# redis-cli
127.0.0.1:6379> save #由Redis主进程来执行RDB,会阻塞所有命令
ok
127.0.0.1:6379> bgsave #开启子进程执行RDB,避免主进程受到影响
Background saving started
配置文件
Redis内部有触发RDB的机制,可以在redis.conf文件中找到,格式如下:
# 900秒内,如果至少有1个key被修改,则执行bgsave
save 900 1
save 300 10
save 60 10000
RDB执行原理
bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据。完成fork后读取内存数据并写入RDB文件。fork采用的是copy-on-write
技术:
- 当主进程执行读操作时,访问共享内存;
- 当主进程执行写操作时,则会拷贝一份数据,执行写操作。
AOF
AOF全称为Append Only File (追加文件)。Redis处理的每一个写命令都会记录在AOF文件,可以看做是命令日志文件。
AOF默认是关闭的,需要修改redis.conf
配置文件来开启AOF:
#是否开启AOF功能,默认是no
appendonly yes
#AOF文件的名称
appendfilename "appendonly.aof"
AOF的命令记录的频率也可以通过redis.conf
文件来配:
#表示每执行一次写命令,立即记录到AOF文件
appendfsync always
#写命令执行完先放入AOF缓冲区,然后表示每隔1秒将缓冲区数据写到AOF文件,是默认方案
appendfsync everysec
#写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
appendfsync no
因为是记录命令,AOF文件会比RDB文件大的多。而且AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义。通过执行bgrewriteaof
命令,可以让AOF文件执行重写功能,用最少的命令达到相同效果。
Redis也会在触发阈值时自动去重写AOF文件。阈值也可以在redis.conf
中配置:
#AOF文件比上次文件增长超过多少百分比则触发重写
auto-aof-rewrite-percentage 100
#AOF文件体积最小多大以上才触发重写
auto-aof-rewrite-min-size 64mb
RDB与AOF对比
RDB和AOF各有自己的优缺点,如果对数据安全性要求较高,在实际开发中往往会结合两者来使用。
假如redis的key过期之后,会立即删除吗
Redis对数据设置数据的有效时间,数据过期以后,就需要将数据从内存中删除掉。可以按照不同的规则进行删除,这种删除规则就被称之为数据的删除策略(数据过期策略)。
惰性删除
惰性删除︰设置该key过期时间后,我们不去管它,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key
set name zhangsan 10
get name #发现name过期了,直接删除key
优点∶对CPU友好,只会在使用该key时才会进行过期检查,对于很多用不到的key不用浪费时间进行过期检查
缺点∶对内存不友好,如果一个key已经过期,但是一直没有使用,那么该key就会一直存在内存中,内存永远不会释放
定期删除
定期删除:每隔一段时间,我们就对一些key进行检查,删除里面过期的key(从一定数量的数据库中取出一定数量的随机key进行检查,并删除其中的过期key)。
定期清理有两种模式:
- SLOW模式是定时任务,执行频率默认为
10hz
,每次不超过25ms
,以通过修改配置文件redis.conf
的hz选项来调整这个次数 - FAST模式执行频率不固定,但两次间隔不低于
2ms
,每次耗时不超过1ms
优点:可以通过限制删除操作执行的时长和频率来减少删除操作对CPU的影响。另外定期删除,也能有效释放过期键占用的内存。
缺点:难以确定删除操作执行的时长和频率。
Redis的过期删除策略:惰性删除+定期删除两种策略进行配合使用
假如缓存过多,内存是有限的,内存被占满了怎么办?
数据淘汰策略
数据的淘汰策略:当Redis中的内存不够用时,此时在向Redis中添加新的key,那么Redis就会按照某一种规则将内存中的数据删除掉,这种数据的删除规则被称之为内存的淘汰策略。
Redis支持8种不同策略来选择要删除的key:
noeviction
:不淘汰任何key,但是内存满时不允许写入新数据,默认就是这种策略volatile-ttl
:对设置了TTL的key,比较key的剩余TTL值,TTL越小越先被淘汰allkeys-random
:对全体key,随机进行淘汰volatile-random
:对设置了TTL的key,随机进行淘汰allkeys-lru
:对全体key,基于LRU算法进行淘汰volatile-lru
:对设置了TTL的key,基于LRU算法进行淘汰allkeys-lfu
:对全体key,基于LFU算法进行淘汰volatile-lfu
:对设置了TTL的key,基于LFU算法进行淘汰
LRU (Least Recently Used)
最近最少使用。用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。如:key1是在3s之前访问的, key2是在9s之前访问的,删除的就是key2。
LFU (Least Frequently Used)
最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高。如:key1最近5s访问了4次,key2最近5s访问了9次,删除的就是key1。
数据淘汰策略-使用建议
- 优先使用 allkeys-lru策略。充分利用LRU算法的优势,把最近最常访问的数据留在缓存中。如果业务有明显的冷热数据区分,建议使用。
- 如果业务中数据访问频率差别不大,没有明显冷热数据区分,建议使用alkeys-random,随机选择淘汰。
- 如果业务中有置顶的需求,可以使用volatile-lru策略,同时置顶数据不设置过期时间,这些数据就一直不被删除,会淘汰其他设置过期时间的数据。
- 如果业务中有短时高频访问的数据,可以使用allkeys-lfu或volatile-lfu策略。
关于数据淘汰策略其他的面试问题
- 数据库有1000万数据,Redis只能缓存20w数据,如何保证Redis中的数据都是热点数据?
使用allkeys-lru(挑选最近最少使用的数据淘汰)淘汰策略,留下来的都是经常访问的热点数据 - Redis的内存用完了会发生什么?
主要看数据淘汰策略是什么?如果是默认的配置( noeviction ),会直接报错
redis分布式锁,是如何实现的
通常情况下,分布式锁使用的场景:集群情况下的定时任务、抢单、幂等性场景。
Redis实现分布式锁主要利用Redis的setnx
命令。setnx是SET if not exists(如果不存在,则SET)的简写。
#添加锁,NX是互斥、EX是设置超时时间
SET lock value NX EX 10
#释放锁,删除即可
DEL key
Redis实现分布式锁如何合理的控制锁的有效时长?
public void redisLock() throws InterruptedException {
//获取锁(重入锁),执行锁的名称
RLock lock = redissonclient.getLock("heimalock" );
//尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
//boolean isLock = lock.tryLock(10,30,TimeUnit.SECONDS);
//判断是否获取成功 加锁、设置过期时间等操作都是基于lua脚本完成
boolean isLock = lock.tryLock(10,TimeUnit.SECONDS);
if (isLock) {
try {
system.out.println("执行业务");
}finally {
//释放锁
lock.unlock();
}
}
}
redisson实现的分布式锁-可重入
public void add1(){
RLock lock = redissonClient.getLock("heimalock");
boolean isLock = lock.tryLock();
//执行业务
add2();
//释放锁
lock.unlock();
}
public void add2(){
RLock lock = redissonClient.getLock("heimalock");
boolean isLock = lock.tryLock();
//执行业务
//释放锁
lock.unlock();
}
利用hash结构记录线程id和重入次数
redisson实现的分布式锁-主从一致性
RedLock(红锁):不能只在一个redis实例上创建锁,应该是在多个redis实例上创建锁(n / 2+ 1),避免在一个redis实例上加锁。
redisson锁不能解决主从一致性问题,但是可以使用redisson提供的红锁来解决,但是这样的话,性能就太低了,如果业务中非要保证数据的强一致性,建议采用zookeeper实现的分布式锁。
----------20231010----------
Redis集群有哪些方案
在Redis中提供的集群方案总共有三种
主从复制;哨兵模式;分片集群
主从复制
单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就需要搭建主从集群,实现读写分离。一般都是一主多从,主节点负责写数据,从节点负责读数据。
主从数据同步原理
- 从节点请求主节点同步数据(replication id、 offset )
- 主节点判断是否是第一次请求,是第一次就与从节点同步版本信息(replication id和offset)
- 主节点执行bgsave,生成rdb文件后,发送给从节点去执行
- 在rdb生成执行期间,主节点会以命令的方式记录到缓冲区(一个日志文件)
- 把生成之后的命令日志文件发送给从节点进行同步
Replication ld:简称replid,是数据集的标记,id一致则说明是同一数据集。每一个master都有唯一的replid,slave则会继承master节点的replid
offset:偏移量,随着记录在repl_baklog中的数据增多而逐渐增大。slave完成同步时也会记录当前同步的offset,如果slave的offset小于master的offset,说明slave数据落后于master,需要更新。
主从增量同步(slave重启或后期数据变化)
- 从节点请求主节点同步数据,主节点判断不是第一次请求,不是第一次就获取从节点的offset值
- 主节点从命令日志中获取offset值之后的数据,发送给从节点进行数据同步
哨兵模式
Redis提供了哨兵(Sentinel)机制来实现主从集群的自动故障恢复。哨兵的结构和作用如下:
监控:Sentinel会不断检查您的master和slave是否按预期工作
自动故障恢复:如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主
通知:Sentinel充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端
服务状态监控
Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令:
- 主观下线:如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线。
- 客观下线:若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好
超过Sentinel实例数量的一半。
哨兵选主规则
- 首先判断主与从节点断开时间长短,如超过指定值就排该从节点
- 然后判断从节点的slave-priority值,越小优先级越高
- 如果slave-prority一样,则判断slave节点的offset值,越大优先级越高(重要)
- 最后是判断slave节点的运行id大小,越小优先级越高。
你们使用redis是单点还是集群,哪种集群
主从(1主1从)+哨兵就可以了。单节点不超过10G内存,如果Redis内存不足则可以给不同服务分配独立的Redis主从节点。
redis集群(哨兵模式)脑裂
集群脑裂是由于主节点和从节点和sentinel处于不同的网络分区,使得sentinel没有能够心跳感知到主节点,所以通过选举的方式提升了一个从节点为主,这样就存在了两个master,就像大脑分裂了一样,这样会导致客户端还在老的主节点那里写入数据,新节点无法同步数据,当网络恢复后,sentinel会将老的主节点降为从节点,这时再从新master同步数据,就会导致数据丢失。
解决方式:
redis中有两个配置参数:
min-replicas-to-write 1表示最少的salve节点为1个
min-replicas-max-lag 5表示数据复制和同步的延迟不能超过5秒
我们可以修改redis的配置,可以设置最少的从节点数量以及缩短主从数据同步的延迟时间,达不到要求就拒绝请求,就可以避免大量的数据丢失。
分片集群
主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决:
- 海量数据存储问题
- 高并发写的问题
使用分片集群可以解决上述问题,分片集群特征:
- 集群中有多个master,每个master保存不同数据
- 每个master都可以有多个slave节点
- master之间通过ping监测彼此健康状态
- 客户端请求可以访问集群任意节点,最终都会被转发到正确节点
数据读写
Redis分片集群引入了哈希槽的概念,Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。
读写数据:根据key的有效部分计算哈希值,对16384取余(有效部分,如果key前面有大括号,大括号的内容就是有效部分,如果没有,则以key本身做为有效部分)余数做为插槽,寻找插槽所在的实例。
Redis是单线程的,但是为什么还那么快
- Redis是纯内存操作,执行速度非常快
- 采用单线程,避免不必要的上下文切换可竞争条件,多线程还要考虑线程安全问题
- 使用I/O多路复用模型,非阻塞IO。Redis是纯内存操作,执行速度非常快,它的性能瓶颈是网络延迟而不是执行速度,IO多路复用模型主要就是实现了高效的网络请求。
用户空间和内核空间
- Linux系统中一个进程使用的内存情况划分两部分:内核空间、用户空间
- 用户空间只能执行受限的命令(Ring3),而且不能直接调用系统资源必须通过内核提供的接口来访问
- 内核空间可以执行特权命令(Ring0),调用一切系统资源
Linux系统为了提高IO效率,会在用户空间和内核空间都加入缓冲区:
- 写数据时,要把用户缓冲数据拷贝到内核缓冲区,然后写入设备
- 读数据时,要从设备读取数据到内核缓冲区,然后拷贝到用户缓冲区
常见的IO模型
- 阻塞lO (Blocking lO)
顾名思义,阻塞IO就是两个阶段都必须阻塞等待:
阶段一:
①用户进程尝试读取数据(比如网卡数据)
②此时数据尚未到达,内核需要等待数据
③此时用户进程也处于阻塞状态
阶段二:
①数据到达并拷贝到内核缓冲区,代表已就绪
②将内核数据拷贝到用户缓冲区
③拷贝过程中,用户进程依然阻塞等待
④拷贝完成,用户进程解除阻塞,处理数据
可以看到,阻塞IO模型中,用户进程在两个阶段都是阻塞状态。
- 非阻塞IO (Nonblocking lO)
顾名思义,非阻塞IO的recvfrom操作会立即返回结果而不是阻塞用户进程。
阶段一:
①用户进程尝试读取数据(比如网卡数据)
②此时数据尚未到达,内核需要等待数据
③返回异常给用户进程
④用户进程拿到error后,再次尝试读取
⑤循环往复,直到数据就绪
阶段二:
①将内核数据拷贝到用户缓冲区
②拷贝过程中,用户进程依然阻塞等待
③拷贝完成,用户进程解除阻塞,处理数据
可以看到,非阻塞IO模型中,用户进程在第一个阶段是非阻塞,第二个阶段是阻塞状态。虽然是非阻塞,但性能并没有得到提高。而且忙等机制会导致CPU空转,CPU使用率暴增。
- IO多路复用(IO Multiplexing)
IO多路复用:是利用单个线程来同时监听多个Socket,并在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。
阶段一:
①用户进程调用select,指定要监听的Socket集合
②内核监听对应的多个socket
③任意一个或多个socket数据就绪则返回readable
④此过程中用户进程阻塞
阶段二:
①用户进程找到就绪的socket
②依次调用recvfrom读取数据
③内核将数据拷贝到用户空间
④用户进程处理数据
IO多路复用是利用单个线程来同时监听多个Socket,并在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。不过监听Socket的方式、通知的方式又有多种实现,常见的有:
select、poll、epoll
差异:
- select和poll只会通知用户进程有Socket就绪,但不确定具体是哪个Socket,需要用户进程逐个遍历Socket来确认
- epoll则会在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间
Redis网络模型
Redis通过IO多路复用来提高网络性能,并且支持各种不同的多路复用实现,并且将这些实现进行封装,提供了统一的高性能事件库。
Redis网络模型就是使用I/O多路复用结合事件的处理器来应对多个Socket请求
- 连接应答处理器
- 命令回复处理器,在Redis6.0之后,为了提升更好的性能,使用了多线程来处理回复事件
- 命令请求处理器,在Redis6.0之后,将命令的转换使用了多线程,增加命令转换速度,在命令执行的时候,依然
是单线程