-
- 1.1. Memcache与Redis的区别
- 1.2. Redis为什么单线程依旧能保持快速
- 1.2.1. Redis单线程模式的执行流程
- 1.2.2. Redis有多线程吗?
- 1.3. Key的过期策略有哪些?有哪些优缺点?
- 1.4. Redis的内存淘汰机制有哪些?LRU算法的实现?
-
- 3.1. AOF
- 3.1.1. AOF是什么?记录格式?写入流程?
- 3.1.2. 三种写回策略
- 3.1.3. AOF重写流程
- 3.1.4. AOF重写时Buffer
- 3.2. RDB
- 3.2.1. RDB是什么?与AOF区别?优缺点?
- 3.2.2. RDB执行方式
- 3.3. AOF和RDB混合
- 3.4. 大key对AOF和RDB的影响
- 3.1. AOF
-
- 4.1. 缓存穿透?缓存雪崩?缓存击穿?
- 4.2. 缓存更新策略?
- 4.2.1. Cache Aside 旁路缓存
- 4.2.2. Read/Write Through 读/写穿
- 4.2.3. Write Back(写回)
- 4.2.4. Cache Aside策略的缺点和解决
- 4.3. 数据库和缓存如何保证一致性?
- 4.4. 多级缓存设计原则
-
- 5.1. 如何设计一个缓存策略,可以动态缓存热点数据?
- 5.2. 分布式锁的解决方案?Redis实现分布式锁的缺点?Redission和RedLock算法是什么?
- 5.2.1. Redis的解决方案
- 5.2.2. 使用redission解决时间超时问题
- 5.2.3. RedLock算法解决主从复制问题
- 5.3. Redis实现消息队列三种方案
- 5.4. Redis实现延迟队列
- 5.5. Zset实现排行榜
-
- 6.1. 主从模式
- 6.2. 哨兵模式
- 6.2.1. 哨兵模式原理
- 6.2.2. 哨兵模式原理
- 6.2.3. 主观下线和客观下线
- 6.2.4. 哨兵模式故障切换过程?
- 6.3. 集群cluster模式
- 6.3.1. 什么是cluster集群模式
- 6.3.2. hash slot算法
- 6.3.3. redis cluster的主观下线和客观下线
- 6.3.4. 故障转移过程
-
- Redis功能
- 7.1. Redis命令
- 7.2. Redis的大key如何处理?
- 7.3. Redis虚拟内存
- 7.4. Redis事务实现
- Redis功能
-
- 杂题
- 8.1. PING-PONG机制
- 8.2. 如何应对主从数据不一致?
- 8.3. 主从切换如何减少数据丢失?
- 杂题
1. 高性能
1.1. Memcache与Redis的区别
都是内存数据库
区别
- Redis支持更丰富的数据结构,但是Memcache只支持KV
- Redis通过AOF和RDB实现持久化,Memcache不支持持久化
- Redis支持原生集群模式,Memcache不支持
1.2. Redis为什么单线程依旧能保持快速
- Redis大部分操作都在内存中,并且采用了高效的数据结构,因此Redis瓶颈可能是机器的内存或者网络带宽,而非CPU。既然CPU不是瓶颈,那么自然采用单线程
- 采用多线程避免了多线程之间的竞争,省去了多线程切换带来的性能开销和保证线程安全的开销
- Redis采用IO多路复用机制epoll处理Socket请求
1.2.1. Redis单线程模式的执行流程
初始化
- 首先,调用epoll_create()创建一个epoll对象
- bind()绑定端口,listen()监听该socket
- epoll_ctl()将listen socket加入到epoll(),由内核管理,同事注册连接处理函数
事件循环
-
首先调用处理发送队列函数,查看发送队列里面是否有数据未发送,如果有则write()
-
epoll_wait()等待连接事件到来:
1. 如果是连接事件,accept()返回socket,epoll_ctl()加入到内核,内核中是一棵红黑树2. 读事件,read()->解析->处理->处理结果放到发送队列->等待发送 3. 写事件,write()

