Redis主从
搭建主从集群
单节点Redis的并发能力有上限(上万),要进一步提高Redis的并发能力,就要搭建主从集群,实现读写分离。
主节点会把数据同步给从节点,让每个从节点的数据和主节点一样。
- 启动多个Redis实例
- 建立集群
在从节点
通过命令配置主从关系:slaveof <masterip> <masterport>
info replication
:查看节点状态
- 临时:在控制台输入slaveof命令
- 永久:在redis.conf文件中利用slaveof命令指定master节点
成功后,进入主节点,输入info replication
可以看到两个slave,并且在主节点写入后,在从节点可以读取到。
主从同步原理
当主从第一次同步连接或断开重连时,从节点都会发送psync请求,尝试数据同步
【全量同步】:执行bgsave命令,将完整的内存数据生成RDB文件,把RDB文件写到磁盘中,再通过网络传送到slave(效率超级低)
【增量同步】:通过repl_backlog缓冲区,对比主从节点之间的命令差异,发送slave未同步的命令给slave
- replicationID:每个master节点都有自己唯一的id,主从节点建立连接后,主从节点的replid都保持一致,从节点请主节点时会携带replid,如果replid和主节点的replid一致,说明从节点
不是第一次来同步
;否则replid就是第一次来同步
。 - offset:repl_backlog中写入过的数据长度,写操作越多,offset值越大,主从的offset一致代表数据一致。
【问题】repl_backlog是一个缓冲区,用来记录slave和master建立连接后,master中的写命令,但是这个缓冲区的大小默认只有1M。
【解决】repl_backlog是一个环形数组,但是如果slave宕机了很长时间,缓冲区slave还没同步的数据又被master覆盖了,此时slave只能做全量同步(效率低)。
【执行全量同步的时机】:slave节点第一次连接master节点;slave节点断开时间太久,repl_backlog中的offset已经被覆盖。
【执行增量同步的时机】:slave节点断开又恢复,并在repl_backlog中能找到offset时。
全量同步效率低的解决
- 在master中配置
repl-diskless-sync: yes
启动无磁盘复制,避免全量同步时的磁盘IO。 - redis单节点上的
内存占用
不要太大,减少RDB导致的过多磁盘IO。 - 适当
提高repl_backlog的大小
,发现slave宕机时尽快实现故障恢复,尽量避免全量同步。 - 限制一个master上的slave节点数量,如果实在太多slave,可以采用
主-从-从链式结构
,减少master压力。
Redis哨兵
哨兵原理
哨兵Sentinel机制来实现主从集群的自动故障恢复。哨兵的作用:
- 监控:Sentinel会不断检查master和slave是否按预期工作。
- 自动故障恢复:如果master故障,sentinel会将一个slave提升为master。当故障恢复后也以新的master为主。
- 通知:当集群发生故障转移时,sentinel会将最新节点角色信息推送给新的redis客户端。
1. 服务状态监控
sentinel基于心跳机制检测服务状态,每隔1s向集群的每个实例发送ping命令:
- 主观下线:如果某个sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线。
- 客观下线:如果超过指定数量(quorum)的sentinel都认为该实例主管下线,则该实例客观下线。quorum的值最好超过sentinel实力数量的一半。
2. 选举新的master
一旦发现master故障,sentinel需要在slave中选择一个作为新的mater,选择依据:
- 首先判断slave节点与master节点断开时间长短,如果超过指定值(down-after-milliseconds * 10)则会排除该slave节点。
- 然后判断slave节点的slave-priority值,值越小优先级越高。(如果是0则用不参与选举,默认是0)
- 如果slave-priority一样,则判断slave节点的offset值,越大说明数据越新,优先级越高。
- 最后是判断slave节点的运行id大小,越小优先级越高。
3. 故障转移
当选中其中一个slave为新的master后,故障转移的步骤:
- sentinel给备选的slave节点发送
slaveof no one
命令,让该节点成为master。 - sentinel给所有其他slave发送
slaveof 192.168.140.101 7002
命令,让这些slave成为新的master的从节点,开始从新的master上同步数据。 - 最后sentinel将故障节点标记为slave(修改故障节点的配置文件redis.conf),当故障节点恢复后,会自动成为新的master的slave节点。
搭建哨兵集群
哨兵的配置文件sentinel.conf:
conf
sentinel announce-ip "192.168.140.101"
sentinel monitor hmaster 192.168.140.101 7001 2
sentinel down-after-milliseconds hmaster 5000
sentinel failover-timeout hmaster 60000
sentinel announce-ip
:当前sentinel的ipsentinel monitor hmaster 192.168.140.101 7001 2
:sentinel monitor 主节点名 主节点ip 主节点端口 认定master下线的quorum值(sentinel监控配置)sentinel down-after-milliseconds hmaster 5000
:哨兵ping节点,超过5s就算超时sentinel failover-timeout hmaster 60000
:哨兵监测到主节点宕机后做故障恢复,如果故障恢复又失败了, 再过60s再次做故障恢复。
Redis分片集群
搭建分片集群
【问题】主从和哨兵可以解决高可用、高并发度的问题,但是还存在两个问题:海量数据存储问题;高并发写的问题。
【解决】使用分片集群可以解决,分片集群的特征:
- 集群中有多个master,每个master保存不同数据;
- 每个master可以有多个slave节点;
- master之间可以通过ping检测彼此健康状态
分片集群可以理解成有多个主从集群组合成的,且不需要哨兵节点,master之间可以通过ping来判断是否下线。
- 使用docker-compose部署,新建docker-compose.yaml文件:
yaml
version: "3.2"
services:
r1:
image: redis
container_name: r1
network_mode: "host"
entrypoint: ["redis-server", "--port", "7001", "--cluster-enabled", "yes", "--cluster-config-file", "node.conf"]
r2:
image: redis
container_name: r2
network_mode: "host"
entrypoint: ["redis-server", "--port", "7002", "--cluster-enabled", "yes", "--cluster-config-file", "node.conf"]
r3:
image: redis
container_name: r3
network_mode: "host"
entrypoint: ["redis-server", "--port", "7003", "--cluster-enabled", "yes", "--cluster-config-file", "node.conf"]
r4:
image: redis
container_name: r4
network_mode: "host"
entrypoint: ["redis-server", "--port", "7004", "--cluster-enabled", "yes", "--cluster-config-file", "node.conf"]
r5:
image: redis
container_name: r5
network_mode: "host"
entrypoint: ["redis-server", "--port", "7005", "--cluster-enabled", "yes", "--cluster-config-file", "node.conf"]
r6:
image: redis
container_name: r6
network_mode: "host"
entrypoint: ["redis-server", "--port", "7006", "--cluster-enabled", "yes", "--cluster-config-file", "node.conf"]
输入docker compose up -d
启动redis集群
- 使用命令创建集群:
进入任意节点容器:
docker exec -it r1 bash
执行命令:
redis-cli --cluster create --cluster-replicas 1 \
192.168.140.101:7001 192.168.140.101:7002 192.168.140.101:7003 \
192.168.140.101:7004 192.168.140.101:7005 192.168.140.101:7006
1
:表示副本数量,一个主、一个从redis会默认前三个是主节点,后三个是从节点
散列插槽
在redis集群中,共有16384个hash slots,集群中每个redis节点都会分配一定数量的hash slots:
redis数据不是与节点绑定,而是与插槽slot绑定。当读写数据时,Redis基于CRC16算法对key做hash运算,将得到的结果与16384取余,就计算出这个key的slot值。
redis在计算key的hash值又分成两种情况:
- key中包含{},根据{}之间的字符串计算hash slot
- key中不包含{},根据整个key字符串计算hash slot
user这个key计算出的hash值时5474,如果按照之前的方式建立连接,那么在集群1(范围:0-5460)中插入hash值为5474的key,会报错。
解决办法:集群模式下建立连接时,应该加上-c参数:
redis-cli -c -p 7001
Redis数据结构
RedisObject
redis中任意数据类型的键和值都会被封装成一个RedisObject,也叫Redis对象
type:数据类型(string、hash、list、set、zset)
encoding:数据在内存中的存储方式
lru:对象最近一次被访问的时间(太久没被访问的key会在内存不足时淘汰)
refcount:当前对象如果被别人引用了,这个值就会+1,如果为0也会被回收
*ptr:指向实际存放数据的内存地址
SkipList
跳表,首先是链表,与传统链表的差异:
- 元素按照升序排列
- 节点可以包含多个指针,指针跨度不同
跳表是一个有序的双向链表,每个节点可以包含多层指针(最多允许32层),层级越高,跨度越大,增删改查效率和红黑树基本一致,实现更简单,但是空间复杂度更高
SortedSet(zset)

