本文是对《Redis 核心技术与实战》的学习总结,很不错的课程,推荐学习。
一、架构

Redis可分为六大模块,分别是网络访问模块(通过网络访问框架进行Redis的访问,扩大了Redis的使用范围)、基于不同的value类型的操作模块(针对不同的数据类型,提供了不同的接口)、索引模块()、存储模块(AOF/RDB)、高可用集群支撑模块(主从复制、哨兵机制)、高可扩展集群支撑模块(数据分片)。
二、快的原因
1、基于内存;
2、k-v的数据结构,查找时间复杂度O(1);
3、单线程,避免上下文切换和多线程切换消耗CPU;
4、单进程:避免上下文切换和多进程切换消耗CPU;
5、IO多路复用机制,避免IO阻塞;
6、自建了VM机制。
三、支持的数据类型及底层的数据结构
数据类型:String、List、Hash、Sorted Set、Set、BitMap等。

四、AOF
1、写后日志;
2、AOF记录的是命令;
3、先执行命令,后将命令记录到AOF日志。

优:保证了记录的命令语法正确;
缺:未记录日志就宕机,如果redis用作缓存,可从数据库恢复数据;如果用做数据库,无法用日志进行恢复;
4、写回策略
appendfsync 的三个可选值:

5、重写日志
为避免线程阻塞,主线程 fork 出后台的 bgrewriteaof 子进程,子进程是会拷贝父进程的页表,即虚实映射关系,而不会拷贝物理内存,子进程复制了父进程页表,也能共享访问父进程的内存数据了,此时,类似于有了父进程的所有内存数据。
一个拷贝,两处日志:

五、RDB
1、RDB即内存快照,能记录内存数据某一时刻的数据状态,记录的是数据;
2、通过save 和 bgsave生成RDB文件:
save:在主线程中执行,会阻塞主线程,不可取;
bgsave:主线程fock出子线程,在子线程中进行RDB的写入,避免主线程阻塞;
3、键值对被修改时,会生产一个该数据的副本,主线程在副本进行数据的修改

4、每秒全量快照的问题
频繁进行全量数据写入磁盘,磁盘压力大,也会频繁fork子线程,造成主线程阻塞。
5、增量快照
需要记录哪些数据被修改,会带来额外的开销。
6、RDB和AOF混合
RDB全量快照+AOF记录快照期间修改的数据
六、主从同步
启动多个 Redis 实例时,它们相互之间可以通过 replicaof(Redis 5.0 之前使用 slaveof)命令形成主库和从库的关系。
1、主从库间数据第一次同步的三个阶段

runID:是每个 Redis 实例启动时都会自动生成的一个随机 ID,用来唯一标记这个实例,当从库和主库第一次复制时,因为不知道主库的 runID,所以将 runID 设为“?”;
offset:此时设为 -1,表示第一次复制;
FULLRESYNC:表示第一次复制采用的全量复制;
replication buffer:记录 RDB 文件生成后收到的所有写操作。2、主从级联模式分担全量复制时的主库压力

3、一旦主从库完成了全量复制,它们之间就会一直维护一个网络连接,主库会通过这个连接将后续陆续收到的命令操作再同步给从库,这个过程也称为基于长连接的命令传播,可以避免频繁建立连接的开销。
4、主从库间网络断了怎么办?
Redis 2.8 开始,网络断了之后,主从库会采用增量复制的方式继续同步。
主库会把断连期间收到的写操作命令,写入 replication buffer,同时也会把这些操作命令也写入 repl_backlog_buffer 这个缓冲区。
repl_backlog_buffer 是一个环形缓冲区,主库会记录自己写到的位置,从库则会记录自己已经读到的位置。

七、哨兵机制
哨兵即特殊模式下的Redis 进程。主要负责:监控、选主、通知;
监控:周期性的给主、从库发送PING命令,若未在规定时间内响应,则会将其判定为“下线”;

选主:主库挂了,按照一定的规则选择一个从库变为主库;

选出新主库后,通知从库和客户端。
八、哨兵集群
1、哨兵之间通过pub/sub 机制(发布 / 订阅机制)将同一“频道”的哨兵建立集群

2、哨兵通过向主库发送 INFO命令,主库告诉哨兵从库的连接信息,与从库建立连接,并进行监控

3、基于 pub/sub 机制的客户端事件通知

4、由哪个哨兵执行主从切换?
进行投票选举出Leader,任何一个想成为 Leader 的哨兵,要满足两个条件:第一,拿到半数以上的赞成票;第二,拿到的票数同时还需要大于等于哨兵配置文件中的 quorum 值。
九、切片集群
切片集群,也叫分片集群,就是指启动多个 Redis 实例组成一个集群,然后按照一定的规则,把收到的数据划分成多份,每一份用一个实例来保存。

