目标:
整理redis知识,主要包含如下内容:
1、redis复制
2、redis持久化
3、redis线程模型
4、redis常见问题处理
5、redis高可用
6、redis过期策略
7、redis实现分布式锁
8、redis管道/事务/lua脚本
9、redis基础

第一部分: 复制
1 redis的主从复制是如何实现的?
Redis包含部分重新同步和完全重新同步。
当从实例连接到主实例,发送(master_replid;master_repl_offset)
请求来进行部分同步,(master_replid;master_repl_offset)表示与主实例同步的最后一个快照。
如果主实例接受部分同步,则会从从实例上次停止的最后一个偏移处进行增量同步,具体是主实例将
backlog缓冲区命令发送给从实例;
否则,表示完全重新同步,主实例将数据转存到RDB文件中,将该文件发送给从实例,从实例将内存
中所有数据清除,再将RDB文件中数据导入。

第二部分: 持久化
1 如何实现持久化?
持久化就是把内存数据写到磁盘,防止数据丢失。
Redis包含RDB(默认)和AOF两种持久化方式。
RDB:
Redis DataBase缩写,内存终数据对象通过rdbSave来生成RDB文件,
通过rdbLoad将文件加载到内存。

AOF:
Append-only file缩写,服务器执行任务时flushAppendOnlyFile函数被调用。
WRITE: 根据条件将缓存写入AOF文件
SAVE: 根据条件将AOF文件保存到磁盘

aof比rdb更新频率高,优先使用aof还原数据,aof比rdb安全。

第三部分: 线程模型
1 redis为什么快?
纯内存操作,单线程操作避免上下文切换,采用非阻塞I/0多路复用

2 redis为什么单线程?
Redis基于内存操作,cpu不是瓶颈,单线程容易实现,并且避免上下文切换和竞争条件,
采用队列技术将并发访问变成串行访问。

3 redis线程模型原理?
包含: 套接字,I/O多路复用程序,事件分派器,事件处理器。
)I/O 多路复用程序负责监听多个套接字, 并向文件事件分派器传送那些产生了事件的套接字。
I/O 多路复用程序总是会将所有产生事件的套接字都入队到一个队列里面, 然后通过这个队列
每次一个套接字的方式向文件事件分派器传送套接字:
当上一个套接字产生的事件被处理完毕之后(该套接字为事件所关联的事件处理器执行完毕)

第四部分: 常见问题处理
1 redis缓存穿透问题
缓存系统,都是按照key去缓存查询,如果不存在对应的value,就应该去后端系统查找(比如DB)。
恶意请求故意查询不存在的key,请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透。
处理方法:
1) 对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该key对应的数据insert了之后清理缓存。
2) 对一定不存在的key进行过滤。

2 redis缓存雪崩问题
当缓存服务器重启或者大量缓存集中在某一个时间段失效,导致系统崩溃。
处理方法:
1) 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
2) 做二级缓存,A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期

3 Redis缓存和数据库会存在一致性问题吗?怎么解决
会出现。解决具体思路: 先删缓存,再更新数据库
1) 主要思路
更新的时候,先删除缓存,再更新数据库(防止如果先更新数据库然后删除缓存失败带来不一致)
读取的时候,先读缓存,如果没有,就读数据库并将数据放入缓存。
并将修改DB和更新缓存的操作放在一个队列中。来保证
更新缓存一定是在DB修改之后。
具体将对同一个数据的读写路由到同一个队列,可以根据数据的唯一标识,计算哈希,
通过一致性哈希,路由到同一个队列上完成


2)其他措施
解决缓存删除失败问题: 添加重试机制,一致性要求越高,重试越快
给缓存设置失效期

第五部分: 高可用
redis高可用采用的是哨兵(sentinel),多个redis-slave配备了多个哨兵进程,哨兵监控redis-master,
一旦出现故障,将一台slave提升为master。
客户端通过连接哨兵来获取Redis的master地址,发生故障,哨兵会报告新的服务器地址。

1 主备切换流程是什么?
1) 一个哨兵认为master不可用,此时被仍为主观不可用,当有指定个数的哨兵都认为master不可用,此时状态进入客观不可用,进入主备切换流程。
2) 进入主备切换流程后,需要一定个数的哨兵都同意进行进行主备切换授权,此时才真正开始进行主备切换。
3) 开始进行主备切换的时候,一个sentinel被授权, 获得挂掉的master的最新配置版本号,主备切换后,该版本号用于最新配置。
4) 一个sentinel成功对master进行主备切换,会把最新配置通过广播形式告诉其他sentinel,其他sentinel则更新对应master配置。
5) 当将一个slave选举为master并发送命令后,即使其他slave还没有针对新master重新配置自己,主备切换也被认为是成功的,所有sentinels将会发布新的配置信息。
一个相互通信的sentinel集群最终会采用版本号最高且相同的配置。

2 Sentine之间和Slaves之间是如何自动发现的?
sentinel利用master的发布/订阅机制自动发现其他的sentinel节点
每个sentinel向每个master和slave发布/订阅频道 __sentinel__:hello 每秒发送一次消息来宣布存在,
每个sentinel订阅每个master和slave的频道__sentinel__:hello的内容来发现未知sentinel,
检测新的sentinel,则加入自身维护的master列表。每个sentinel发送的消息中包含其当前维护的最新master配置,
如果某个sentinel发现自己配置版本低于接收到的配置版本,则用新配置更新自己的master配置。

