目录
- 1. Redis 基础知识
- 2. 基本数据结构(底层实现)
- 2.1 SDS
- 2.2 链表
- 2.3 字典
- 2.4 跳跃表
- 2.5 整数集合
- 2.6 压缩列表
- 3. 对象
- 3.1 字符串对象
- 3.2 列表对象
- 3.3 哈希对象
- 3.4 集合对象 set
- 3.5 有序集合对象 zset
- 4. 数据库
- 4.1 RDB 持久化
- 4.2 AOF 持久化
- 4.3 数据淘汰策略
- 5. 客户端与服务器
- 5.1 复制
- 5.2 Sentinel(哨兵)
- 5.3 集群
- 6. 事务
- 7. 缓存管理
- 7.1 缓存穿透
- 7.2 缓存击穿
- 7.3 缓存雪崩
- 8. 高并发系统设计
1. Redis 基础知识
(1)慢查询日志和布隆过滤器简单介绍:
- 慢查询日志:用于记录执行时间超过给定时长的命令请求,用户可以通过这个功能产生的日志来监视和优化查询速度。
- 布隆过滤器:⼆进制数组进行存储,若判断元素存在则可能实际存在,若判断不存在则⼀定不存在。布隆过滤器详解的文章:布隆过滤器(Bloom Filter)详解。
(2)redis中incr、incrby、decr、decrby属于string数据结构,它们是原子性递增或递减操作:
- incr 递增1并返回递增后的结果;
- incrby 根据指定值做递增或递减操作并返回递增或递减后的结果(incrby递增或递减取决于传入值的正负);
- decr 递减1并返回递减后的结果;
- decrby 根据指定值做递增或递减操作并返回递增或递减后的结果(decrby递增或递减取决于传入值的正负);
(3)Redis为什么这么快?
- 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速;
- 数据结构简单,对数据操作也简单;
- 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用考虑各种锁的问题,不存在加锁和释放锁操作。CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽;
- 使用多路I/O复用模型,非阻塞IO;
2. 基本数据结构(底层实现)
- 键:字符串对象
- 值:字符串对象、列表对象、哈希对象、集合对象、有序集合对象
2.1 SDS
(1)Redis底层使用SDS(简单动态字符串)替代C字符串原因:
- 常数复杂度获取字符串长度。因为有 len 属性记录字符串长度(不计算最后⼀个空字符)
- 杜绝了缓冲区溢出的可能性。因为有 free 属性记录未用空间长度,并当空间不足时 SDS API 自动扩容。
- ⼆进制安全,可存入文本或者⼆进制数据。所有 SDS API 都会以处理⼆进制的方式来处理 SDS 存放在 buf 数组里的元素,程序不会对其中的数据做任何限制、过滤或者假设,数据在写入时是什么样的,它被读取时就是什么样。因为 SDS 使用 len 属性的值而不是空字符来判断字符串是否结束。
(2)SDS 空间预分配:
- 如果对 SDS 进行修改后,SDS 的长度(即 len 值)将小于 1MB,那么程序分配和 len 属性同样大小的未使用空间,这时 SDS len 属性的值将和 free 属性的值相同。举个例子,如果修改后 SDS的 len 将变成 13 字节,那么程序也会分配 13 字节的未使用空间,SDS 的 buf 数组的实际长度将变成 13+13+1=27 字节。
- 如果对 SDS 进行修改后,SDS 的长度将大于等于 1MB,那么程序会分配 1MB 的未使用空间。举个例子,如果修改后,SDS 的 len 将变成 30MB,那么 SDS 的 buf 数组的实际长度将为30MB+1MB+1byte
(3)惰性空间释放:
- 用于优化 SDS 字符缩短操作,当缩短 SDS 字符串时,并不立即使用内存重分配来回收缩后多出来的字节,而是使用 free 属性将这些字节的数量记录下来,并等待将来使用。
2.2 链表
(1)链表图如下:
(2)特性:
- 双向无环,链表头尾均有⼀个指针指向空。
- O(1) 时间复杂度获取表头指针、表尾指针和链表长度。
- 多态:链表节点使用 void* 指针来保持节点值,并且可以通过 list 结构的 dup(复制)、free(释放)、match(对比)三个属性为节点值设置类型特定函数,所以链表可以⽤于保存各种不同类型的值。
2.3 字典
(1)Redis 的字典使用 哈希表 作为底层实现,⼀个哈希表里面可以有多个哈希表节点,而每个节点就保存了字典中的⼀个键值对。哈希表结构如下:
(2)字典结构:
- type 属性和 privdata 属性是针对不同类型的键值对,为创建多态字典而设置的。type 属性是⼀个指向 dictType 结构的指针,每个 dictType 结构保存了⼀簇用于操作特定类型键值对的函数,Redis 会为用途不同的字典设置不同的类型特定函数。privdata 属性则保存了需要传给那些类型特定函数的可选参数。
- ht 属性是⼀个包含两个项的数组,数组中的每个项都是⼀个 dictht 哈希表,一般情况下,字典只使用ht[0] 哈希表,ht[1] 哈希表只会在对 ht[0] 哈希表进行 rehash 时使用。另⼀个与 rehash 有关的属性就是 rehashidx,它记录了 rehash 目前的进度,如果目前没有在进行 rehash,那么它的值为 -1。
(2)计算索引:
- 利用哈希算法计算哈希值,然后将哈希值与哈希大小掩码取与运算,得到索引下标。
(3)解决冲突:
- 链地址法,且每次添加到链表表头。
(4)rehash 方式:
- 为字典的 ht[1] 哈希表分配空间,这个哈希表的空间大小取决于要执行的操作和 ht[0] 当前包含的键值对数量。当执行扩展操作时,ht[1] 的大小为第⼀个大于等于 ht[0].used * 2 的 2^n;当执行收缩操作时,ht[1] 的大小为第⼀个大于等于 ht[0].used 的 2^n。之后将 ht[0] 上的所有键值对rehash 到 ht[1] 上。
- 最后释放 ht[0],将 ht[1] 设置为 ht[0],并在 ht[1] 新创建⼀个空白哈希表,为下⼀次rehash 做准备。注意 rehash 操作是 渐进式的,即在执行增加、删除、查找或更新操作的时候同步进行 rehash 操作(trehashidx 记录索引),直到所有 rehash 完成,则置 -1。同时新增加的会加入到 ht[1],且外部查找时先查 ht[0],再查 ht[1]
(5)负载因子 = 哈希表已有节点数量 / 哈希表大小
- 扩展操作需满足:当没有创建当前服务的子进程时,负载因子大于等于 1;当创建当前服务的子进程时,负载因子大于等于 5。因为大多数操作系统都采用写时复制技术来优化子进程的使用效率,所以在子进程存在期间,服务器会提高执行扩展操作所需的负载因子,从而尽可能避免在子进程存在期间进行哈希表扩展操作,可以避免不必要的内存写入操作,最大限度地节约内存。
- 收缩操作需满足:负载因子小于 0.1。
2.4 跳跃表
(1)跳跃表是⼀种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。在大部分情况下,跳跃表的效率可以和平衡树相媲美,并且跳跃表的实现比平衡树更简单。跳跃表是作为有序集合键的底层实现之⼀。
- 其中每个节点层高(最大层高 level)是 1到32 随机生成的,length 是除表头外节点的数量。每个节点有层属性(前进指针和跨度)、后退指针、分值(用来排序)和成员对象(唯⼀存在)四部分,其中表头节点不使用后面三部分但是有。
2.5 整数集合
(1)集合中不包含任何重复的元素,且数组中的元素按升序排列。数组中元素的真实类型由 encoding 决定。当有更大长度的元素时将发生(类型)升级,整数集合不支持降级。
2.6 压缩列表
(1)压缩列表:
- 当⼀个哈希键只包含少量键值对,并且每个键值对的键和值要么是小整数值,要么是长度比较短的字符串时,Redis 就会用压缩列表来做哈希键的底层实现。
- 压缩列表是 Redis 为了节约内存 而开发的,是由 连续内存块 组成的顺序型数据结构。⼀个压缩列表可以包含任意多个节点,每个节点保存⼀个字节数组或者⼀个整数值。
- 压缩列表节点 的第⼀部分是上⼀个节点的长度(用于由当前位置计算得到上一个节点的位置),第⼆部分是当前节点的类型和长度,第三部分是当前节点的内容。由于第⼀部分会根据上个节点的长度动态确定存储位数大小,因此当插入或删除节点时可能(只有当连续多个大字节节点存在时)会出现连锁更新,但是其几率较低。
3. 对象
(1)共五类,包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象。
- 实现了引用计数、对象共享(通过引用计数实现)、最后访问时间记录(当内存占用超过阈值将自动回收空转时间长的对象)等信息。
3.1 字符串对象
(1)字符串表格图如下(详细的String介绍见博客《Redis的五种数据类型(String、Hash、List)》的第1小节):
- 其中 raw 即为 SDS,而 embstr 是 SDS 的改版(核心结构⼀样),仅需⼀次内存分配(SDS 需要两次:创建 RedisObject 和 SDS 对象),且创建的对象是在连续内存内,更好利用缓存。
3.2 列表对象
(1)列表对象的编码可以是 ziplist 或者 linkedlist。当列表对象可以同时满足以下两个条件时,列表对象使用 ziplist 编码(详细的List介绍见博客《Redis的五种数据类型(String、Hash、List)》的第3小节):
- 列表对象保存的所有字符串元素的长度都小于 64 字节。
- 列表对象保存的元素数量小于 512 个。
(2)注意:不能满足这两个条件的列表对象需要使用 linkedlist 编码。
3.3 哈希对象
(1)哈希对象的编码可以是 ziplist 或者 hashtable。(详细的哈希介绍见博客《Redis的五种数据类型(String、Hash、List)》的第2小节)
- ziplist 编码的哈希对象使⽤压缩列表作为底层实现,当有新键值对要加入时,依次放入键和值。
- hashtable 编码的哈希对象使用字典作为底层实现,哈希对象中的每个键值对都是用一个字典键值对来保存。
- 对于使用何种编码,与列表对象的两条规则类似。
3.4 集合对象 set
(1)集合对象的编码可以是 intset 或者 hashtable(值设置为NULL)。详细的set介绍见博客《Redis的五种数据类型(Set、Zset)》的第1小节。
- 当集合对象中的所有元素都是整数且数量不超过 512 时,使用 intset 编码实现。
3.5 有序集合对象 zset
(1)有序集合的编码可以是 ziplist 或者 skiplist。(详细的zset介绍见博客《Redis的五种数据类型(Set、Zset)》的第2小节)
- ziplist 内的集合元素按分值从小到大进行排序。
- skiplist 编码的有序集合对象使用 zset 结构作为底层实现,⼀个 zet 结构同时包含⼀个字典和⼀个跳跃表。zset 结构中的跳跃表按分值从小到大保存了所有集合元素,每个跳跃表节点都保存了⼀个集合元素,通过跳跃表,程序可以对有序集合进行 范围型操作。此外,zset 结构中的 dict 字典为有序集合创建了⼀个从成员到分值的映射,字典中的每个键值对都保存了⼀个集合元素:字典的键保存了元素的成员,而字典的值则保存了元素的分值。
- 有序集合每个元素的成员都是⼀个字符串对象,而每个元素的分值都是⼀个 double 类型的浮点数。虽然 zset 结构同时使用了跳跃表和字典来保存有序集合元素,但这两种数据结构都会 通过指针来共享 相同元素的成员和分值,因此不会浪费额外的内存。
(2)当有序集合对象同时满足以下两个条件时,对象使用 ziplist 编码:
- 有序集合保存的元素数量小于 128 个;
- 有序集合保存的所有元素成员的长度都小于 64 字节;
(3)注意:不满足任意⼀个条件,有序集合对象将使用 skiplist 编码。
4. 数据库
(1)服务器默认创建 16 个数据库,客户端默认连接第⼀个数据库(0 号数据库)。
(2)过期键删除策略:
- 惰性删除:所有读写数据库的命令前都将进行过期键检测,若过期则删除否则继续执行;
- 定期删除:在规定时间内,分多次遍历服务器中的各个服务器,并从过期字段属性中随机检查指定数量的键是否过期,并删除过期键
4.1 RDB 持久化
(1)Redis 是内存数据库,数据库状态存储在内存里,可通过 RDB 持久化将数据库状态保存到 RDB 文件(而进制文件,保存键值对)中。(详细的RDB持久化介绍见博客《Redis的持久化》的第2小节)
- SAVE、BGSAVE 都是保存 RDB 文件的命令,后者 fork 子线程达到可边保存边执行其他命令的不阻塞处理。RDB 文件载⼊是在服务器启动时自动执行的,没有相关命令。因为 AOF 文件的更新频率通常比RDB 文件的更新频率高,所以如果服务器开启了 AOF 持久化功能,那么服务器会优先使用 AOF 文件来还原数据库状态。只有在 AOF 持久化功能处于关闭状态时,服务器才会使用 RDB 文件来还原数据库状态。
- 间隔性数据保存实现原理:利用 BGSAVE 命令,通过设置每隔多少时间至少保存多少次,会自动(100ms)检查是否满足保存条件并进行保存操作。(记录了已保存次数以及上次保存的时间戳)
(2)RDB 文件结构:
(3)其中 每个非空数据库 在 RDB 文件中都可以保存为如下三个部分:
- 长度 1 字节,当读入程序遇到这个值的时候,它知道接下来将读入的是⼀个数据库号码。
- 保存数据库号码,长度不定。当程序读入 db_number 部分之后,服务器会调用 SELECT 命令,根据读入的数据库号码进行数据库切换,使得之后读入的键值对可以载⼊到正确的数据库中。
- 保存数据库中所有键值对数据,如果键值对带有过期时间,那么过期时间也会和键值对保存在⼀起。
4.2 AOF 持久化
(1)与 RDB 持久化通过保存数据库中的键值对来记录数据库状态不同,AOF 持久化是通过保存 Redis 服务器所执行的写命令来记录数据库状态的。AOF 持久化功能的实现可以分为(详细的AOF持久化介绍见博客《Redis的持久化》的第3小节):
- 命令追加:服务器在执行完⼀个写命令之后,会把写命令追加到服务器状态的 AOF 缓存区的末尾。
- 文件写入与同步,默认值为下图的 everysec
(2)AOF 文件载入:(使用伪客户端是因为写命令只能通过客户端向服务端发送)
(3)AOF 文件重写:实际并不需要对现有的 AOF 文件进行任何读取、分析或者写入操作,这个功能是通过读取服务器当前的数据库状态来实现的。为了防止在重写的过程中数据库状态发生改变从而数据不⼀致,服务器需要执
行以下三个工作:
- 执行客户端发来的命令;
- 将执行后的写命令追加到 AOF 缓冲区;
- 将执行后的写命令追加到 AOF 重写缓冲区。
(4)RDB与AOF区别:
- RDB可以理解为是⼀种全量数据更新机制,AOF可以理解为是⼀种增量的更新机制,AOF重写可以理解为是⼀种全量+增量的更新机制(第⼀次是全量,后⾯都是增量)。
- RDB适合服务器数据库数据量小,写命令频繁的场景。
- AOF适合数据量大,写命令少的场景。
- AOF重写适合在AOF运行了很久的写命令之后执行。
4.3 数据淘汰策略
(1)Redis 内存数据集大小上升到⼀定大小的时候,就会进行数据淘汰策略。
- volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰。
- volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰。
- volatile-random:从已设置过期时间的数据集中任意选择数据淘汰。
- volatile-lfu 从设置过期时间的数据集(server.db[i].expires)中挑选出使用频率最小的数据淘汰。没有设置过期时间的key不会被淘汰,这样就可以在增加内存空间的同时保证需要持久化的数据不会丢失。
- no-enviction:当内存达到限制的时候,不淘汰任何数据,不可写入任何数据集,所有引起申请内存的命令会报错。
- allkeys-lru:从数据集中挑选最近最少使用的数据淘汰。
- allkeys-random:当内存达到限制的时候,从数据集中任意选择数据淘汰。
- allkeys-lfu 从数据集(server.db[i].dict)中挑选使用频率最小的数据淘汰,该策略要淘汰的key面向的是全体key集合,而非过期的key集合。
5. 客户端与服务器
(1)伪客户端:
- 伪客户端的 fd 属性值为 -1;伪客户端处理的命令请求来源于 AOF 文件或者 Lua 脚本,而不是网络,所以这种客户端不需要套接字连接,自然也不需要记录套接字描述符。目前 Redis 服务器会在两个地方用到伪客户端,⼀个用于载入 AOF 文件并还原数据库状态(载⼊完成则退出),而另⼀个则用于执行Lua 脚本中包含的 Redis 命令(随服务器⼀直存在)。
(2)不修改数据库的命令也可能写入 AOF 文件:
- PUBSUB 命令虽然没有修改数据库,但该命令向频道的所有订阅者发送消息这一行为带有副作用,接收到消息的所有客户端的状态都会因为这个命令而改变。因此,服务器需要使用 REDIS_FORCE_AOF标志,强制将这个命令写入 AOF 文件,这样在将来载入 AOF 文件时,服务器就可以再次执行相同的PUBSUB 命令,并产生相同的副作用。而 SCRIPT LOAD 命令虽然没有修改数据库,但它修改了服务器状态,所以它是⼀个带有副作用的命令。
(3)服务器使用两种模式来限制客户端输出缓冲区的大小:
- 硬性限制:如果输出缓冲区的大小超过了硬性限制所设置的大小,那么服务器立即关闭客户端。
- 软性限制:如果输出缓冲区的大小超过了软性限制所设置的大小,但还没超过硬性限制,那么服务器会继续监视客户端,如果输出缓冲区的大小⼀直超出软性限制,并且持续时间超过服务器设定的时长,那么服务器将关闭客户端;相反地,如果输出缓冲区的大小在指定时间之内,不再超出软性限制,那么客户端就不会被关闭。
(4)服务器从启动到可处理客户端的命令请求需要执行步骤:
- 初始化服务器状态。
- 载入服务器配置(包含用户自定义配置)。
- 初始化服务器数据结构。
- 还原数据库状态。
- 执行事件循环。
5.1 复制
(1)在 Redis 中,用户可以通过执行 SLAVEOF 命令,让⼀个服务器去复制另⼀个服务器,称呼被复制的服务器为主服务器,而对主服务器进行复制的服务器则被称为从服务器。复制的功能分为 同步(主从⼀致)和 命令传播(主服务器修改后从服务器更新至一致)两个操作。详细的介绍Redis的主从复制见博客《Redis的主从复制》。
(2)复制步骤:
- 从服务器向主服务器发送 PSYNC 命令;
- 然后主服务器执行 BGSAVE 命令,在后台生成⼀个 RDB 文件,之后使用一个缓冲区记录从现在开始执行的所有写命令;
- 当主服务器的 BGSAVE 命令执行完成后,主服务器会将 BGSAVE 命令生成的 RDB 文件发送给从服务器,从服务器接收并载入这个 RDB 文件,将自己的数据库状态更新至主服务器执行 BGSAVE命令时的数据库状态;
- 主服务器将记录在缓冲区里面的所有写命令发送给从服务器,从服务器执行这些写命令,将自己的数据库状态更新至主服务器数据库当前所处的状态。
- 当上述同步完成后,将使用下图流程(命令传播阶段)。当主服务器接收到新的写命令时,将向所有的从服务器发送写命令并存⼊固定长度的复制积压缓冲区,同时主从服务器各自维护复制偏移量(可根据偏移量来判断是否主从⼀致)。若某从服务器断线重连后,可通过 PSYNC 命令向主服务器发送自己的复制偏移量,主服务器会根据该复制偏移量决定如何对该从服务器执行同步。若复制偏移量之后的数据仍在复制积压缓冲区内,则将之后的写命令发送给该从服务器进行同步;否则,将对该从服务器执行上述 4 个步骤重新同步。
(2)心跳检测:
- 主从服务器可以通过发送和接收心跳检测命令来检测两者之间的 网络连接是否正常;
- 辅助实现 min_slaves 配置选项,防止主服务器在不安全的情况下执行写命令;
- 通过复制偏移量检测命令是否丢失。主服务器收到命令,若之前的写命令在半路丢失,可以检查到从服务器当前的复制偏移量少于自己的复制偏移量,从而重新发送从服务器缺少的数据给从服务器。
5.2 Sentinel(哨兵)
(1)Sentinel 是 Redis 的高可用性解决方案(详细的哨兵机制介绍见博客《Redis的哨兵机制》):
- 由⼀个或多个 Sentinel 实例组成的 Sentinel 系统可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器,并在被监视的主服务器进⼊下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求。
(2)Sentinel 启动需要执行的步骤:
- 初始化服务器。本质是 Redis 普通服务器,但是不加载 RDB 文件等。
- 将普通 Redis 服务器使用的代码替换成 Sentinel 专用代码。即加载不同于普通服务器的命令表。
- 初始化 Sentinel 状态。根据给定的配置文件,初始化 Sentinel 的监视主服务器列表等。
- 创建连向主服务器的网络连接。对于每个被 Sentinel 监视的主服务器来说,Sentinel 会创建两个连向主服务器的异步网络连接:⼀个是命令连接,这个连接专门用于向主服务器发送命令,并接收命令回复。另⼀个是订阅连接(解决客户端不在线或断线之后无法接收消息的问题,因为被发送的消息都不会保存在 Redis 服务器里),这个连接是专门用于订阅主服务器的频道。
(3)Sentinel 监视某服务器,同时也可感知到监视该服务器的其他 Sentinel 并做相应更新,对其建立命令连接但不建立订阅连接,这是因为 Sentinel 需要通过接收主服务器或者从服务器发来的频道信息来发现未知的新 Sentinel,所以才需要建立订阅连接,而相互已知的 Sentinel 只要使用命令连接来进行通信就足够了。
(4)检测主观下线状态:
- 默认情况下,Sentinel 会以每秒⼀次的频率向所有与它创建了命令连接的实例(包括主从服务器、其他Sentinel 在内)发送 PING 命令,并通过实例返回的 PING 命令回复来判断实例是否在线,若在指定时间内连续返回⽆效回复则判断下线。注意每个 Sentinel 指定的时间可能是不⼀样的。
(5)检测客观下线状态:
- 当 Sentinel 将⼀个主服务器判断为主观下线之后,为了确认这个主服务器是否真的下线了,它会向同样监视这⼀主服务器的其他 Sentinel 进行询问,看它们是否也认为主服务器已经进入了下线状态(可以是主观下线或者客观下线)。当 Sentinel 从其他 Sentinel 那里接受到足够数量的已下线判断后,Sentinel 就会将该服务器判断为客观下线,并对主服务器执行故障转移操作。
(6)选举领头 Sentinel:当一个主服务器被判断为客观下线时,监视这个下线主服务器的各个 Sentinel 会进行协商,选举出⼀个领头 Sentinel,并由领头 Sentinel 对下线主服务器执行故障转移操作。协商规则如下:
- 监视该客观下线的每个 Sentinel 会互相向其他 Sentinel 发送消息,而自身把接收到的发送第⼀个消息的那个 Sentinel 设置为自己的领头,并拒绝后续的所有该消息;
- 统计自身是多少 Sentinel 的领头,当超过半数时将成为真正的领头Sentinel,并执行故障转移操作;
- 若在给定时限内都不满足,则在⼀段时间之后再次选举,直到选出领头Sentinel 为止。
(7)故障转移:领头 Sentinel 将对已下线的主服务器执行故障转移操作:
- 在已下线的主服务器属下的所有从服务器里面,挑选出⼀个从服务器,并将其转为主服务器。⾸先排除不在线的从服务器,其次排除最近没有成功通信过的从服务器,然后根据配置去除数据比较旧的从服务器,最后按优先级、复制偏移量、运行ID 排序后选择⼀个从服务器。
- 让已下线的主服务器属下的所有从服务器改为复制新的主服务器;
- 将已下线主服务器设置为新的主服务器的从服务器,当这个旧的主服务器重新上线时,它就会成为新的主服务器的从服务器;
5.3 集群
(1)集群是 Redis 提供的分布式数据库方案,通过分片来进行数据共享,并提供复制和故障转移功能(详细的集群介绍见博客《Redis的集群》)。
- Redis 集群通过 分片 的方式来保存数据库中的键值对:集群的整个数据库被分为 16384 个槽(slot),数据库中的每个键都属于这 16384 个槽的其中⼀个,集群中的每个节点可以处理 0 个或最多 16384 个槽。当数据库中的 16384 个槽都有节点在处理时,集群处于上线状态;相反,有任何⼀个槽没有得到处理,那么集群处于下线状态。(集群中的每个节点都会记录自己负责处理的槽以及所有其他节点处理的槽)
- ⼀个集群客户端通常会与集群中的多个节点创建套接字连接,而所谓的节点转向实际上就是换⼀个套接字来发送命令。如果客户端尚未与想要转向的节点创建套接字连接,那么客户端会先根据 MOVED 错误 提供的 IP 地址和端口号来转向连接节点,然后再重试。
(2)重新分片:
- Redis 集群的重新分片操作可以将任意数量已经指派给某个节点的槽改为指派给另⼀个节点,并且相关槽所属的键值对也会从源节点被移动到目标节点。重新分片操作可以在线进行,在重新分片的过程中,集群不需要下线,并且源节点和目标节点都可以继续处理命令请求。
(3)ASK 错误和 MOVED 错误都会导致客户端转向,区别在于:
- MOVED 错误代表槽的负责权已经从⼀个节点转移到了另⼀个节点:在客户端收到关于槽 i 的MOVED 错误之后,客户端每次遇到关于槽 i 的命令请求时,都可以直接将命令请求发送至MOVED 错误所指向的节点,因为该节点就是目前负责槽 i 的节点;
- ASK 错误只是两个节点在迁移槽的过程中使用的⼀种临时措施:在客户端收到关于槽 i 的 ASK 错误之后,客户端只会在接下来的⼀次命令请求中将关于槽 i 的命令请求发送至 ASK 错误所指示的节点,但这种转向不会对客户端今后发送关于槽 i 的命令请求产生任何影响,客户端仍然会将关于槽 i 的命令请求发送至目前负责处理槽 i 的节点,除非 ASK 错误再次出现。
6. 事务
(1)Redis 通过 MULTI(切换为事务状态)、EXEC(执行事务队列)、WATCH(乐观锁) 等命令来实现事务功能。事务提供了⼀种将多个命令请求打包,然后⼀次性、按顺序地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而改去执行其他客户端的命令请求,它会将事务中的所有命令都执行完毕,然后才去处理其他客户端的命令请求。详细的Redis事务介绍见博客《Redis的事务》。
- WATCH 可以在 EXEC 命令执行之前,监视任意数量的数据库键,并在 EXEC 命令执行时,检测被监视的键是否至少有⼀个已经被修改过了,如果是的话,服务器将拒绝执行事务,并向客户端返回代表事务执行失败的空回复。
(2)原子性:
- Redis 的事务和传统的关系型数据库事务的最大区别在于,Redis 不支持事务回滚机制,即使事务队列中的某个命令在执⾏期间出现了错误,整个事务也会继续执行下去,直到将事务队列中的所有命令都执行完毕为止。
(3)一致性:
- 处理⼊队错误:如果⼀个事务在入队命令的过程中,出现了命令不存在或者命令的格式不正确等情况,那么 Redis 将拒绝执行这个事务,所以 Redis 事务的⼀致性不会被带有入队错误的事务影响。
- 执行错误:执行过程中发生的错误都是⼀些不能在入队时被服务器发现的错误,这些错误只会在命令实际执行时被触发。即使在事务的执行过程中发⽣了错误,服务器也不会中断事务的执行,它会继续执行事务中余下的其他命令,并且已执行的命令(包括执行命令所产生的结果)不会被出错的命令影响,因此不会对事务的⼀致性产⽣任何影响。对数据库键执行了错误类型的操作是事务执行期间最常见的错误之⼀。
- 服务器停机:无论服务器是有持久化(加载 AOF 或 RDB 文件)还是无持久化(空白数据库),都将保持数据的⼀致性。
(4)隔离性:
- 因为 Redis 使用单线程的方式来执行事务(以及事务队列中的命令),并且服务器保证,在执行事务期间不会对事务进行中断,因此,Redis 的事务总是以串行的⽅式运行的,并且事务也总是具有隔离性。
(5)持久性:
- 因为 Redis 的事务不过是简单地用队列包裹了⼀组 Redis 命令,并没有为事务提供任何额外的持久化功能,所以 Redis 事务的持久性由 Redis 所使用的持久化模型决定:
- 当服务器在 无持久化的内存模式 下运行时,事务 不具持久性;
- 当服务器在 RDB 持久化模式 下运行时,服务器只会在特定的保存条件被满足时,才会执行BGSAVE 命令对数据库进行保存操作,并且异步执行的 BGSAVE 不能保证事务数据被第⼀时间保存到硬盘,因此不具持久性;
- 当服务器在 AOF 持久化模式 下,并且 appendfsync 选项的值为 always 时,程序总会在执行命令之后调用同步函数,将命令数据真正保存到硬盘,具有持久性;
- 当服务器在 AOF 持久化模式 下,并且 appendfsync 选项的值为 everysec 时,程序会每秒同步⼀次命令数据到硬盘。因为停机可能会恰好发送在等待同步的那⼀秒内,这可能会造成数据丢失,不具有持久性;
- 当服务器在 AOF 持久化模式 下,并且 appendfsync 选项的值为 no 时,程序会交由操作系统来决定何时将命令数据同步到硬盘,该情况下事务 不具有持久性。
不论 Redis 在什么模式下运行,在⼀个事务的最后加上 SAVE 命令总可以保证事务的 持久性。但是资源消耗大。
7. 缓存管理
- 详细的缓存机制介绍见博客《Redis的缓存》。
7.1 缓存穿透
(1)缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求;
(2)解决方案:
- 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截。
- 设置空缓存:将key-value对写为key-null,缓存有效时间可以设置短点,这样可以防止攻击用户反复用同⼀个id暴力攻击。
7.2 缓存击穿
(1)缓存击穿是指缓存没有但数据库有的数据(⼀般是缓存时间到期),这时由于并发用户特别多,引起数据库压力瞬间增大;
(2)解决方法:
- 分布式互斥锁:只允许⼀个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可。
- 永不过期:为每个键值对设置⼀个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去更新缓存
7.3 缓存雪崩
(1)缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至宕机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
(2)解决方案:
- 设置过期时间随机,防止同一时间大量数据过期现象发生。
- 如果缓存数据是分布式部署,将热点数据均匀的分布在不同的缓存数据库中。
- 设置热点数据永不过期。
8. 高并发系统设计
(1)有限资源面对大量请求,如何解决资源的请求??
- 降低流量:比如前端返回⼀些伪消息(可以轻松判断的)。
- 用户请求过来时放入消息队列中处理。