Redis 可以做什么?
string(字符串)
Redis 的字符串是动态字符串,是可以修改的字符串,内部结构实现上类似于 Java 的
ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配,如图中所示,内部为当前字
符串实际分配的空间 capacity 一般要高于实际字符串长度 len。当字符串长度小于 1M 时,
扩容都是加倍现有的空间,如果超过 1M,扩容时一次只会多扩 1M 的空间。需要注意的是
字符串最大长度为 512M。
list(列表)
Redis 的列表相当于 Java 语言里面的 LinkedList,注意它是链表而不是数组。这意味着
list 的插入和删除操作非常快,时间复杂度为 O(1),但是索引定位很慢,时间复杂度为
O(n),这点让人非常意外。
快速列表
如果再深入一点,你会发现 Redis 底层存储的还不是一个简单的 linkedlist,而是称之为
快速链表 quicklist 的一个结构。
首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是 ziplist,也即是
压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。当数据量比较多的
时候才会改成 quicklist。因为普通的链表需要的附加指针空间太大,会比较浪费空间,而且
会加重内存的碎片化。比如这个列表里存的只是 int 类型的数据,结构上还需要两个额外的指针 prev 和 next 。所以 Redis 将链表和 ziplist 结合起来组成了 quicklist。也就是将多个
ziplist 使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空
间冗余。
hash(字典)
Redis 的字典相当于 Java 语言里面的 HashMap,它是无序字典。内部实现结构上同
Java 的 HashMap 也是一致的,同样的数组 + 链表二维结构。第一维 hash 的数组位置碰撞
时,就会将碰撞的元素使用链表串接起来。
不同的是,Redis 的字典的值只能是字符串,另外它们 rehash 的方式不一样,因为
Java 的 HashMap 在字典很大时,rehash 是个耗时的操作,需要一次性全部 rehash。Redis
为了高性能,不能堵塞服务,所以采用了渐进式 rehash 策略。
渐进式 rehash 会在 rehash 的同时,保留新旧两个 hash 结构,查询时会同时查询两个
hash 结构,然后在后续的定时任务中以及 hash 的子指令中,循序渐进地将旧 hash 的内容
一点点迁移到新的 hash 结构中。
set(集合)
zset(有序列表)
zset 可能是 Redis 提供的最为特色的数据结构,它也是在面试中面试官最爱问的数据结
构。它类似于 Java 的 SortedSet 和 HashMap 的结合体,一方面它是一个 set,保证了内部
value 的唯一性,另一方面它可以给每个 value 赋予一个 score,代表这个 value 的排序权
重。它的内部实现用的是一种叫着「跳跃列表」的数据结构。
zset 中最后一个 value 被移除后,数据结构自动删除,内存被回收。 zset 可以用来存
粉丝列表,value 值是粉丝的用户 ID,score 是关注时间。我们可以对粉丝列表按关注时间
进行排序。
zset 还可以用来存储学生的成绩,value 值是学生的 ID,score 是他的考试成绩。我们
可以对成绩按分数进行排序就可以得到他的名次。
跳跃列表
容器性数据结构的通用规则
list/set/hash/zset 这四种数据结构是容器型数据结构,它们共享下面两条通用规则:
create if not exists
drop if no elements
过期时间
如果一个字符串已经设置了过期时间,然后你调用了
set 方法修改了它,它的过期时间会消失。
分布式锁
队列延迟
有没有什么办法能显著降低延迟呢?你当然可以很快想到:那就把睡觉的时间缩短点。这
种方式当然可以,不过有没有更好的解决方案呢?当然也有,那就是 blpop/brpop。
这两个指令的前缀字符 b 代表的是 blocking,也就是阻塞读。
阻塞读在队列没有数据的时候,会立即进入休眠状态,一旦数据到来,则立刻醒过来。消
息的延迟几乎为零。用 blpop/brpop 替代前面的 lpop/rpop,就完美解决了上面的问题。...
空闲连接自动断开
如果线程一直阻塞在哪里,Redis 的客户端连接就成了闲置连接,闲置过久,服务器一般
会主动断开连接,减少闲置资源占用。这个时候 blpop/brpop 会抛出异常来。
所以编写客户端消费者的时候要小心,注意捕获异常,还要重试。...
延时队列的实现
延时队列可以通过 Redis 的 zset(有序列表) 来实现。我们将消息序列化成一个字符串作
为 zset 的 value,这个消息的到期处理时间作为 score,然后用多个线程轮询 zset 获取到期
的任务进行处理,多个线程是为了保障可用性,万一挂了一个线程还有其它线程可以继续处
理。因为有多个线程,所以需要考虑并发争抢任务,确保任务不能被多次执行。
进一步优化
上面的算法中同一个任务可能会被多个进程取到之后再使用 zrem 进行争抢,那些没抢到
的进程都是白取了一次任务,这是浪费。可以考虑使用 lua scripting 来优化一下这个逻辑,将
zrangebyscore 和 zrem 一同挪到服务器端进行原子化操作,这样多个进程之间争抢任务时就不
会出现这种浪费了。
基本使用
值得注意的是位数组的顺序和字符的位顺序是相反的。
统计和查找
魔术指令 bitfield
使用方法
pfadd 这个 pf 是什么意思
它是 HyperLogLog 这个数据结构的发明人 Philippe Flajolet 的首字母缩写,老师觉得他
发型很酷,看起来是个佛系教授。
pfmerge 适合什么场合用?
HyperLogLog 除了上面的 pfadd 和 pfcount 之外,还提供了第三个指令 pfmerge,用于
将多个 pf 计数值累加在一起形成一个新的 pf 值。
比如在网站中我们有两个内容差不多的页面,运营说需要这两个页面的数据进行合并。
其中页面的 UV 访问量也需要合并,那这个时候 pfmerge 就可以派上用场了。
pf 的内存占用为什么是 12k?
布隆过滤器是什么?
当布隆过滤器说某个值存在时,这个值可能不存在;当它说不存在时,那就肯定不存在。
布隆过滤器的应用
爬虫URL去重。 推荐系统推荐视频。
布隆过滤器数学推导
Redis-Cell
Redis 的 Geo 指令基本使用
Redis 提供的 Geo 指令只有 6 个,读者们瞬间就可以掌握。使用时,读者务必再次想
起,它只是一个普通的 zset 结构。
小结 & 注意事项
在一个地图应用中,车的数据、餐馆的数据、人的数据可能会有百万千万条,如果使用
Redis 的 Geo 数据结构,它们将全部放在一个 zset 集合中。在 Redis 的集群环境中,集合
可能会从一个节点迁移到另一个节点,如果单个 key 的数据过大,会对集群的迁移工作造成
较大的影响,在集群环境中单个 key 对应的数据量不宜超过 1M,否则会导致集群迁移出现
卡顿现象,影响线上服务的正常运行。
所以,这里建议 Geo 的数据使用单独的 Redis 实例部署,不使用集群环境。
如果数据量过亿甚至更大,就需要对 Geo 数据进行拆分,按国家拆分、按省拆分,按
市拆分,在人口特大城市甚至可以按区拆分。这样就可以显著降低单个 zset 集合的大小。
非阻塞 IO
事件轮询(多路复用)
指令队列
Redis 会将每个客户端套接字都关联一个指令队列。客户端的指令通过队列来排队进行
顺序处理,先到先服务。
响应队列
Redis 同样也会为每个客户端套接字关联一个响应队列。Redis 服务器通过响应队列来将
指令的返回结果回复给客户端。 如果队列为空,那么意味着连接暂时处于空闲状态,不需要
去获取写事件,也就是可以将当前的客户端描述符从 write_fds 里面移出来。等到队列有数据
了,再将描述符放进去。避免 select 系统调用立即返回写事件,结果发现没什么数据可以
写。出这种情况的线程会飙高 CPU。
定时任务
服务器处理要响应 IO 事件外,还要处理其它事情。比如定时任务就是非常重要的一件
事。如果线程阻塞在 select 系统调用上,定时任务将无法得到准时调度。那 Redis 是如何解
决这个问题的呢?
Redis 的定时任务会记录在一个称为最小堆的数据结构中。这个堆中,最快要执行的任
务排在堆的最上方。在每个循环周期,Redis 都会将最小堆里面已经到点的任务立即进行处
理。处理完毕后,将最快要执行的任务还需要的时间记录下来,这个时间就是 select 系统调
用的 timeout 参数。因为 Redis 知道未来 timeout 时间内,没有其它定时任务需要处理,所以
可以安心睡眠 timeout 的时间。
Nginx 和 Node 的事件处理原理和 Redis 也是类似的
RESP(Redis Serialization Protocol)
快照原理RDB((Redis Database))
Redis 使用操作系统的多进程 COW(Copy On Write) 机制来实现快照持久化,这个机制
很有意思,也很少人知道。多进程 COW 也是鉴定程序员知识广度的一个重要指标。
fork(多进程)
子进程做数据持久化,它不会修改现有的内存数据结构,它只是对数据结构进行遍历读
取,然后序列化写到磁盘中。但是父进程不一样,它必须持续服务客户端请求,然后对内存
数据结构进行不间断的修改。
这个时候就会使用操作系统的 COW 机制来进行数据段页面的分离。数据段是由很多操
作系统的页面组合而成,当父进程对其中一个页面的数据进行修改时,会将被共享的页面复
制一份分离出来,然后对这个复制的页面进行修改。这时子进程相应的页面是没有变化的,
还是进程产生时那一瞬间的数据。
随着父进程修改操作的持续进行,越来越多的共享页面被分离出来,内存就会持续增
长。但是也不会超过原有数据内存的 2 倍大小。另外一个 Redis 实例里冷数据占的比例往
往是比较高的,所以很少会出现所有的页面都会被分离,被分离的往往只有其中一部分页
面。每个页面的大小只有 4K,一个 Redis 实例里面一般都会有成千上万的页面。
子进程因为数据没有变化,它能看到的内存里的数据在进程产生的一瞬间就凝固了,再
也不会改变,这也是为什么 Redis 的持久化叫「快照」的原因。接下来子进程就可以非常安
心的遍历数据了进行序列化写磁盘了。
AOF(Append Only File)原理
AOF 日志存储的是 Redis 服务器的顺序指令序列,AOF 日志只记录对内存进行修改的
指令记录。
假设 AOF 日志记录了自 Redis 实例创建以来所有的修改性指令序列,那么就可以通过
对一个空的 Redis 实例顺序执行所有的指令,也就是「重放」,来恢复 Redis 当前实例的内
存数据结构的状态。
Redis 会在收到客户端修改指令后,先进行参数校验,如果没问题,就立即将该指令文
本存储到 AOF 日志中,也就是先存到磁盘,然后再执行指令。这样即使遇到突发宕机,已
经存储到 AOF 日志的指令进行重放一下就可以恢复到宕机前的状态。
Redis 在长期运行的过程中,AOF 的日志会越变越长。如果实例宕机重启,重放整个
AOF 日志会非常耗时,导致长时间 Redis 无法对外提供服务。所以需要对 AOF 日志瘦
身。
AOF 重写
Redis 提供了 bgrewriteaof 指令用于对 AOF 日志进行瘦身。其原理就是开辟一个子进
程对内存进行遍历转换成一系列 Redis 的操作指令,序列化到一个新的 AOF 日志文件中。
序列化完毕后再将操作期间发生的增量 AOF 日志追加到这个新的 AOF 日志文件中,追加
完毕后就立即替代旧的 AOF 日志文件了,瘦身工作就完成了。
fsync
AOF 日志是以文件的形式存在的,当程序对 AOF 日志文件进行写操作时,实际上是将
内容写到了内核为文件描述符分配的一个内存缓存中,然后内核会异步将脏数据刷回到磁盘
的。
这就意味着如果机器突然宕机,AOF 日志内容可能还没有来得及完全刷到磁盘中,这个
时候就会出现日志丢失。那该怎么办?
Linux 的 glibc 提供了 fsync(int fd)函数可以将指定文件的内容强制从内核缓存刷到磁
盘。只要 Redis 进程实时调用 fsync 函数就可以保证 aof 日志不丢失。但是 fsync 是一个
磁盘 IO 操作,它很慢!如果 Redis 执行一条指令就要 fsync 一次,那么 Redis 高性能的
地位就不保了。
所以在生产环境的服务器中,Redis 通常是每隔 1s 左右执行一次 fsync 操作,周期 1s
是可以配置的。这是在数据安全性和性能之间做了一个折中,在保持高性能的同时,尽可能
使得数据少丢失。
Redis 同样也提供了另外两种策略,一个是永不 fsync——让操作系统来决定合适同步磁
盘,很不安全,另一个是来一个指令就 fsync 一次——非常慢。但是在生产环境基本不会使
用,了解一下即可。
运维
快照是通过开启子进程的方式进行的,它是一个比较耗资源的操作。
1、遍历整个内存,大块写磁盘会加重系统负载
2、AOF 的 fsync 是一个耗时的 IO 操作,它会降低 Redis 性能,同时也会增加系
统 IO 负担
所以通常 Redis 的主节点是不会进行持久化操作,持久化操作主要在从节点进行。从节
点是备份节点,没有来自客户端请求的压力,它的操作系统资源往往比较充沛。
但是如果出现网络分区,从节点长期连不上主节点,就会出现数据不一致的问题,特别
是在网络分区出现的情况下又不小心主节点宕机了,那么数据就会丢失,所以在生产环境要
做好实时监控工作,保证网络畅通或者能快速修复。另外还应该再增加一个从节点以降低网
络分区的概率,只要有一个从节点数据同步正常,数据也就不会轻易丢失。
Redis 4.0 混合持久化
重启 Redis 时,我们很少使用 rdb 来恢复内存状态,因为会丢失大量数据。我们通常
使用 AOF 日志重放,但是重放 AOF 日志性能相对 rdb 来说要慢很多,这样在 Redis 实
例很大的情况下,启动需要花费很长的时间。
Redis 4.0 为了解决这个问题,带来了一个新的持久化选项——混合持久化。将 rdb 文
件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是自
持久化开始到持久化结束的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小。
于是在 Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可
以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。
小结
这就是管道的本质了,它并不是服务器的什么特性,而是客户端通过改变了读写的顺序
带来的性能的巨大提升。
Redis 事务的基本使用
优化
上面的 Redis 事务在发送每个指令到事务缓存队列时都要经过一次网络读写,当一个事
务内部的指令较多时,需要的网络 IO 时间也会线性增长。所以通常 Redis 的客户端在执行
事务时都会结合 pipeline 一起使用,这样可以将多次 IO 操作压缩为单次 IO 操作。比如我
们在使用 Python 的 Redis 客户端时执行事务时是要强制使用 pipeline 的。
Watch
乐观锁。
watch 会在事务开始之前盯住 1 个或多个关键变量,当事务执行时,也就是服务器收到
了 exec 指令要顺序执行缓存的事务队列时,Redis 会检查关键变量自 watch 之后,是否被
修改了 (包括当前事务所在的客户端)。如果关键变量被人动过了,exec 指令就会返回 null
回复告知客户端事务执行失败,这个时候客户端一般会选择重试。
注意事项
Redis 禁止在 multi 和 exec 之间执行 watch 指令,而必须在 multi 之前做好盯住关键
变量,否则会出错。
PubSub 缺点
PubSub 的生产者传递过来一个消息,Redis 会直接找到相应的消费者传递过去。如果一
个消费者都没有,那么消息直接丢弃。如果开始有三个消费者,一个消费者突然挂掉了,生
产者会继续发送消息,另外两个消费者可以持续收到消息。但是挂掉的消费者重新连上的时
候,这断连期间生产者发送的消息,对于这个消费者来说就是彻底丢失了。
如果 Redis 停机重启,PubSub 的消息是不会持久化的,毕竟 Redis 宕机就相当于一个
消费者都没有,所有的消息直接被丢弃。
正是因为 PubSub 有这些缺点,它几乎找不到合适的应用场景。所以 Redis 的作者单独
开启了一个项目 Disque 专门用来做多播消息队列。该项目目前没有成熟,一直长期处于
Beta 版本,但是相应的客户端 sdk 已经非常丰富了,就待 Redis 作者临门一脚发布一个
Release 版本。关于 Disque 的更多细节,本小册不会多做详细介绍,感兴趣的同学可以去阅
读相关文档。
补充
近期 Redis5.0 新增了 Stream 数据结构,这个功能给 Redis 带来了持久化消息队列,
从此 PubSub 可以消失了,Disqueue 估计也永远发不出它的 Release 版本了。具体内容请读
者阅读 Stream 章节内容(第 23 节)。
32bit vs 64bit
Redis 如果使用 32bit 进行编译,内部所有数据结构所使用的指针空间占用会少一半,
如果你对 Redis 使用内存不超过 4G,可以考虑使用 32bit 进行编译,可以节约大量内存。
4G 的容量作为一些小型站点的缓存数据库是绰绰有余了,如果不足还可以通过增加实例的
方式来解决。
小对象压缩存储 (ziplist)
如果 Redis 内部管理的集合数据结构很小,它会使用紧凑存储形式压缩存储。
这就好比 HashMap 本来是二维结构,但是如果内部元素比较少,使用二维结构反而浪
费空间,还不如使用一维数组进行存储,需要查找时,因为元素少进行遍历也很快,甚至可
以比 HashMap 本身的查找还要快。比如下面我们可以使用数组来模拟 HashMap 的增删改
操作。
Redis 的 intset 是一个紧凑的整数数组结构,它用于存放元素都是整数的并且元素个数
较少的 set 集合。
如果整数可以用 uint16 表示,那么 intset 的元素就是 16 位的数组,如果新加入的整
数超过了 uint16 的表示范围,那么就使用 uint32 表示,如果新加入的元素超过了 uint32
的表示范围,那么就使用 uint64 表示,Redis 支持 set 集合动态从 uint16 升级到 uint32,
再升级到 uint64
如果 set 里存储的是字符串,那么 sadd 立即升级为 hashtable 结构。还记得 Java 的
HashSet 么,它内部是使用 HashMap 实现的。
内存回收机制
Redis 虽然无法保证立即回收已经删除的 key 的内存,但是它会重用那些尚未回收的空
闲内存。这就好比电影院里虽然人走了,但是座位还在,下一波观众来了,直接坐就行。而
操作系统回收内存就好比把座位都给搬走了。这个比喻是不是很 6?
内存分配算法
最终一致
Redis 的主从数据是异步同步的,所以分布式的 Redis 系统并不满足「一致性」要求。
当客户端在 Redis 的主节点修改了数据后,立即返回,即使在主从网络断开的情况下,主节
点依旧可以正常对外提供修改服务,所以 Redis 满足「可用性」。
Redis 保证「最终一致性」,从节点会努力追赶主节点,最终从节点的状态会和主节点
的状态将保持一致。如果网络断开了,主从节点的数据将会出现大量不一致,一旦网络恢
复,从节点会采用多种策略努力追赶上落后的数据,继续尽力保持和主节点一致。
主从同步
Redis 同步支持主从同步和从从同步,从从同步功能是 Redis 后续版本增加的功能,为
了减轻主库的同步负担。后面为了描述上的方便,统一理解为主从同步。
增量同步
Redis 同步的是指令流,主节点会将那些对自己的状态产生修改性影响的指令记录在本
地的内存 buffer 中,然后异步将 buffer 中的指令同步到从节点,从节点一边执行同步的指
令流来达到和主节点一样的状态,一遍向主节点反馈自己同步到哪里了 (偏移量)。
因为内存的 buffer 是有限的,所以 Redis 主库不能将所有的指令都记录在内存 buffer
中。Redis 的复制内存 buffer 是一个定长的环形数组,如果数组内容满了,就会从头开始覆
盖前面的内容。
如果因为网络状况不好,从节点在短时间内无法和主节点进行同步,那么当网络状况恢
复时,Redis 的主节点中那些没有同步的指令在 buffer 中有可能已经被后续的指令覆盖掉
了,从节点将无法直接通过指令流来进行同步,这个时候就需要用到更加复杂的同步机制 — — 快照同步。
快照同步
快照同步是一个非常耗费资源的操作,它首先需要在主库上进行一次 bgsave 将当前内
存的数据全部快照到磁盘文件中,然后再将快照文件的内容全部传送到从节点。从节点将快照文件接受完毕后,立即执行一次全量加载,加载之前先要将当前内存的数据清空。加载完
毕后通知主节点继续进行增量同步。
在整个快照同步进行的过程中,主节点的复制 buffer 还在不停的往前移动,如果快照同
步的时间过长或者复制 buffer 太小,都会导致同步期间的增量指令在复制 buffer 中被覆
盖,这样就会导致快照同步完成后无法进行增量复制,然后会再次发起快照同步,如此极有
可能会陷入快照同步的死循环。
所以务必配置一个合适的复制 buffer 大小参数,避免快照复制的死循环。
增加从节点
当从节点刚刚加入到集群时,它必须先要进行一次快照同步,同步完成后再继续进行增
量同步。
无盘复制
主节点在进行快照同步时,会进行很重的文件 IO 操作,特别是对于非 SSD 磁盘存储
时,快照会对系统的负载产生较大影响。特别是当系统正在进行 AOF 的 fsync 操作时如果
发生快照,fsync 将会被推迟执行,这就会严重影响主节点的服务效率。
所以从 Redis 2.8.18 版开始支持无盘复制。所谓无盘复制是指主服务器直接通过套接字
将快照内容发送到从节点,生成快照是一个遍历的过程,主节点会一边遍历内存,一遍将序
列化的内容发送到从节点,从节点还是跟之前一样,先将接收到的内容存储到磁盘文件中,
再进行一次性加载。
我们可以将 Redis Sentinel 集群看成是一个 ZooKeeper 集群,它是集群高可用的心脏,
它一般是由 3~5 个节点组成,这样挂了个别节点集群还可以正常运转。
它负责持续监控主从节点的健康,当主节点挂掉时,自动选择一个最优的从节点切换为
主节点。客户端来连接集群时,会首先连接 sentinel,通过 sentinel 来查询主节点的地址,
然后再去连接主节点进行数据交互。当主节点发生故障时,客户端会重新向 sentinel 要地
址,sentinel 会将最新的主节点地址告诉客户端。如此应用程序将无需重启即可自动完成节
点切换。比如上图的主节点挂掉后,集群将可能自动调整为下图所示结构。
Codis 分片原理
Codis 要负责将特定的 key 转发到特定的 Redis 实例,那么这种对应关系 Codis 是如
何管理的呢?
Codis 将所有的 key 默认划分为 1024 个槽位(slot),它首先对客户端传过来的 key 进 行 crc32 运算计算哈希值,再将 hash 后的整数值对 1024 这个整数进行取模得到一个余
数,这个余数就是对应 key 的槽位。
每个槽位都会唯一映射到后面的多个 Redis 实例之一,Codis 会在内存维护槽位和
Redis 实例的映射关系。这样有了上面 key 对应的槽位,那么它应该转发到哪个 Redis 实例
就很明确了。
槽位数量默认是 1024,它是可以配置的,如果集群节点比较多,建议将这个数值配置大
一些,比如 2048、4096。
不同的 Codis 实例之间槽位关系如何同步?
如果 Codis 的槽位映射关系只存储在内存里,那么不同的 Codis 实例之间的槽位关系
就无法得到同步。所以 Codis 还需要一个分布式配置存储数据库专门用来持久化槽位关系。
Codis 开始使用 ZooKeeper,后来连 etcd 也一块支持了。
扩容
刚开始 Codis 后端只有一个 Redis 实例,1024 个槽位全部指向同一个 Redis。然后一
个 Redis 实例内存不够了,所以又加了一个 Redis 实例。这时候需要对槽位关系进行调整,
将一半的槽位划分到新的节点。这意味着需要对这一半的槽位对应的所有 key 进行迁移,迁
移到新的 Redis 实例。
自动均衡
Redis 新增实例,手工均衡 slots 太繁琐,所以 Codis 提供了自动均衡功能。自动均衡会
在系统比较空闲的时候观察每个 Redis 实例对应的 Slots 数量,如果不平衡,就会自动进行
迁移。
Codis 的代价
同样为了支持扩容,单个 key 对应的 value 不宜过大,因为集群的迁移的最小单位是
key,对于一个 hash 结构,它会一次性使用 hgetall 拉取所有的内容,然后使用 hmset 放置
到另一个节点。如果 hash 内部的 kv 太多,可能会带来迁移卡顿。官方建议单个集合结构
的总字节容量不要超过 1M。如果我们要放置社交关系数据,例如粉丝列表这种,就需要注
意了,可以考虑分桶存储,在业务上作折中。
Codis 的优点
Codis 在设计上相比 Redis Cluster 官方集群方案要简单很多,因为它将分布式的问题交
给了第三方 zk/etcd 去负责,自己就省去了复杂的分布式一致性代码的编写维护工作。而
Redis Cluster 的内部实现非常复杂,它为了实现去中心化,混合使用了复杂的 Raft 和
Gossip 协议,还有大量的需要调优的配置参数,当集群出现故障时,维护人员往往不知道从
何处着手。
RedisCluster 是 Redis 的亲儿子,它是 Redis 作者自己提供的 Redis 集群化方案。
相对于 Codis 的不同,它是去中心化的,如图所示,该集群有三个 Redis 节点组成,
每个节点负责整个集群的一部分数据,每个节点负责的数据多少可能不一样。这三个节点相
互连接组成一个对等的集群,它们之间通过一种特殊的二进制协议相互交互集群信息。
Stream 的消费模型借鉴了 Kafka 的消费分组的概念,它弥补了 Redis Pub/Sub 不能持
久化消息的缺陷。但是它又不同于 kafka,Kafka 的消息可以分 partition,而 Stream 不行。
如果非要分 parition 的话,得在客户端做,提供不同的 Stream 名称,对消息进行 hash 取
模来选择往哪个 Stream 里塞。
Redis 每秒执行多少次指令?
Redis 连接了多少客户端?
Redis 内存占用多大 ?
复制积压缓冲区多大?
Redlock 算法
过期的 key 集合
定时扫描策略
Redis 默认会每秒进行十次过期扫描,过期扫描不会遍历过期字典中所有的 key,而是
采用了一种简单的贪心策略。
1、从过期字典中随机 20 个 key;
2、删除这 20 个 key 中已经过期的 key;
3、如果过期的 key 比率超过 1/4,那就重复步骤 1;
同时,为了保证过期扫描不会出现循环过度,导致线程卡死现象,算法还增加了扫描时
间的上限,默认不会超过 25ms。
所以业务开发人员一定要注意过期时间,如果有大批量的 key 过期,要给过期时间设置
一个随机范围,而不能全部在同一时间过期。
在一些活动系统中,因为活动是一期一会,下一期活动举办时,前面几期的很多数据都
可以丢弃了,所以需要给相关的活动数据设置一个过期时间,以减少不必要的 Redis 内存占
用。如果不加注意,你可能会将过期时间设置为活动结束时间再增加一个常量的冗余时间,
如果参与活动的人数太多,就会导致大量的 key 同时过期。
从库的过期策略
LRU 算法
近似 LRU 算法
同时 Redis3.0 在算法中增加了淘汰池,进一步提升了近似 LRU 算法的效果。
淘汰池是一个数组,它的大小是 maxmemory_samples,在每一次淘汰循环中,新随机出
来的 key 列表会和淘汰池中的 key 列表进行融合,淘汰掉最旧的一个 key 之后,保留剩余
较旧的 key 列表放入淘汰池中留待下一个循环。
Redis 为什么要懒惰删除(lazy free)?
删除指令 del 会直接释放对象的内存,大部分情况下,这个指令非常快,没有明显延
迟。不过如果删除的 key 是一个非常大的对象,比如一个包含了千万元素的 hash,那么删
除操作就会导致单线程卡顿。
Redis 为了解决这个卡顿问题,在 4.0 版本引入了 unlink 指令,它能对删除操作进行懒
处理,丢给后台线程来异步回收内存。
unlink key
OK
如果有多线程的开发经验,你肯定会担心这里的线程安全问题,会不会出现多个线程同
时并发修改数据结构的情况存在。
关于这点,我打个比方。可以将整个 Redis 内存里面所有有效的数据想象成一棵大树。
当 unlink 指令发出时,它只是把大树中的一个树枝别断了,然后扔到旁边的火堆里焚烧 (异
步线程池)。树枝离开大树的一瞬间,它就再也无法被主线程中的其它指令访问到了,因为主
线程只会沿着这颗大树来访问。
flush
Redis 提供了 flushdb 和 flushall 指令,用来清空数据库,这也是极其缓慢的操作。
Redis 4.0 同样给这两个指令也带来了异步化,在指令后面增加 async 参数就可以将整棵大树
连根拔起,扔给后台线程慢慢焚烧。
异步队列
主线程将对象的引用从「大树」中摘除后,会将这个 key 的内存回收操作包装成一个任
务,塞进异步任务队列,后台线程会从这个异步队列中取任务。任务队列被主线程和异步线
程同时操作,所以必须是一个线程安全的队列。
不是所有的 unlink 操作都会延后处理,如果对应 key 所占用的内存很小,延后处理就
没有必要了,这时候 Redis 会将对应的 key 内存立即回收,跟 del 指令一样。
AOF Sync 也很慢
Redis 需要每秒一次(可配置)同步 AOF 日志到磁盘,确保消息尽量不丢失,需要调用
sync 函数,这个操作会比较耗时,会导致主线程的效率下降,所以 Redis 也将这个操作移到
异步线程来完成。执行 AOF Sync 操作的线程是一个独立的异步线程,和前面的懒惰删除线
程不是一个线程,同样它也有一个属于自己的任务队列,队列里只用来存放 AOF Sync 任
务。
端口安全
Lua 脚本安全
spiped 原理
hash函数
hashtable 的性能好不好完全取决于 hash 函数的质量。hash 函数如果可以将 key 打散
的比较均匀,那么这个 hash 函数就是个好函数。Redis 的字典默认的 hash 函数是
siphash。siphash 算法即使在输入 key 很小的情况下,也可以产生随机性特别好的输出,而
且它的性能也非常突出。对于 Redis 这样的单线程来说,字典数据结构如此普遍,字典操作
也会非常频繁,hash 函数自然也是越快越好。
hash 攻击
如果 hash 函数存在偏向性,黑客就可能利用这种偏向性对服务器进行攻击。存在偏向
性的 hash 函数在特定模式下的输入会导致 hash 第二维链表长度极为不均匀,甚至所有的
元素都集中到个别链表中,直接导致查找效率急剧下降,从 O(1)退化到 O(n)。有限的服务器
计算能力将会被 hashtable 的查找效率彻底拖垮。这就是所谓 hash 攻击。
扩容条件
正常情况下,当 hash 表中元素的个数等于第一维数组的长度时,就会开始扩容,扩容
的新数组是原数组大小的 2 倍。不过如果 Redis 正在做 bgsave,为了减少内存页的过多分
离 (Copy On Write),Redis 尽量不去扩容 (dict_can_resize),但是如果 hash 表已经非常满
了,元素的个数已经达到了第一维数组长度的 5 倍 (dict_force_resize_ratio),说明 hash 表
已经过于拥挤了,这个时候就会强制扩容。
缩容条件
当 hash 表因为元素的逐渐删除变得越来越稀疏时,Redis 会对 hash 表进行缩容来减少
hash 表的第一维数组空间占用。缩容的条件是元素个数低于数组长度的 10%。缩容不会考
虑 Redis 是否正在做 bgsave。
set 的结构
Redis 里面 set 的结构底层实现也是字典,只不过所有的 value 都是 NULL,其它的特
性和字典一模一样。
Redis 5.0 又引入了一个新的数据结构 listpack,它是对 ziplist 结构的改进,在存储空间
上会更加节省,而且结构上也比 ziplist 要精简。它的整体形式和 ziplist 还是比较接近的,
如果你认真阅读了 ziplist 的内部结构分析,那么 listpack 也是比较容易理解的。
取代 ziplist
listpack 的设计的目的是用来取代 ziplist,不过当下还没有做好替换 ziplist 的准备,因
为有很多兼容性的问题需要考虑,ziplist 在 Redis 数据结构中使用太广泛了,替换起来复杂
度会非常之高。它目前只使用在了新增加的 Stream 数据结构中。