特点:
- 每组数据都包含score和member
- member唯一
- 可根据score排序

dict:hashtable(存储score、member;member为键,score为值。可以满足member唯一性)
zsl:skiplist(存储score、member,在排序时根据score排序)
根据member得到score:去查hash表
直到某个元素的score排名:去hashtable里,根据member查到score,再去skiplist里根据score得到排名
Redis内存回收
内存过期处理
redis提供了expire命令,可以给key设置TTL(存活时间)
过期key处理
redis的本质还是键值型数据库,所有的数据都存储在redisDB的结构中,其中包含两个哈希表
:
- dict:保存redis中所有键值对
- expires:保存redis中所有设置了过期时间的key,以及到期时间(写入时间+TTL)
redis不会实时检测key的过期时间,它不会在key过期后立刻删除。而是采用两种延迟删除的策略:
- 惰性删除:当有命令需要一个key的时候,检查该key的存活时间,如果已经过期才执行删除。
- 周期删除:通过一个定时任务,周期性的抽样部分有TTL的key,如果过期则执行删除。
内存淘汰策略
内存淘汰:当Redis内存达到设置的阈值时,Redis就会主动挑选部分key删除以释放更多的内存。
Redis在每次处理客户端命令时,都会对内存使用情况判断,如果必要,则执行内存淘汰。内存淘汰的策略有:
- 前缀:
- allkeys:对所有的key进行淘汰,从dict的哈希表中挑选
- volatile:只对设置了TTL的key进行淘汰,从expires的哈希表中挑选
- 后缀:
- ttl:淘汰ttl小的
- random:随机挑选
- lru:基于LRU算法
- lfu:基于LFU算法
LRU(最近最少使用) :用当前时间 - 最后一次访问时间,值越大越先被淘汰(越久没被访问到的越先淘汰)
LFU(最少频率使用):统计每个key的访问频率,值越小越先被淘汰
那么redis怎么直到最近一次访问时间(LRU)或 访问次数(LFU)呢?
在redisObject这个数据结构里:
- 当使用LRU策略时,lru这个变量存储的是以秒为单位的最近一次访问时间。
- 当使用LFU策略时,lru这个变量:
- 高16位以分钟为单位的最近一次访问时间。
- 低8位记录
逻辑访问次数
。
Redis缓存问题
缓存一致性
保证缓存一致性主要有三种模式:
Cache Aside Pattern
(常用)- Read / Write Through Pattern
- Write Behind Caching Pattern
Cache Aside模式
由业务的开发者 在更新数据库的同时更新缓存。有一定的业务侵入,但是一致性更好
【注1】:如何保证redis与数据库的一致性?
从数据库中
查
一条数据,可以直接把这条数据存到redis缓存中;但是如果需要做
增删改
操作时,redis中可以直接删除,没必要对redis中的数据也做同样的增删改操作。【注2】:在做增删改操作时,应该先删除redis再改数据库,还是先改数据库再删redis?
如果先删redis,再改数据库:当有一个线程A需要删除数据时,它先将redis中的数据删除,来到数据库,此时又有一个线程B来查询同一条数据,先去查询redis未命中,也来到数据库中,此时如果是线程B先执行了查询操作,线程B将查询后的结果又存入redis中,线程A后把这条数据删除。但是redis中却存在线程B的数据,就出现了数据的不一致性,所以需要先删除数据库再改redis。
Read / Write Through模式
缓存与数据库整合为一个服务(没有现成的哈哈哈哈哈),由服务来维护一致性。业务开发者直接调用该服务接口,无需关心一致性问题。
Write Behind Caching模式
增删改查业务直接基于缓存,由其他线程异步调用的将缓存数据持久化到数据库,保证最终一致性。(高性能,弱 / 最终一致性)
缓存穿透
缓存穿透:客户请求的数据在数据库中不存在,就不会写入缓存,这将导致每次查询这个该数据都会去访问数据库,可能导致数据库挂掉。
只要用户请求的是不存在的数据,那么每次请求都会发到数据库中
缓存空对象
实现简单,维护方便,但是会有额外的内存消耗。
布隆过滤
请求过来会先经过布隆过滤器,布隆过滤器先判断数据库中是否存在这条数据,如果不存在,就会拒绝这个请求。
注意,布隆过滤器判断一个元素不存在时,它绝对不存在;但是如果它判断一个元素存在,这个元素可能会不存在。
缓存雪崩
缓存雪崩:在同一时段,大量的缓存key同时失效 ,或redis服务宕机 ,导致大量请求到达数据库,带来巨大压力。
解决:
- 给不同的key的TTL添加随机值(避免大量的缓存key同时失效)
- 利用redis集群提高服务的可用性(避免服务宕机)
- 给缓存业务添加降级限流策略(防止大量的请求过来)
- 给业务添加多级缓存(建立nginx缓存、JVM本地缓存...)
缓存击穿
缓存击穿(热点Key问题):一个被高并发访问 并缓存重建业务较复杂 的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
解决方案:
-
互斥锁:一个线程查询数据库的时候加锁,此时其他线程都无法访问。
-
逻辑过期:线程1第一次来查询这个数据,发现数据过期,就先获取互斥锁,(此时会开启一个新的线程,线程1暂时返回过期数据)。
开启的新线程(线程2)就重新查询数据,写入缓存,最后释放锁。
在新线程(线程2)写入缓存这一过程中,如果还有别的线程(线程3、4)要访问,它先去获取互斥锁,发现互斥锁之前已经被别的线程获取了,此时它也直接返回过期的数据。直到数据被更新为止。

穿透就是毫无攻击性直达数据库,因为是不存在的缓存,雪崩就是同时过期或宕机,击穿就是有攻击性因为是存在的缓存跟它战斗然后击穿。