3 候选人规则是怎样的?
1) slaves优先级越小排名越靠前
2)优先级相同,看复制下标,哪个从master接收的复制数据多,就越靠前。
3)优先级和下标相同,选择进程ID较小的哪个。

第六部分: 过期策略
redis采用的是定期删除+惰性删除策略。
定期删除,redis默认每个100ms检查,随机抽取一定数量的key,检查是否过期,过期则删除。
惰性删除在获取某个key的时候,redis会检查如果这个key设置了过期时间那么是否过期了,过期则删除。
allkeys-lru:从数据集中挑选最近最少使用的数据淘汰

第七部分: 实现分布式锁
使用SETNX命令+EXPIRE实现分布式锁。
将 key 的值设为 value ,当且仅当 key 不存在。 若给定的 key 已经存在,则 SETNX 不做任何动作
解锁:使用 del key 命令就能释放锁
解决死锁:
1)通过Redis中expire()给锁设定最大持有时间,如果超过,则Redis来帮我们释放锁。
2)使用 setnx key “当前系统时间+锁持有的时间”和getset key “当前系统时间+锁持有的时间”组合的命令就可以实现。

第八部分: 管道/事务/lua脚本
redis管道:
将多个命令打包在一起,并将它们一次性发送,减少网络传输传输时间

redis事务:
Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的
Redis会将一个事务中的所有命令序列化,然后按顺序执行。
1).如果在一个事务中的命令出现错误,那么所有的命令都不会执行;
2).如果在一个事务中出现运行错误,那么正确的命令会被执行。
命令:
1)MULTI命令用于开启一个事务,它总是返回OK。 MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
2)EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。 当操作被打断时,返回空值 nil 。
3) WATCH 命令可以为 Redis 事务提供 check-and-set (CAS)行为。 可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。

redis lua脚本:
当业务场景涉及付出咋的逻辑判断,最好选择lua脚本而非事务。

第九部分: 基础
1 redis是什么?
基于内存的轻量级键值数据库。

2 redis数据类型有哪些?分别适用什么?
String字符串:
格式: set key value

Hash(哈希)
格式: hmset name  key1 value1 key2 value2
Redis hash 是一个键值(key=>value)对集合。
hash特别适合用于存储对象。

List(列表)
Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)
格式: lpush  name  value
在 key 对应 list 的头部添加字符串元素
格式: rpush  name  value
在 key 对应 list 的尾部添加字符串元素
格式: lrem name count value
key 对应 list 中删除 count 个和 value 相同的元素
格式: llen name  
返回 key 对应 list 的长度

Set(集合)
格式: sadd  name  value
Redis的Set是string类型的无序集合。
集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。

sorted set
sorted set多了一个权重参数score,集合中的元素能够按score进行排列。可以做排行榜应用,取TOP N操作

3 redis的key如何寻址的?
2)数据库的所有键值都存储在redisDb.dict中
,那么我们要知道如果找到key的位置,就有必要了解一下dict 的结构了:

typedef struct dict {
 // 特定于类型的处理函数
 dictType *type;
 // 类型处理函数的私有数据
 void *privdata;
 // 哈希表(2个)
 dictht ht[2];
 // 记录 rehash 进度的标志,值为-1 表示 rehash 未进行
 int rehashidx;
 // 当前正在运作的安全迭代器数量
 int iterators;
 } dict;


由上述的结构可以看出,redis 的字典使用哈希表作为其底层实现。dict 类型使用的两个指向哈希表的指针,其中 0 号哈希表(ht[0])主要用于存储数据库的所有键值,而1号哈希表主要用于程序对 0 号哈希表进行 rehash 时使用,rehash 一般是在添加新值时会触发,这里不做过多的赘述。所以redis 中查找一个key,其实就是对进行该dict 结构中的 ht[0] 进行查找操作。
4、既然是哈希,那么我们知道就会有哈希碰撞,那么当多个键哈希之后为同一个值怎么办呢?redis采取链表的方式来存储多个哈希碰撞的键。也就是说,当根据key的哈希值找到该列表后,如果列表的长度大于1,那么我们需要遍历该链表来找到我们所查找的key。当然,一般情况下链表长度都为是1,所以时间复杂度可看作o(1)。

当redis 拿到一个key 时,如果找到该key的位置。
了解了上述知识之后,我们就可以来分析redis如果在内存找到一个key了。
1、当拿到一个key后, redis 先判断当前库的0号哈希表是否为空,即:if (dict->ht[0].size == 0)。如果为true直接返回NULL。
2、判断该0号哈希表是否需要rehash,因为如果在进行rehash,那么两个表中者有可能存储该key。如果正在进行rehash,将调用一次_dictRehashStep方法,_dictRehashStep 用于对数据库字典、以及哈希键的字典进行被动 rehash,这里不作赘述。
3、计算哈希表,根据当前字典与key进行哈希值的计算。
4、根据哈希值与当前字典计算哈希表的索引值。
5、根据索引值在哈希表中取出链表,遍历该链表找到key的位置。一般情况,该链表长度为1。
6、当 ht[0] 查找完了之后,再进行了次rehash判断,如果未在rehashing,则直接结束,否则对ht[1]重复345步骤。
到此我们就找到了key在内存中的位置了。

Redis 4.x Cookbook