1、Redis Cluster采用哈希槽实现数据与实例之间的映射。一个切片集群中共有16384个哈希槽。将键值对的key通过CRC16算法计算为一个16bit的值,通过16bit的值对16384取模,得到0~16383的模,即代表对应的哈希槽位置。
2、哈希槽如何分配到实例?
方法一:在部署Redis Cluster 方案时,可以使用 cluster create 命令创建集群,此时,Redis 会自动把这些槽平均分布在集群实例上;
方法二:可以使用 cluster meet 命令手动建立实例间的连接,形成集群,再使用 cluster addslots 命令,指定每个实例上的哈希槽个数。
3、客户端如何定位数据?
步骤一:Redis实例会把自己的哈希槽信息发送给相连的实例,当实例之间相互连接后,每个实例就有所有哈希槽的映射关系;
步骤二:客户端收到哈希槽信息后,会把哈希槽信息缓存在本地
步骤三:当客户端请求键值对时,会先计算键所对应的哈希槽,然后就可以给相应的实例发送请求了。
4、重定向机制
当客户端把一个键值对的操作请求发给一个实例时,如果这个实例上并没有这个键值对映射的哈希槽,那么,这个实例就会给客户端返回下面的 MOVED 命令响应结果,这个结果中就包含了新实例的访问地址。

5、数据迁移时,哈希槽与实例之前映射关系变化
如果客户端请求的数据还在迁移中,则客户端就会收到一条 ASK 报错信息,表示客户端请求的键值对所在的哈希槽13320,在 172.16.19.5 这个实例上,但是这个哈希槽正在迁移;客户端需要先给 172.16.19.5 这个实例发送一个 ASKING 命令。这个命令的意思是,让这个实例允许执行客户端接下来发送的命令。然后,客户端再向这个实例发送 GET 命令,以读取数据。

6、MOVED和ASK对比
MOVED 会更新客户端缓存的哈希槽分配信息,后续所有请求均走新实例;
ASK 命令并不会更新客户端缓存的哈希槽分配信息,只是让客户端能给新实例发送一次请求。
十、缓存淘汰策略

ttl:根据过期时间的先后进行淘汰,越早过期的越先被淘汰;
random:随机淘汰除;
LRU:最近最少使用的原则进行淘汰;
LFU:在 LRU 策略基础上,为每个数据增加了一个计数器,来统计这个数据的访问次数。通过访问频率和上一次访问时间进行淘汰。
十一、缓存异常
- 缓存一致性
- 缓存雪崩、击穿、穿透
- 布隆过滤器:由一个初值都为 0 的 bit 数组和 N 个哈希函数组成,可以用来快速判断某个数据是否存在。
标记某个数据的过程:
1、使用N个哈希函数分别得到N个哈希值;
2、N个哈希值分别对bit数组的长度取模,得到哈希在数组中的具体位置;
3、将对应的数组位置的值制设置1。



十一、String
简单动态字符串(Simple Dynamic String,SDS)结构体

1、buf:字节数组,保存实际数据。为了表示字节数组的结束,Redis 会自动在数组最后加一个“\0”,这就会额外占用 1 个字节的开销。
2、len:占 4 个字节,表示 buf 的已用长度。
3、alloc:也占个 4 字节,表示 buf 的实际分配长度,一般大于 len。
备注:len+alloc=8字节,是SDS中额外的开销。RedisObject 结构体

三种编码模式

dictEntry 的结构体

Redis 会使用一个全局哈希表保存所有键值对,哈希表的每一项是一个 dictEntry 的结构体,用来指向一个键值对。dictEntry 结构中有三个 8 字节的指针,分别指向 key、value 以及下一个 dictEntry,三个指针共 24 字节。
Redis 使用的内存分配库 jemalloc 。jemalloc 在分配内存时,会根据我们申请的字节数 N,找一个比 N 大,但是最接近 N 的 2 的幂次数作为分配的空间,这样可以减少频繁分配的次数。
十二、集合

十三、Redis实现分布式锁
基础理论:C(一致性:Consistence)、A(可用性:Availability)、P(分区容错性:Network partitioning)
分布式锁应该具备:排他性;可重入性;锁的获取、释放及失效机制。
Redis是基于AP模型(高可用)实现的分布式锁:
方案一:Redis采用SETNX命令来实现排他性,当key不存在时返回1,存在则返回0;通过expire命令设置锁的失效时间。锁过期了任务还没执行完,可通过定时任务去给key续期。
方案二:当然Redission提供了一个分布式锁的封装实现,业务只需要调用lock()、unlock()方法。所有指令通过lua脚本实现,lua脚本具备原子性;内置了watch dog的机制每隔10秒对key做续期。
常见问题:
缓存失效:主节点挂了,但未将数据同步给从节点,为避免主从切换导致锁失效,Redis官方提供的RedisLock。
缓存误删:A线程删了B线程的的锁。key采用机器IP+线程ID。
ZK实现分布式锁
十四、Redis实现MQ
基于List
1、发送消息:LPUSH
2、保存消息
BRPOPLPUSH进行消息备份存储。
如果消费者程序读了消息但没能正常处理,等它重启后,就可以从备份 List 中重新读取消息并进行处理了。
3、消费消息:BRPOP
4、缺点
List 类型不支持消费组的实现,导致消息消费慢,会给内存带来压力。
基于Streams
1、XADD:插入消息,保证有序,可以自动生成全局唯一 ID;
2、XREAD:用于读取消息,可以按 ID 读取数据;
3、XREADGROUP:按消费组形式读取消息;
4、XPENDING 和 XACK:XPENDING 命令可以用来查询每个消费组内所有消费者已读取但尚未确认的消息,而 XACK 命令用于向消息队列确认消息处理已完成。
对比

