1.2.2. Redis有多线程吗?
指的是后台线程:关闭文件、AOF刷盘、释放内存
- redis将任务生产出来放入消息队列,后台线程消费者从消息队列取消息
Redis6.0之后性能瓶颈到了网络IO上,因此开三个线程处理网路IO请求
1.3. Key的过期策略有哪些?有哪些优缺点?
redis会对所有带有过期时间的key设置一个过期字典。
a. 判断key是否在过期字典,若不在,返回
b. 若在,则把超时时间与系统时间做比较
删除策略:
- 惰性删除:不主动,查找的时候如果超时了就删除
- 定期删除:随机抽取20个看没有过期,如果删除比例超过25%就重复,否则取消
设置过期时间的命令
EXPIRE key seconds可以设置过期时间
TTL key可以查询一个key的过期时间
1.4. Redis的内存淘汰机制有哪些?LRU算法的实现?
redis内存满了会发生内存淘汰
redis的内存淘汰策略共有8种:
- noeviction: 默认策略, 不会删除任何数据, 拒绝写入操作并返回 error, 并只相应读操作
- volatile-ttl: 删除最近要过期的数据
- volatile-lru: 根据 LRU 算法删除设置了超时的键, 直到足够的空间为止, 如果没有, 则选择 noeviction
- allkeys-lru: 根据 LRU 算法删除键, 直到足够的空间为止 ( 纯缓存服务器 )
- volatile-lfu: 根据 LFU 算法删除设置了超时的键, 直到足够的空间为止, 如果没有, 则选择 noeviction
- allkeys-lfu: 根据 LFU 算法删除键, 直到足够的空间为止 ( 纯缓存服务器 )
- volatile-random: 随即删除设置了过期的键, 直到足够的空间为止
- allkeys-random: 随机删除所有的键, 直到足够的空间为止
当 Redis 一直工作在内存溢出且设置非 noeviction 策略时, 会频繁回收内存. 影响服务器性能
不用传统的LRU?
- 链表带来空间开销
- 数据访问要放到链表头,如果有大量数据被访问,就会带来很多链表移动操作,会很耗时,进而会降低 Redis 缓存性能。
Redis的LRU实现?
Redis的对象结构RedisObject添加额外的字段:最后一次访问时间
随机选5个,踢出最久没有使用的那个
有缓存污染的问题:好久没用过但一直存在
Redis的LFU实现?
将LRU的redisObject的最后一次访问改成频次
2. 数据结构
2.1. String
可以是数字(整数或者浮点数),也可以是SDS
内部实现:
2.1.1. SDS
redis内部实现了字符串类,有别于C的字符串:
结构:free指针,len指针,buf指针(兼容C字符串)
区别:
- 获取字符串长度O(1),直接根据len字段
- 缓冲区安全,比如strcat()函数可能会因为分配的缓冲区不足导致溢出缓冲区
- 缓冲区预分配,小于 < 1MB free = len,大于IMB固定free = 1MB
- 二进制安全,特殊格式根据len而不是根据结束符
- 兼容<string.h>函数
2.1.2. 编码
结构:
redisObject有字段type、encoding、ptr
string对象的内部编码(encoding)有三种:int、raw和embstr,int的话ptr指向long,raw和embstr的话encoding指向SDS
2.1.3. API
SET KEY VALUE
GET KEY
EXISTS KEY
STRLEN KEY
DEL KEY
INCR NUMBER
INCREBY NUMBER 10
DECR NUMBER
EXPIRE KEY 60
TTL KEY
SET KEY VALUE EX 60
SETNX KEY VALUE
2.1.4. 应用场景
缓存对象
- 直接缓存整个对象的 JSON,命令例子:
SET user:1 '{"name":"xiaolin", "age":18}'。 - 采用将 key 进行分离为 user:ID:属性,采用 MSET 存储,用 MGET 获取各属性值,命令例子:
MSET user:1:name xiaolin user:1:age 18 user:2:name xiaomei user:2:age 20。
常规计数
因为 Redis 处理命令是单线程,所以执行命令的过程是原子的。因此 String 数据类型适合计数场景,比如计算访问次数、点赞、转发、库存数量等等。
分布式锁
SETNX不存在才加锁,则可以key不存在则表示加锁成功,key存在则表示加锁失败
一般上锁会加上过期时间
解锁的时候要判断unique_ptr是否为加锁客户端,是的话,才将lock_key删除。所以需要用lua脚本写解锁脚本保持原子性
共享session信息
分布式系统用redis存储session信息
2.2. List
内部实现:
- 如果列表的元素个数小于某个值(512),列表每个元素都小于一个值(64),使用压缩列表zipList
- 如果不满足则用双向链表
3.2版本之后使用quickList
ziplist怎么实现
本质是连续的数组,由一个个entry组成,每个entry有prelen表示前一个元素的长度(可以逆序遍历),encoding表示当前节点的实际数据的类型和长度,data保存了实际的数据
ziplist问题:
- 查找复杂度高
- 连锁更新问题,压缩列表在扩容的时候,如果新插入的元素较大,后续元素prelen都要变化(因为prelen1字节或者5字节记录,一开始分配1字节,新插入一个大于254字节的元素,那么e1就要prelen扩容为5字节),造成连锁更新
listpack
其不再记录前一个元素的长度,不再支持逆序遍历
quickList
本质是LinkedList + zipList,解决zipList因为全部连续导致的全部连锁更新问题。
2.2.1. API
LPUSH KEY [value ...]
RPUSH KEY [value ...]
LPOP key
RPOP key
LRANGE key start stop
BLPOP key [key ...] timeout 阻塞写入
BRPOP key [key ...] timeout 阻塞读出
2.2.2. 应用场景
消息队列
LPUSH阻塞写入队列
RPOP 阻塞从队列读出
为了唯一性,添加key的全局唯一id: ID:
可靠性:BRPOPLPUSH 命令,这个命令的作用是让消费者程序从一个 List 中读取消息,同时,Redis 会把这个消息再插入到另一个 List(可以叫作备份 List)留存。
2.3. Hash
满足512,64的时候使用zipList
不满足的时候使用hash_table:
- 采用拉链法来解决hash冲突,并在需要时进行rehash操作(负载因子=used/size有关,比如大于1且没有bgsave则扩容,大于5且在bgsave的时候扩容),在进行rehash时会使用两个hash表,一个用于当前使用,称为主hash表,一个用于扩容,称为备用hash表(渐进式hash,先分配ht[1]的空间,当进行rehash操作的时候用rehashIdx记录进度)
2.3.1. API
HSET key field value
HGET key field
HMSET key field [value ...]
HMGET key [field ...]
HDEL key [field ...]
HLEN key
HGETALL key
HINCRBY key field n
2.3.2. 应用场景
缓存对象
比如:
- 添加商品:
HSET cart:{用户id} {商品id} 1 - 添加数量:
HINCRBY cart:{用户id} {商品id} 1 - 商品总数:
HLEN cart:{用户id} - 删除商品:
HDEL cart:{用户id} {商品id} - 获取购物车所有商品:
HGETALL cart:{用户id}
2.4. Set
小于512,64时整数集合(有序整数数组,带有length,节约内存,查找O(N))
不满足则hash表
2.4.1. API
SADD key [member ...]
SREM key [member ...]
SMEMBERS key
SCARD key
SISMEMBER key member
SRANDOMEMBER key [count]
SPOP key [count]
// Set运算操作
SINTER [key ...]
SINTERSTORE destination [key ...]
SUNION [key ...]
SUNIONSTORE destination [key ...]
SDIFF [key ...]
SDIFFSTORE destination [key ...]
2.4.2. 应用场景
set 类型比较适合用来数据去重和保障数据的唯一性
在主从集群中,为了避免主库因为 Set 做聚合计算(交集、差集、并集)时导致主库被阻塞,我们可以选择一个从库完成聚合统计,或者把数据返回给客户端,由客户端来完成聚合统计。
点赞
保证一个用户只能点一个赞
SADD article:1 uid:1
SADD article:1 uid:2
SADD article:1 uid:3
上面有用户1,2,3给文章1点赞了
取消点赞
SREM article:1 uid:1
也可以获取给文章1点赞的用户列表和用户数量
共同关注
# uid:1 用户关注公众号 id 为 5、6、7、8、9
> SADD uid:1 5 6 7 8 9
(integer) 5
# uid:2 用户关注公众号 id 为 7、8、9、10、11
> SADD uid:2 7 8 9 10 11
(integer) 5
则uid:1和uid:2共同关注的公众号:
SINTER uid:1 uid:2
给uid:2推荐uid:1关注的公众号
SDIFF uid:1 uid:2
抽奖活动
保证一个用户不会中奖两次
- SADD放到抽奖箱
- 允许重复抽奖 SRANDOMEMBER
- 不允许重复抽奖 SPOP
2.5. Zset
小于512,64使用压缩列表
大于则用跳表skiplist
2.5.1. API
ZADD key [score member ...] 向集合中加入多个元素,每个元素有一个score,按从小到大排序
2.6. OTHERS
2.6.1. bitmap
Bitmap 本身是用 String 类型作为底层数据结构实现的一种统计二值状态的数据类型。
String 类型是会保存为二进制的字节数组,所以,Redis 就把字节数组的每个 bit 位利用起来,用来表示一个元素的二值状态,你可以把 Bitmap 看作是一个 bit 数组。
2.6.2. hyperloglog
hyperloglog是用来统计一个集合中不重复的元素个数,其只需要花费12KB的内容,就可以对接近2^64个数字进行去重操作
2.6.3. geo
geo类型支持给定经纬度,查询距离该点某个范围内的点的集合。比较适合用来实现附近的人。
2.7. 总结
string: 整数字符串=>long,否则 SDS
list, zset, hash在小于512个元素,且每个元素都小于64时会使用ziplist数据结构。set使用整数集合(都是整数元素)
否则list使用quicklist,zset使用skiplist,hash和set使用hash_table
3. AOF&RDB
3.1. AOF
3.1.1. AOF是什么?记录格式?写入流程?
AOF持久化,采用日志追加到磁盘AOF文件。重启可以恢复。默认不开启
记录格式:
*3
$3
set
$4
name
$7
xiaolin
AOF写入流程
先执行命令,再写log
why:高性能理念
- 错误命令的话,还要有恢复检查的开销
- 不会阻塞当前命令的执行
风险:执行的命令没写入日志,再次恢复没了
3.1.2. 三种写回策略
-
Always:每次写log都刷盘
-
EverySec:写入page cache,由每秒刷盘
-
N 由page cache决定
本质:fsync()函数负责page cache写盘,三个策略调fsync()时机不同
3.1.3. AOF重写流程
当AOF文件过大时,redis会执行重写机制(后台进程bgrewriteaof ),也就是扫描AOF文件,并把key重复的合并成一条命令。
后台进程的好处(写时复制)
- 子进程AOF重写期间,主进程可以继续处理命令请求,从而避免阻塞
- 线程共享内存,需要加锁保证数据安全(有什么需要父子进程区分的数据吗?)。进程采用COW写时复制,一开始共享,写的时候又独立,不需要加锁。
3.1.4. AOF重写时Buffer
主进程修改key-value,主子进程内存数据不一致怎么办?
父进程把改变写到父子进程都能感知的一块缓存中,等待子进程执行完AOF重写后主进程一次把改变写入AOF文件即可
具体:主进程不断把修改写入AOF重写缓冲区。子进程重写AOF文件完,用信号(异步)通知主进程,主进程调用信号处理函数,一次把缓冲区更改写入AOF文件,然后新的AOF覆盖原有的。
重写过程什么时候会发生阻塞?
- 写时复制
- 主进程一次将AOF重写缓冲区写入AOF文件
3.2. RDB
3.2.1. RDB是什么?与AOF区别?优缺点?
- AOF日志的是原命令,RDB快照记录的是二进制数据。因此RDB快照在恢复时直接倒入内存即可,而AOF需要一条一条的执行命令。
- 优点
备份速度快
- 缺点
1.非实时,需要手动调用
2.bgsave因为写时复制所以会备份期间主进程操作丢失,且cow可能很多,save的话主进程会阻塞
3.2.2. RDB执行方式
-
save:主进程阻塞
-
bgsave:会创建一个子进程来生成 RDB 文件,这样可以避免主线程的阻塞;Redis 还可以通过配置文件的选项来实现每隔一段时间自动执行一次 bgsave 命令,例如使用
save 300 10表示在300秒之内,对数据库进行了至少10次修改。
RDB 文件的加载工作是在服务器启动时自动执行的,Redis 并没有提供专门用于加载 RDB 文件的命令。
3.3. AOF和RDB混合
写时复制,bgsave fork自主进程,这样就能不阻塞主进程。
但是会丢失掉fork期间主进程的修改,所以就有了混合日志
混合日志指:在使用RDB备份的时候,使用AOF缓冲区记录主进程的修改,这样既能提高备份速度,也能提高数据正确性。
3.4. 大key对AOF和RDB的影响
fork子进程的时候写时复制其页表也阻塞时间比较长,开启了内存大页机制更是。
4. 缓存设计
4.1. 缓存穿透?缓存雪崩?缓存击穿?
4.1.1. 什么是缓存穿透?
数据库和缓存都没有数据,大量这样的请求到来
解决:
- 非法请求过滤,API入口做请求参数校验
- 在缓存中构建NULL或者空值
- 业务线程通过布隆过滤器快速判断(BITMAP)
4.1.2. 什么是缓存雪崩?
redis的key过期(因为缓存一致性)时间集中,大量请求同时访问
解决
- key过期时间设随机数
- 设置缓存不过期,后台线程更新缓存
4.1.3. 什么是缓存击穿?
热点 KEY 失效之后会有大量对该key的请求打到数据库
解决
- setNX设置互斥锁,同一时间只有一个线程执行业务,未获取到锁的线程要么重建缓存,要么设置空值
- 不设置过期时间,后台线程更新缓存
4.2. 缓存更新策略?
4.2.1. Cache Aside 旁路缓存
写策略
先更新数据库,在删除缓存
- 反过来不行:

- 正过来行:

因为缓存写入快于数据库IO,可能性很小
读策略
命中缓存读缓存,不命中读数据库,然后写入缓存
写多的话,会频繁清除缓存,不好
可以在写入数据库时也写入缓存,但是加setNx分布式锁,例子如下:
对于read操作,先1read,读miss,然后2 read读db,此时拿到了version1的数据,准备写cache,但是此时gc卡住了,对于write操作,先写db,数据变为version2,写cache,cache中的数据变为version2,然后read的gc完成,把cache中的数据写为version1。这样就有脏数据了。
4.2.2. Read/Write Through 读/写穿
本质:用户只与缓存打交道不与数据库打交道
读策略
存在返回,不存在由缓存组件查出来写入缓存
写策略
存在则该缓存并写入库,不存在则直接更新库
问题
缓存组件做不到
4.2.3. Write Back(写回)
改缓存为脏,后台写库
Redis 并没有异步更新数据库的功能。用不了
不是强一致性,因为断电了脏数据就没了
4.2.4. Cache Aside策略的缺点和解决
问题1:写策略中写数据库与删缓存并不是原子性的,删缓存的时候失败了怎么办?
消息队列补偿,删除操作写入canal或者消息队列,不断重试
问题1:读缓存的写操作延迟,当有另外的新线程写删缓存后才写入,那么最终写入脏数据
4.3. 数据库和缓存如何保证一致性?
问题:假如先写库再写缓存,则A线程写库1,B线程写库2,B线程写缓存2,A线程写缓存1,数据库与缓存不一致
先写缓存再写库是一样的,但是Cache Aside不容易发生,所以可以保证缓存一致性
再给缓存设一个过期时间,设一个写延时双删策略(删缓存后隔500ms再删)
这样会使缓存命中率降低,所以可以写库以后再写入缓存,只不过加一个setNx分布式锁
还有别的方法是mysql binlog用canal提取,接入kafka并任务补偿重试给redis
4.4. 多级缓存设计原则
4.4.1. 场景
BFF 聚合数据时 上下游数据缓存。例如账号服务 下的 vip服务,基本信息服务,积分荣誉服务等。每个子服务都有自己的缓存 这时候是先更新下游还是上游呢
4.4.2. 解决方案
- 清理的优先级:先下游(子服务)再上游,类似cache Aside
- 下游过期时间要大于上游,防止cache miss
分布式技术(内含面试例子)—— 1、事务、存储、缓存 - 知乎 (zhihu.com)
5. Redis应用场景
5.1. 如何设计一个缓存策略,可以动态缓存热点数据?
通过数据最新访问时间来做排名,并过滤掉不常访问的数据,只留下经常访问的数据。
- Zset,score是访问时间
- 系统会定期过滤掉队列中排名最后的 200 个商品,然后再从数据库中随机读取出 200 个商品加入队列中;
- 这样当请求每次到达的时候,会先从队列中获取商品 ID,如果命中,就根据 ID 再从另一个缓存数据结构中读取实际的商品信息,并返回。
相关命令 :ZRANGE (从小到大排序) 、 ZREVRANGE (从大到小排序)
5.2. 分布式锁的解决方案?Redis实现分布式锁的缺点?Redission和RedLock算法是什么?
常见的锁是为了解决两个线程占用同一内存资源的问题。分布式锁是为了解决两个应用占用相同资源的问题。
5.2.1. Redis的解决方案
- 加锁:setNx,key加超时时间防止client挂掉死锁 SET key client_id NX PX 10000
- 解锁:两个步骤,需要lua保证原子性:
// 释放锁时,先比较 是否和client_id 相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
redis无法判断客户端,所以用client_id比较。
缺点
- 超时时间不好控制,解决办法启动daemon thread,只要application仍然拿着锁,且锁快要过期了,就去续约,这个可以通过redission来解决超时时间的问题
- 在集群环境下,如果master挂了,且数据没有同步,那么新来的master是没有记录锁数据的,这种情况会导致严重的并发问题
5.2.2. 使用redission解决时间超时问题
只要线程一加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下,如果线程1还持有锁,那么就会不断的延长锁key的生存时间。因此,Redisson就是使用Redisson解决了锁过期释放,业务没执行完问题。
5.2.3. RedLock算法解决主从复制问题
思想借鉴了raft的超过半数投票:向N个redis实例发送加锁请求,每个都有个过期时间戳 + TTL,如果大多数redis加锁成功才会加锁成功,如果只有少数则释放已经加的锁,重复尝试加锁。(选master会选靠谱的,一定是加了锁的?且就算挂掉一个也是还能找到加锁的)
代码:Redis分布式锁与Redlock算法实现_Redis_脚本之家 (jb51.net)
5.3. Redis实现消息队列三种方案
5.3.1. List
lpush和rpop轮询,lbpush和rbpop阻塞
问题
-
不能重复消费
-
单消费者
5.3.2. Pub和Sub
Redis2.0之后引入的消息传递模型,消费者可以订阅一个或多个channel,生产者向对应的channel发送消息后,所有订阅者都能收到相关信息。
API
SUBSCRIBE [channel ...] 订阅一个或多个频道
PUBLISH channel msg 向频道发msg
PSUBSCRIBE pattren 匹配订阅多个频道
问题
-
不能重复消费
-
有数据上限
5.3.3. stream
问题
- 主从复制的时候消息可能丢失
- redis的数据都存在内存中,因此一旦发生消息挤压就有oom的风险
Redis消息队列的三种实现方式_Redis_脚本之家 (jb51.net)
5.4. Redis实现延迟队列
利用zset的score
score是下单时间 + 超时时间
定时或轮询zrangebysocre查询满足要求的任务(当前时间大于score的)
应用场景
1.订单超时处理
延迟队列可以用于处理订单超时问题。当用户下单后,将订单信息放入延迟队列,并设置一定的超时时间。如果在超时时间内用户未支付订单,消费者会从延迟队列中获取到该订单,并执行相应的处理操作,如取消订单、释放库存等。
2.优惠券过期提醒
延迟队列可以用于优惠券的过期提醒功能。将即将过期的优惠券信息放入延迟队列,并设置合适的延迟时间。当延迟时间到达时,消费者将提醒用户优惠券即将过期,引导用户尽快使用。
3.消息重试机制
延迟队列可以用于实现消息的延迟重试机制。当某个消息处理失败时,将该消息放入延迟队列,并设置一定的延迟时间。在延迟时间过后,消费者再次尝试处理该消息。这可以用于处理网络请求失败、数据库写入异常等情况下的消息重试。
4.异步通知与提醒
延迟队列可以用于异步通知和提醒功能。例如,当用户完成某个操作后,系统可以将相关通知消息放入延迟队列,并设置一定的延迟时间,以便在合适的时机发送通知给用户。
问题
高并发情况下一个多个线程可能同时处理同一个订单,所以需要加分布式锁
5.5. Zset实现排行榜
score = 分数 + 时间戳/1e13
相关命令 :ZRANGE (从小到大排序) 、 ZREVRANGE (从大到小排序)、ZREVRANK (指定元素排名)。
Redis有序集合命令ZREVRANGEBYSCORE详解与应用-CSDN博客
6. Redis部署模式
6.1. 主从模式
分为全量复制和增量复制两种
replaceof 确定主从关系
6.1.1. 全量复制
- 数据同步阶段
i. 第一次复制,slave向master发送sync,offset = -1
ii. master收到sync,执行bgsave,生成RDB
iii. master向slave发送RDB
iv. slave收到RDB,载入,解析
- 命令传播阶段
i. master增删时,命令写入repliaction buffer(每个从服务器都有一个),发送给从节点
ii. 两者通过心跳机制(TCP长连接)维护
6.1.2. 增量复制
- 数据同步阶段
i. 网络延迟后,slave向master发送sync,offset
ii. master维护一个环形repl_backlog_buffer缓存,根据自己的offset和从节点的offset的差决定要发的命令
iii. 如果在repl_backlog_buffer里面,则加入对应从服务器的repliacation_buffer准备发送,如果不在里面则全量复制
6.2. 哨兵模式
6.2.1. 哨兵模式原理
使用哨兵模式,当主节点出现故障的时候,由哨兵自动完成故障的转移,并通知应用方,实现高可用。
四个功能:
- 监控主从节点状态
- 通知故障
- 故障转移,选出新主节点
- 告诉客户端新主节点
6.2.2. 哨兵模式原理
通过哨兵模式启动redis后,哨兵进程会自动监控master/slave的运行状态,基本原理是心跳机制 + 投票机制
心跳机制:每个哨兵向其他哨兵,master,slave发送定时消息,确认对方是否活着,如果发现对方在指定时间内未响应,则暂时认为对方宕机(主观下线)
投票机制:如果哨兵群中多数哨兵(投票)都认为一个master宕机了,就通过投票法从剩下的slave节点中,选择一台升级为master节点
6.2.3. 主观下线和客观下线
- 主观下线:心跳检测,超过一段时间没有回复
- 客观下线:当一个哨兵认为主观下线后,向其他哨兵咨询,超过一半哨兵认为该master有问题,就会认为客观下线。(只有master分主客观下线)
6.2.4. 哨兵模式故障切换过程?
四步:
- 从旧主节点的从节点中选择一个作为新的主节点
- 将其余的从节点修改复制目标为新的主节点(slaveof命令)
- 将主节点的ip通过发布者/订阅者机制通知客户端
- 监控旧主节点,上线后让它改为从节点
6.3. 集群cluster模式
6.3.1. 什么是cluster集群模式
哨兵模式并不具备sharding能力
哨兵模式需要部署单独的哨兵节点,哨兵的可用性会影响集群的可用性
CLUSTER MEET握手,加入集群
6.3.2. hash slot算法
常见的hash算法不好扩容
hash slot算法在加入或更改节点的时候只重新分配桶。每个node负责一部分slot的范围
16384个槽
6.3.3. redis cluster的主观下线和客观下线
主观下线:集群中每个节点都会定期向其他节点发送ping,当 cluster-note-timeout 时间内某节点无法与另一个节点顺利完成 PING 消息通信时, 则将该节点标记为主观下线状态
客观下线:当一个节点下线之后, 相应的节点状态会跟随消息在集群内传播, 当有半数持有槽的主节点都标记了某个节点是主观下线时, 就会进行客观下线, 通过集群广播下线节点的 fail 信息 ( 故障节点的 id, 并通知故障节点的从节点触发故障转移 )
6.3.4. 故障转移过程
- 从所有节点中选择一个作为新的节点
- 新的主节点负责之前的hash slot
- 发送pong消息,通知集群已经完成了故障转移
7. Redis功能
7.1. Redis命令
客户端->指令缓冲区->封装为协议(AOF文件格式一样)->发送命令(需要排队)->Redis解析器->Redis预处理->Redis执行器
lua和aof都可以作为伪客户端使用
如何一次发送多个
- MSet
- 管道pipline
- lua脚本(支持原子性)
redis pipline
pipline可以一次发送多条指令以降低网络IO,使用Send添加指令,使用Exec一次性执行。(节省RTT)
7.2. Redis的大key如何处理?
大key指的不是key大而是key对应的value大
String类型的值大于10KB
Hash,List,Set,ZSet类型的元素个数超过5000个
大key的影响
客户端超时阻塞
如何找到大key?
-
redis-cli —bigkeys找到大key
-
使用scan枚举所有的key,然后计算value大小:
i. String类型,可以直接STRLEN获取
ii. 集合类型,如果能知道元素平均内存大小,通过命令再获取集合元素个数:
List: LLEN Hash:HLen Set:SCARD ZSet:ZCARD
- RdbTools工具查找大key
如何删除大key?
- 分批次删除
例如大set,使用scan扫描后可以每次扫描集合中的100个元素,然后使用srem命令每次删除一个键
- 异步删除
unlink代替del,unlink会交给异步线程删除
7.3. Redis虚拟内存
其实就是内存淘汰策略,没有使用os的swap机制,而是自己制定了淘汰策略
7.4. Redis事务实现
命令:MULTI EXEC DISCARD WATCH
流程
- 服务器接收客户端的命令,如果是MULTI,会将redisClient中的flags属性的REDIS_MULTI标识打开(client.flag |= REDIS_MULTI)
- 开启事务后,服务器收到get/set操作,会加入queue队列,返回QUEUED
- 遇到EXEC指令,该命令会立即被服务端执行。服务端会遍历整个客户端的事务队列,(如果开启了WATCH要先坚持client是否状态为REDIS_DIRTY,是的话拒绝执行)执行队列中的所有命令,最后将结果全部返回给客户端
WATCH用法
- Redis维护了watch_keys字典,该字典的结构为<key, List<client_id>>,当某个client修改了key时,就会去watch_keys字典找到该key对应的其他client_id,并修改对应的client_id为REDIS_DIRTY
- 在事务EXEC之前如果client_id是REDIS_DIRTY则拒绝执行
ACID
- 原子性:不支持事务回滚,只支持入队时候检查出错就都不执行,EXEC出错还是会继续下一个
- 一致性(满足):i. 入队检测错误 ii. 执行检测错误不执行 iii. 宕机AOF和RDB恢复
- 持久性:AOF开启always选项才能保证
8. 杂题
8.1. PING-PONG机制
redis节点间总是每隔一段时间向别的节点(这里可以不分主从关系)发送PING,从节点收到后发送PONG,来感知对方的状态。还可以通过这个携带自己的复制偏移量
8.2. 如何应对主从数据不一致?
redis的主从模式下和raft有区别的,redis的master节点执行完命令就发送客户端已完成了,这就有可能导致主从不一致
解决:
只能自己手动控制,比如读从服务器的时候先通过命令判断以下(INFO REPLICATION)跟主节点的offset的差距。
8.3. 主从切换如何减少数据丢失?
两种情况:
- 异步复制同步丢失:主节点还没给从节点就宕机了
- 脑裂
第一条解决就是限制数据复制, 在redis中配置,主节点必须要要与至少n个从节点连接,且主从数据复制和同步的延迟不能超过m秒。
第二条就是哨兵模式