适用命令:redis-cli -h 127.0.0.1 -p 6379 --bigkeys
当我们调用套接字的读写方法,默认它们是阻塞的,比如
read
方法要传递进去一个参数
n
,表示读取这么多字节后再返回,如果没有读够线程就会卡在那里,直到新的数据到来或者
连接关闭了,
read
方法才可以返回,线程才能继续处理。而
write
方法一般来说不会阻塞,除
非内核为套接字分配的写缓冲区已经满了,
write
方法就会阻塞,直到缓存区中有空闲空间挪
出来了。
非阻塞
IO
在套接字对象上提供了一个选项
Non_Blocking
,当这个选项打开时,读写方
法不会阻塞,而是能读多少读多少,能写多少写多少。能读多少取决于内核为套接字分配的
读缓冲区内部的数据字节数,能写多少取决于内核为套接字分配的写缓冲区的空闲空间字节
数。读方法和写方法都会通过返回值来告知程序实际读写了多少字节。
有了非阻塞
IO
意味着线程在读写
IO
时可以不必再阻塞了,读写可以瞬间完成然后线
程可以继续干别的事了。
事件
轮询
(
多路复用
)
非阻塞
IO
有个问题,那就是线程要读数据,结果读了一部分就返回了,线程如何知道
何时才应该继续读。也就是当数据到来时,线程如何得到通知。写也是一样,如果缓冲区满
了,写不完,剩下的数据何时才应该继续写,线程也应该得到通知。
事件轮询
API
就是用来解决这个问题的,最简单的事件轮询
API
是
select
函数,它是
操作系统提供给用户程序的
API
。输入是读写描述符列表
read_fds & write_fds
,输出是与之
对应的可读可写事件。同时还提供了一个
timeout
参数,如果没有任何事件到来,那么就最多
等待
timeout
时间,线程处于阻塞状态。一旦期间有任何事件到来,就可以立即返回。时间过
了之后还是没有任何事件到来,也会立即返回。拿到事件后,线程就可以继续挨个处理相应
的事件。处理完了继续过来轮询。于是线程就进入了一个死循环,我们把这个死循环称为事
件循环,一个循环为一个周期。
每个客户端套接字
socket
都有对应的读写文件描述符。
read_events, write_events = select(read_fds, write_fds, timeout)
for event in read_events:
handle_read(event.fd)
for event in write_events:
handle_write(event.fd)
handle_others()
# 处理其它事情,如定时任务等
因为我们通过
select
系统调用同时处理多个通道描述符的读写事件,因此我们将这类系
统调用称为多路复用
API
。现代操作系统的多路复用
API
已经不再使用
select
系统调用,而
改用
epoll(linux)
和
kqueue(freebsd & macosx)
,因为
select
系统调用的性能在描述符特别多时
性能会非常差。它们使用起来可能在形式上略有差异,但是本质上都是差不多的,都可以使
用上面的伪代码逻辑进行理解。
服务器套接字
serversocket
对象的读操作是指调用
accept
接受客户端新连接。何时有新连
接到来,也是通过
select
系统调用的读事件来得到通知的。
事件轮询
API
就是
Java
语言里面的
NIO
技术
Java
的
NIO
并不是
Java
特有的技术,其它计算机语言都有这个技术,只不过换了一
个词汇,不叫
NIO
而已。
指令
队列
Redis
会将每个客户端套接字都关联一个指令队列。客户端的指令通过队列来排队进行
顺序处理,先到先服务。
响
应队列
Redis
同样也会为每个客户端套接字关联一个响应队列。
Redis
服务器通过响应队列来将
指令的返回结果回复给客户端。 如果队列为空,那么意味着连接暂时处于空闲状态,不需要
去获取写事件,也就是可以将当前的客户端描述符从
write_fds
里面移出来。等到队列有数据
了,再将描述符放进去。避免
select
系统调用立即返回写事件,结果发现没什么数据可以
写。出这种情况的线程会飙高
CPU
。
定
时任务
服务器处理要响应
IO
事件外,还要处理其它事情。比如定时任务就是非常重要的一件
事。如果线程阻塞在
select
系统调用上,定时任务将无法得到准时调度。那
Redis
是如何解
决这个问题的呢?
Redis
的定时任务会记录在一个称为
最小堆
的数据结构中。这个堆中,最快要执行的任
务排在堆的最上方。在每个循环周期,
Redis
都会将最小堆里面已经到点的任务立即进行处
理。处理完毕后,将最快要执行的任务还需要的时间记录下来,这个时间就是
select
系统调
用的
timeout
参数。因为
Redis
知道未来
timeout
时间内,没有其它定时任务需要处理,所以
可以安心睡眠
timeout
的时间。
Nginx
和
Node
的事件处理原理和
Redis
也是类似的
原理
2
:
交
头接耳
——
通信
协议
Redis
的作者认为数据库系统的瓶颈一般不在于网络流量,而是数据库自身内部逻辑处
理上。所以即使
Redis
使用了浪费流量的文本协议,依然可以取得极高的访问性能。
Redis
将所有数据都放在内存,用一个单线程对外提供服务,单个节点在跑满一个
CPU
核心的情
况下可以达到了
10w/s
的超高
QPS
。
RESP(Redis Serialization Protocol)
RESP
是
Redis
序列化协议的简写。它是一种直观的文本协议,优势在于实现异常简
单,解析性能极好
小
结
Redis
协议里有大量冗余的回车换行符,但是这不影响它成为互联网技术领域非常受欢
迎的一个文本协议。
有很多开源项目使用 RESP 作为它的通讯协议
。在技术领域性能并不总
是一切,还有简单性、易理解性和易实现性,这些都需要进行适当权衡
原理
3
:
未雨
绸缪
——
持久化
Redis
的数据全部在内存里,如果突然宕机,数据就会全部丢失,因此必须有一种机制
来保证
Redis
的数据不会因为故障而丢失,这种机制就是
Redis
的持久化机制。
Redis
的持久化机制有两种,
第一种是快照,第二种是 AOF 日志
。快照是一次全量备
份,
AOF
日志是连续的增量备份。快照是内存数据的二进制序列化形式,在存储上非常紧
凑,而
AOF
日志记录的是内存数据修改的指令记录文本。
AOF
日志在长期的运行过程中会
变的无比庞大,数据库重启时需要加载
AOF
日志进行指令重放,这个时间就会无比漫长。
所以需要定期进行
AOF
重写,给
AOF
日志进行瘦身。
快照原理
Redis 使用操作系统的多进程 COW(Copy On Write) 机制来实现快照持久化。会使用操作系统的 COW 机制来进行数据段页面的分离
AOF
原理
AOF
日志存储的是
Redis
服务器的顺序指令序列,
AOF
日志只记录对内存进行修改的
指令记录。
Redis
在长期运行的过程中,
AOF
的日志会越变越长。如果实例宕机重启,重放整个
AOF
日志会非常耗时,导致长时间
Redis
无法对外提供服务。所以需要对
AOF
日志瘦
身
AOF
重写
Redis
提供了
bgrewriteaof
指令用于对
AOF
日志进行瘦身。其原理就是开辟一个子进
程对内存进行遍历转换成一系列
Redis
的操作指令,序列化到一个新的
AOF
日志文件中。
序列化完毕后再将操作期间发生的增量
AOF
日志追加到这个新的
AOF
日志文件中,追加
完毕后就立即替代旧的
AOF
日志文件了,瘦身工作就完成了。
fsync
在生产环境的服务器中,
Redis
通常是每隔
1s
左右执行一次
fsync
操作,周期
1s
是可以配置的。这是在数据安全性和性能之间做了一个折中,在保持高性能的同时,尽可能
使得数据少丢失。
运
维
快照是通过开启子进程的方式进行的,它是一个比较耗资源的操作。
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
全量文件重放,重启效率因此大幅得到提升
原理
4
:
雷
厉风行
——
管道
大多数同学一直以来对
Redis
管道有一个误解,他们以为这是
Redis
服务器提供的一种
特别的技术,有了这种技术就可以加速
Redis
的存取效率。但是实际上
Redis
管道
(Pipeline)
本身并不是
Redis
服务器直接提供的技术,这个技术本质上是由客户端提供的,
跟服务器没有什么直接的关系。下面我们对这块做一个深入探究。
Redis
的消息交互
管道压力测试
39W qps到极限

深入理解管道本质
我们开始以为
write
操作是要等到对方收到消息才会返回,但实际上不是这样的。
write
操作只负责将数据写到本地操作系统内核的发送缓冲然后就返回了。剩下的事交给操作系统
内核异步将数据送到目标机器。但是如果发送缓冲满了,那么就需要等待缓冲空出空闲空间
来,这个就是写操作
IO
操作的真正耗时。
我们开始以为
read
操作是从目标机器拉取数据,但实际上不是这样的。
read
操作只负
责将数据从本地操作系统内核的接收缓冲中取出来就了事了。但是如果缓冲是空的,那么就
需要等待数据到来,这个就是读操作
IO
操作的真正耗时。
所以对于
value = redis.get(key)
这样一个简单的请求来说,
write
操作几乎没有耗时,直接
写到发送缓冲就返回,而
read
就会比较耗时了,因为它要等待消息经过网络路由到目标机器
处理后的响应消息
,
再回送到当前的内核读缓冲才可以返回。这才是一个网络来回的真正开
销。
而对于管道来说,连续的
write
操作根本就没有耗时,之后第一个
read
操作会等待一个
网络的来回开销,然后所有的响应消息就都已经回送到内核的读缓冲了,后续的
read
操作
直接就可以从缓冲拿到结果,瞬间就返回了
原理
5
:
同舟共
济
——
事
务
Redis
在形式上看起来也差不多,分别是
multi/exec/discard
。
multi
指示事务的开始,
exec
指示事务的执行,
discard
指示事务的丢弃。
> multi
OK
> incr books
QUEUED
> incr books
QUEUED
> exec
(integer) 1
(integer) 2
上面的指令演示了一个完整的事务过程,所有的指令在
exec
之前不执行,而是缓存在
服务器的一个事务队列中,服务器一旦收到
exec
指令,才开执行整个事务队列,执行完毕
后一次性返回所有指令的运行结果。因为
Redis
的单线程特性,它不用担心自己在执行队列
的时候被其它指令打搅,可以保证他们能得到的「原子性」执行。
上图显示了以上事务过程完整的交互效果。
QUEUED
是一个简单字符串,同
OK
是一
个形式,它表示指令已经被服务器缓存到队列里了
redis事务没有原子性
优化
上面的
Redis
事务在发送每个指令到事务缓存队列时都要经过一次网络读写,当一个事
务内部的指令较多时,需要的网络
IO
时间也会线性增长。所以
通常 Redis 的客户端在执行
事务时都会结合 pipeline 一起使用,这样可以将多次 IO 操作压缩为单次 IO 操作
。比如我
们在使用
Python
的
Redis
客户端时执行事务时是要强制使用
pipeline
的。
pipe = redis.pipeline(transaction=true)
pipe.multi()
pipe.incr("books")
pipe.incr("books")
values = pipe.execute()
Watch
Redis
提供了这种
watch
的机制,它就是一种乐观锁
watch
会在事务开始之前盯住
1
个或多个关键变量,当事务执行时,也就是服务器收到
了
exec
指令要顺序执行缓存的事务队列时,
Redis
会检查关键变量自
watch
之后,是否被
修改了
(
包括当前事务所在的客户端
)
。如果关键变量被人动过了,
exec
指令就会返回
null
回复告知客户端事务执行失败,这个时候客户端一般会选择重试
注意事项
Redis
禁止在
multi
和
exec
之间执行
watch
指令,而必须在
multi
之前做好盯住关键
变量,否则会出错。
使用Java实现余额加倍
public class TransactionDemo {
public static void main(String[] args) {
Jedis jedis = new Jedis();
String userId = "abc";
String key = keyFor(userId);
jedis.setnx(key, String.valueOf(5)); // setnx 做初始化
System.out.println(doubleAccount(jedis, userId));
jedis.close();
}
public static int doubleAccount(Jedis jedis, String userId) {
String key = keyFor(userId);
while (true) {
jedis.watch(key);
int value = Integer.parseInt(jedis.get(key));
value *= 2; // 加倍
Transaction tx = jedis.multi();
tx.set(key, String.valueOf(value));
List<Object> res = tx.exec();
if (res != null) {
break; // 成功了
}
}
return Integer.parseInt(jedis.get(key)); // 重新获取余额
}
public static String keyFor(String userId) {
return String.format("account_{}", userId);
}
}
原理
6
:
小道消息
—— PubSub
为了支持消息多播,
Redis
不能再依赖于那
5
种基本数据类型了。它单独使用了一个模
块来支持消息多播,这个模块的名字叫着
PubSub
,也就是
PublisherSubscriber
,发布者订阅
者模型。
原理
7
:
开源
节流
——
小
对象压缩
Redis
是一个非常耗费内存的数据库,它所有的数据都放在内存里。如果我们不注意节
约使用内存,
Redis
就会因为我们的无节制使用出现内存不足而崩溃。
Redis
作者为了优化数
据结构的内存占用,也苦心孤诣增加了非常多的优化点
32bit vs 64bit
Redis
如果使用
32bit
进行编译,内部所有数据结构所使用的指针空间占用会少一半,
如果你对
Redis
使用内存不超过
4G
,可以考虑使用
32bit
进行编译,可以节约大量内存。
4G
的容量作为一些小型站点的缓存数据库是绰绰有余了,如果不足还可以通过增加实例的
方式来解决。
小
对象压缩存储
(ziplist)
Redis
的
ziplist
是一个紧凑的字节数组结构,如下图所示,每个元素之间都是紧挨着
的
Redis
的
intset
是一个紧凑的整数数组结构,它用于存放元素都是整数的并且元素个数
较少的
set
集合
存储界限
当集合对象的元素不断增加,或者某个
value
值过大,这种小对象存储也会被升级为标准结构
内存回收机制
Redis
并不总是可以将空闲内存立即归还给操作系统。
如果当前
Redis
内存有
10G
,当你删除了
1GB
的
key
后,再去观察内存,你会发现
内存变化不会太大。原因是操作系统回收内存是以页为单位,如果这个页上只要有一个
key
还在使用,那么它就不能被回收。
Redis
虽然删除了
1GB
的
key
,但是这些
key
分散到了
很多页面中,每个页面都还有其它
key
存在,这就导致了内存不会立即被回收。
不过,如果你执行
flushdb
,然后再观察内存会发现内存确实被回收了。原因是所有的
key
都干掉了,大部分之前使用的页面都完全干净了,会立即被操作系统回收。
Redis
虽然无法保证立即回收已经删除的
key
的内存,但是它会重用那些尚未回收的空
闲内存
内存分配算法
内存分配是一个非常复杂的课题,需要适当的算法划分内存页,需要考虑内存碎片,需
要平衡性能和效率。
Redis
为了保持自身结构的简单性,在内存分配这里直接做了甩手掌柜,将内存分配的
细节丢给了第三方内存分配库去实现。目前
Redis
可以使用
jemalloc(facebook)
库来管理内
存,也可以切换到
tcmalloc(google)
。因为
jemalloc
相比
tcmalloc
的性能要稍好一些,所以
Redis
默认使用了
jemalloc
原理
8
:
有
备无患
——
主从同步
很多企业都没有使用到
Redis
的集群,但是至少都做了主从。有了主从,当
master
挂
掉的时候,运维让从库过来接管,服务就可以继续,否则
master
需要经过数据恢复和重启
的过程,这就可能会拖很长的时间,影响线上业务的持续服务。
CAP
原理
CAP
原理就好比分布式领域的牛顿定律,它是分布式存储的理论基石。自打
CAP
的论
文发表之后,分布式存储中间件犹如雨后春笋般一个一个涌现出来。理解这个原理其实很简
单,本节我们首先对这个原理进行一些简单的讲解。
C
-
C
onsistent
,
一致性
A
-
A
vailability
,
可用性
P
-
P
artition tolerance
,
分区容忍性
分布式系统的节点往往都是分布在不同的机器上进行网络隔离开的,这意味着必然会有
网络断开的风险,这个网络断开的场景的专业词汇叫着「
网络分区
」
一句话概括
CAP
原理就是——
网络分区发生时,一致性和可用性两难全
增量同步
Redis
同步的是指令流,主节点会将那些对自己的状态产生修改性影响的指令记录在本
地的内存
buffer
中,然后异步将
buffer
中的指令同步到从节点,从节点一边执行同步的指
令流来达到和主节点一样的状态,一遍向主节点反馈自己同步到哪里了
(
偏移量
)
。
因为内存的
buffer
是有限的,所以
Redis
主库不能将所有的指令都记录在内存
buffer
中。
Redis
的复制内存
buffer
是一个定长的环形数组,如果数组内容满了,就会从头开始覆
盖前面的内容。
如果因为网络状况不好,从节点在短时间内无法和主节点进行同步,那么当网络状况恢
复时,
Redis
的主节点中那些没有同步的指令在
buffer
中有可能已经被后续的指令覆盖掉
了,从节点将无法直接通过指令流来进行同步,这个时候就需要用到更加复杂的同步机制 —
— 快照同步
快照同步
快照同步是一个非常耗费资源的操作,它首先需要在主库上进行一次
bgsave
将当前内
存的数据全部快照到磁盘文件中,然后再将快照文件的内容全部传送到从节点。从节点将快
照文件接受完毕后,立即执行一次全量加载,加载之前先要将当前内存的数据清空。加载完
毕后通知主节点继续进行增量同步。
在整个快照同步进行的过程中,主节点的复制
buffer
还在不停的往前移动,如果快照同
步的时间过长或者复制
buffer
太小,都会导致同步期间的增量指令在复制
buffer
中被覆
盖,这样就会导致快照同步完成后无法进行增量复制,然后会再次发起快照同步,如此极有
可能会陷入快照同步的死循环。所以务必配置一个合适的复制
buffer
大小参数,避免快照复制的死循环
增加从
节点
当从节点刚刚加入到集群时,它必须先要进行一次快照同步,同步完成后再继续进行增
量同步
无
盘复制
主节点在进行快照同步时,会进行很重的文件
IO
操作,特别是对于非
SSD
磁盘存储
时,快照会对系统的负载产生较大影响。特别是当系统正在进行
AOF
的
fsync
操作时如果
发生快照,
fsync
将会被推迟执行,这就会严重影响主节点的服务效率。
所以从
Redis 2.8.18
版开始支持无盘复制。所谓无盘复制是指主服务器直接通过套接字
将快照内容发送到从节点,生成快照是一个遍历的过程,主节点会一边遍历内存,一遍将序
列化的内容发送到从节点,从节点还是跟之前一样,先将接收到的内容存储到磁盘文件中,
再进行一次性加载
Wait
指令
Redis
的复制是异步进行的,
wait
指令可以让异步复制变身同步复制,确保系统的强一
致性
(
不严格
)
。
wait
指令是
Redis3.0
版本以后才出现的
wait
提供两个参数,第一个参数是从库的数量
N
,第二个参数是时间
t
,以毫秒为单
位。它表示等待
wait
指令之前的所有写操作同步到
N
个从库
(
也就是确保
N
个从库的同
步没有滞后
)
,最多等待时间
t
。如果时间
t=0
,表示无限等待直到
N
个从库同步完成达成
一致。
小
结
主从复制是
Redis
分布式的基础,
Redis
的高可用离开了主从复制将无从进行。后面的
章节我们会开始讲解
Redis
的集群模式,这几种集群模式都依赖于本节所讲的主从复制。
不过复制功能也不是必须的,如果你将
Redis
只用来做缓存,跟
memcache
一样来对
待,也就无需要从库做备份,挂掉了重新启动一下就行。但是只要你使用了
Redis
的持久化
功能,就必须认真对待主从复制,它是系统数据安全的基础保障
集群
1
:
李代桃僵
—— Sentinel(哨兵)
我们可以将
Redis Sentinel
集群看成是一个
ZooKeeper
集群,它是集群高可用的心脏,
它一般是由
3
~
5
个节点组成,这样挂了个别节点集群还可以正常运转。
它负责持续监控主从节点的健康,当主节点挂掉时,自动选择一个最优的从节点切换为
主节点。客户端来连接集群时,会首先连接
sentinel
,通过
sentinel
来查询主节点的地址,
然后再去连接主节点进行数据交互。当主节点发生故障时,客户端会重新向
sentinel
要地
址,
sentinel
会将最新的主节点地址告诉客户端。如此应用程序将无需重启即可自动完成节
点切换
消息
丢失
Redis 主从采用异步复制,意味着当主节点挂掉时,从节点可能没有收到全部的同步消
息,这部分未同步的消息就丢失了。如果主从延迟特别大,那么丢失的数据就可能会特别
多
。
Sentinel
无法保证消息完全不丢失,但是也尽可能保证消息少丢失。它有两个选项可以
限制主从延迟过大。(
异步刷盘可能会丢数据
)
min-slaves-to-write 1
min-slaves-max-lag 10
第一个参数表示主节点必须至少有一个从节点在进行正常复制,否则就停止对外写服
务,丧失可用性。
何为正常复制,何为异常复制?这个就是由第二个参数控制的,它的单位是秒,表示如
果
10s
没有收到从节点的反馈,就意味着从节点同步不正常,要么网络断开了,要么一直没
有给反馈
sentinel
的默认端口是
26379
,不同于
Redis
的默认端口
6379
,通过
sentinel
对象的
discover_xxx
方法可以发现主从地址,主地址只有一个,从地址可以有多个
有个问题是,但
sentinel
进行主从切换时,
客户端如何知道地址变更了
?
通过分析源
码,我发现
redis-py 在建立连接的时候进行了主库地址变更判断
。
连接池建立新连接时,会去查询主库地址,然后跟内存中的主库地址进行比对,如果变
更了,就断开所有连接,重新使用新地址建立新连接。如果是旧的主库挂掉了,那么所有正
在使用的连接都会被关闭,然后在重连时就会用上新地址。
但是这样还不够,如果是
sentinel
主动进行主从切换,主库并没有挂掉,而之前的主库
连接已经建立了在使用了,没有新连接需要建立,那这个连接是不是一致切换不了?
继续深入研究源码,我发现
redis-py 在另外一个点也做了控制。那就是在处理命令的时
候捕获了一个特殊的异常 ReadOnlyError,在这个异常里将所有的旧连接全部关闭了,后续指
令就会进行重连
。
主从切换后,之前的主库被降级到从库,所有的修改性的指令都会抛出
ReadonlyError
。
如果没有修改性指令,虽然连接不会得到切换,但是数据不会被破坏,所以即使不切换也没
关系
集群
2
:
分而治之
—— Codis
Codis
是
Redis
集群方案之一
Codis
分片原理
Codis
要负责将特定的
key
转发到特定的
Redis
实例,那么这种对应关系
Codis
是如
何管理的呢?
Codis
将所有的
key
默认划分为
1024
个槽位
(slot)
,它首先对客户端传过来的
key
进
行
crc32
运算计算哈希值,再将
hash
后的整数值对
1024
这个整数进行取模得到一个余
数,这个余数就是对应
key
的槽位
槽位数量默认是
1024
,它是可以配置的,如果集群节点比较多,建议将这个数值配置大
一些,比如
2048
、
4096
。
不同的
Codis
实例之间槽位关系如何同步
?
如果
Codis
的槽位映射关系只存储在内存里,那么不同的
Codis
实例之间的槽位关系
就无法得到同步。所以
Codis
还需要一个分布式配置存储数据库专门用来持久化槽位关系。
Codis
开始使用
ZooKeeper
,后来连
etcd
也一块支持了
Codis
将槽位关系存储在
zk
中,并且提供了一个
Dashboard
可以用来观察和修改槽位
关系,当槽位关系变化时,
Codis Proxy
会监听到变化并重新同步槽位关系,从而实现多个
Codis Proxy
之间共享相同的槽位关系配置
Codis
的代价
Codis
给
Redis
带来了扩容的同时,也损失了其它一些特性。因为
Codis
中所有的
key
分散在不同的
Redis
实例中,所以
事务就不能再支持了,事务只能在单个 Redis 实例中完
成
。同样
rename
操作也很危险,它的参数是两个
key
,如果这两个
key
在不同的
Redis
实
例中,
rename
操作是无法正确完成的。
Codis
的官方文档中给出了一系列不支持的命令列
表
Codis
的
优点
Codis
在设计上相比
Redis Cluster
官方集群方案要简单很多,因为它将分布式的问题交
给了第三方
zk/etcd
去负责,自己就省去了复杂的分布式一致性代码的编写维护工作。而
Redis Cluster
的内部实现非常复杂,它为了实现去中心化,混合使用了复杂的
Raft
和
Gossip
协议,还有大量的需要调优的配置参数,当集群出现故障时,维护人员往往不知道从
何处着手
集群
3
:
众志成城
—— Cluster
RedisCluster
是
Redis
的亲儿子,它是
Redis
作者自己提供的
Redis
集群化方案。
相对于
Codis
的不同,它是
去中心化
的,如图所示,该集群有三个
Redis
节点组成,
每个节点负责整个集群的一部分数据,每个节点负责的数据多少可能不一样。这三个节点相
互连接组成一个对等的集群,它们之间通过一种特殊的二进制协议相互交互集群信息
容
错
Redis Cluster
可以为每个主节点设置若干个从节点,单主节点故障时,集群会自动将其
中某个从节点提升为主节点。如果某个主节点没有从节点,那么当它发生故障时,集群将完
全处于不可用状态。不过
Redis
也提供了一个参数
cluster-require-full-coverage
可以允许部分
节点故障,其它节点还可以继续提供对外访问
网
络抖动
真实世界的机房网络往往并不是风平浪静的,它们经常会发生各种各样的小问题。比如
网络抖动就是非常常见的一种现象,突然之间部分连接变得不可访问,然后很快又恢复正
常
。
为解决这种问题,
Redis Cluster
提供了一种选项
cluster-node-timeout
,表示当某个节点持
续
timeout
的时间失联时,才可以认定该节点出现故障,需要进行主从切换。如果没有这个
选项,网络抖动会导致主从频繁切换
(
数据的重新复制
)
。
还有另外一个选项
cluster-slave-validity-factor
作为倍乘系数来放大这个超时时间来宽松容
错的紧急程度。如果这个系数为零,那么主从切换是不会抗拒网络抖动的。如果这个系数大
于
1
,它就成了主从切换的松弛系数
可能下
线
(PFAIL-Possibly Fail)
与确定下
线
(Fail)
因为
Redis Cluster 是去中心化的
,一个节点认为某个节点失联了并不代表所有的节点都
认为它失联了。所以集群还得经过一次协商的过程,
只有当大多数节点都认定了某个节点失
联了,集群才认为该节点需要进行主从切换来容错
。
Redis 集群节点采用 Gossip 协议来广播自己的状态以及自己对整个集群认知的改变
。比
如一个节点发现某个节点失联了
(PFail)
,它会将这条信息向整个集群广播,其它节点也就可
以收到这点失联信息。如果一个节点收到了某个节点失联的数量
(PFail Count)
已经达到了集
群的大多数,就可以标记该节点为确定下线状态
(Fail)
,然后向整个集群广播,强迫其它节
点也接收该节点已经下线的事实,并立即对该失联节点进行主从切换
Cluster
基本使用
Cluster
不支持事务,
Cluster
的
mget
方法相
比
Redis
要慢很多,被拆分成了多个
get
指令,
Cluster
的
rename
方法不再是原子的,它
需要将数据从原节点转移到目标节点
槽位迁移感知
第一个
moved
是用来纠正槽位的
第二个
asking
指令和
moved
不一样,它是用来临时纠正槽位的
集群
变更感知
当服务器节点变更时,客户端应该即时得到通知以实时刷新自己的节点关系表。那客户
端是如何得到通知的呢?这里要分
2
种情况:
目标节点挂掉了,客户端会抛出一个
ConnectionError
,紧接着会随机挑一个节点来
重试,这时被重试的节点会通过
moved error
告知目标槽位被分配到的新的节点地址。
运维手动修改了集群信息,将
master
切换到其它节点,并将旧的
master
移除集
群。这时打在旧节点上的指令会收到一个
ClusterDown
的错误,告知当前节点所在集群不可
用
(
当前节点已经被孤立了,它不再属于之前的集群
)
。这时客户端就会关闭所有的连接,清
空槽位映射关系表,然后向上层抛错。待下一条指令过来时,就会重新尝试初始化节点信
息
拓展
1
:
耳听八方
—— Stream
Redis5.0
最大的
新特性就是多出了一个数据结构
Stream
,它是一个新的强大的支持多播的可持久化的消息队
列,作者坦言
Redis Stream
狠狠地借鉴了
Kafka
的设计
消息
ID
消息
ID
的形式是
timestampInMillis-sequence
,例如
1527846880572-5
,它表示当前的消
息在毫米时间戳
1527846880572
时产生,并且是该毫秒内产生的第
5
条消息。消息
ID
可以
由服务器自动生成,也可以由客户端自己指定
增
删改查
1
、
xadd
追加消息
2
、
xdel
删除消息,这里的删除仅仅是设置了标志位,不影响消息总长度
3
、
xrange
获取消息列表,会自动过滤已经删除的消息
4
、
xlen
消息长度
5
、
del
删除
Stream
独立消
费
我们可以在不定义消费组的情况下进行
Stream
消息的独立消费,当
Stream
没有新消
息时,甚至可以阻塞等待。
Redis
设计了一个单独的消费指令
xread
,可以将
Stream
当成普
通的消息队列
(list)
来使用。使用
xread
时,我们可以完全忽略消费组
(Consumer Group)
的存在,就好比
Stream
就是一个普通的列表
(list)
。
# 从 Stream 头部读取两条消息
127.0.0.1:6379> xread count 2 streams codehole 0-0
1) 1) "codehole"
2) 1) 1) 1527851486781-0
2) 1) "name"
2) "laoqian"
3) "age"
4) "30"
2) 1) 1527851493405-0
2) 1) "name"
2) "yurui"
3) "age"
4) "29
客户端如果想要使用
xread
进行顺序消费,一定要记住当前消费到哪里了,也就是返回
的消息
ID
。下次继续调用
xread
时,将上次返回的最后一个消息
ID
作为参数传递进去,
就可以继续消费后续的消息
创
建消
费组
Stream
通过
xgroup create
指令创建消费组
(Consumer Group)
,需要传递起始消息
ID
参数用
来初始化
last_delivered_id
变量
消
费
Stream
提供了
xreadgroup
指令可以进行消费组的组内消费,需要提供消费组名称、消
费者名称和起始消息
ID
。它同
xread
一样,也可以阻塞等待新消息。读到新消息后,对应
的消息
ID
就会进入消费者的
PEL(
正在处理的消息
)
结构里,客户端处理完毕后使用
xack
指令通知服务器,本条消息已经处理完毕,该消息
ID
就会从
PEL
中移除
Stream
消息太多怎么
办
?
读者很容易想到,要是消息积累太多,
Stream
的链表岂不是很长,内容会不会爆掉
?xdel
指令又不会删除消息,它只是给消息做了个标志位。
Redis
自然考虑到了这一点,所以它提供了一个定长
Stream
功能。在
xadd
的指令提供
一个定长长度
maxlen
,就可以将老的消息干掉,确保最多不超过指定长度
小
结
Stream
的消费模型借鉴了
Kafka
的消费分组的概念,它弥补了
Redis Pub/Sub
不能持
久化消息的缺陷。但是它又不同于
kafka
,
Kafka
的消息可以分
partition
,而
Stream
不行。
如果非要分
parition
的话,得在客户端做,提供不同的
Stream
名称,对消息进行
hash
取
模来选择往哪个
Stream
里塞
拓展
2
:
无所不知
—— Info
指令
Info
指令显示的信息非常繁多,分为
9
大块,每个块都有非常多的参数,这
9
个块分
别是
:
1
、
Server
服
务器运行的环境参数
2
、
Clients
客
户端相关信息
3
、
Memory
服
务器运行内存统计数据
4
、
Persistence
持久化信息
5
、
Stats
通用
统计数据
6
、
Replication
主从复制相关信息
7
、
CPU CPU
使用情况
8
、
Cluster
集群信息
9
、
KeySpace
键值对统计数量信息
Redis
每秒
执行多少次指令
?
info stats |grep ops
info clients
这个信息也是比较有用的,通过观察这个数量可以确定是否存在意料之外的连接。如果
发现这个数量不对劲,接着就可以使用
client list
指令列出所有的客户端链接地址来确定源
头。
关于客户端的数量还有个重要的参数需要观察,那就是
rejected_connections
,它表示因
为超出最大连接数限制而被拒绝的客户端连接次数,如果这个数字很大,意味着服务器的最
大连接数设置的过低需要调整
maxclients
参数
Redis
内存占用多大
?
info memory | grep used | grep human
复制
积压缓冲区多大
?
info replication |grep backlog
拓展
3
:
拾
遗漏补
——
再
谈分布式锁
Sentinel
集群中,主节点挂掉时,从节点会取而代之,客户端上却并没有明显感
知。原先第一个客户端在主节点中申请成功了一把锁,但是这把锁还没有来得及同步到从节
点,主节点突然挂掉了。然后从节点变成了主节点,这个新的节点内部没有这个锁,所以当
另一个客户端过来请求加锁时,立即就批准了。这样就会导致系统中同样一把锁被两个客户
端同时持有,不安全性由此产生
不过
这种不安全也仅仅是在主从发生 failover 的情况下才会产生
,而且持续时间极短,
业务系统多数情况下可以容忍。
Redlock
算法
加锁时,它会向过半节点发送
set(key, value, nx=True, ex=xxx)
指令,只要过半节点
set
成功,那就认为加锁成功。释放锁时,需要向所有节点发送
del
指令。不过
Redlock
算法还
需要考虑出错重试、时钟漂移等很多细节问题,同时因为
Redlock
需要向多个节点进行读
写,意味着相比单实例
Redis
性能会下降一些
Redlock
使用
场景
如果你很在乎高可用性,希望挂了一台
redis
完全不受影响,那就应该考虑
redlock
。不
过代价也是有的,需要更多的
redis
实例,性能也下降了,代码上还需要引入额外的
library
,运维上也需要特殊对待,这些都是需要考虑的成本,使用前请再三斟酌
拓展
4
:
朝生暮死
——
过期策略
过期的
key
集合
redis
会将每个设置了过期时间的
key
放入到一个
独立的字典中
,以后会定时遍历这个
字典来删除到期的
key
。除了定时遍历之外,它还会使用惰性策略来删除过期的
key
,所谓
惰性策略就是在客户端访问这个
key
的时候,
redis
对
key
的过期时间进行检查,如果过期
了就立即删除。
定时删除是集中处理,惰性删除是零散处理
定
时扫描策略
Redis
默认会每秒进行十次过期扫描,过期扫描不会遍历过期字典中所有的
key
,而是
采用了一种简单的贪心策略。
1
、
从
过期字典中随机
20
个
key
;
2
、
删除这
20
个
key
中已
经过期的
key
;
3
、
如果
过期的
key
比率超
过
1/4
,
那就重复步
骤
1
;
同时,为了保证过期扫描不会出现循环过度,导致线程卡死现象,算法还增加了扫描时
间的上限,默认不会超过
25ms
。
如果有大批量的
key
过期,
要给过期时间设置 一个随机范围
,而不能全部在同一时间过期
从
库的过期策略
从库不会进行过期扫描,
从库对过期的处理是被动的
。主库在
key
到期时,会在
AOF
文件里增加一条
del
指令,同步到所有的从库,从库通过执行这条
del
指令来删除过期的
key
。
因为指令同步是异步进行的,所以主库过期的
key
的
del
指令没有及时同步到从库的
话,会出现主从数据的不一致,主库没有的数据在从库里还存在,比如上一节的集群环境
分
布式锁的算法漏洞就是因为这个同步延迟产生的
拓展
5
:
优胜劣汰
—— LRU
当
Redis
内存超出物理内存限制时,内存的数据会开始和磁盘产生频繁的
交换
(swap)
。
交换会让
Redis
的性能急剧下降,对于访问量比较频繁的
Redis
来说,这样龟速的存取效率
基本上等于不可用
当实际内存超出
maxmemory
时,
Redis
提供了几种可选策略
(maxmemory-policy)
来让
用户自己决定该如何腾出新的空间以继续提供读写服务。
noeviction
不会继续服务写请求
(DEL
请求可以继续服务
)
,读请求可以继续进行。这样
可以保证不会丢失数据,但是会让线上的业务不能持续进行。这是默认的淘汰策略。
volatile-lru
尝试淘汰设置了过期时间的
key
,最少使用的
key
优先被淘汰。没有设置过
期时间的
key
不会被淘汰,这样可以保证需要持久化的数据不会突然丢失。
volatile-ttl
跟上面一样,除了淘汰的策略不是
LRU
,而是
key
的剩余寿命
ttl
的值,
ttl
越小越优先被淘汰。
volatile-random
跟上面一样,不过淘汰的
key
是过期
key
集合中随机的
key
。
allkeys-lru
区别于
volatile-lru
,这个策略要淘汰的
key
对象是全体的
key
集合,而不
只是过期的
key
集合。这意味着没有设置过期时间的
key
也会被淘汰。
allkeys-random
跟上面一样,不过淘汰的策略是随机的
key
。
volatile-xxx
策略只会针对带过期时间的
key
进行淘汰,
allkeys-xxx
策略会对所有的
key
进行淘汰。如果你只是拿
Redis
做缓存,那应该使用
allkeys-xxx
,客户端写缓存时
不必携带过期时间。如果你还想同时使用
Redis
的持久化功能,那就使用
volatile-xxx
策略,这样可以保留没有设置过期时间的
key
,它们是永久的
key
不会被
LRU
算法淘
汰。
LRU
算法
实现
LRU
算法除了需要
key/value
字典外,还需要附加一个链表,链表中的元素按照
一定的顺序进行排列。当空间满的时候,会踢掉链表尾部的元素。当字典的某个元素被访问
时,它在链表中的位置会被移动到表头。所以链表的元素排列顺序就是元素最近被访问的时
间顺序。
位于链表尾部的元素就是不被重用的元素,所以会被踢掉。位于表头的元素就是最近刚
刚被人用过的元素,所以暂时不会被踢。
近似
LRU
算法
Redis
使用的是一种近似
LRU
算法,它跟
LRU
算法还不太一样。之所以不使用
LRU
算法,是因为需要消耗大量的额外的内存,需要对现有的数据结构进行较大的改造。近似
LRU
算法则很简单,在现有数据结构的基础上使用随机采样法来淘汰元素,能达到和
LRU
算法非常近似的效果。
Redis
为实现近似
LRU
算法,它给每个
key
增加了一个额外的小字
段,这个字段的长度是
24
个
bit
,也就是最后一次被访问的时间戳。
上一节提到处理
key
过期方式分为集中处理和懒惰处理,
LRU
淘汰不一样,它的处理
方式只有懒惰处理。当
Redis
执行写操作时,发现内存超出
maxmemory
,就会执行一次
LRU
淘汰算法。这个算法也很简单,就是随机采样出
5(
可以配置
)
个
key
,然后淘汰掉最
旧的
key
,如果淘汰后内存还是超出
maxmemory
,那就继续随机采样淘汰,直到内存低于
maxmemory
为止。
如何采样就是看
maxmemory-policy
的配置,如果是
allkeys
就是从所有的
key
字典中
随机,如果是
volatile
就从带过期时间的
key
字典中随机。每次采样多少个
key
看的是
maxmemory_samples
的配置,默认为
5
。
拓展
6
:
平波
缓进
——
懒惰删除
删除指令
del
会直接释放对象的内存,大部分情况下,这个指令非常快,没有明显延
迟。不过如果删除的
key
是一个非常大的对象,比如一个包含了千万元素的
hash
,那么删
除操作就会导致单线程卡顿。
Redis
为了解决这个卡顿问题,在
4.0
版本引入了
unlink
指令,它能对删除操作进行懒
处理,丢给后台线程来异步回收内存。
> unlink key
OK
flush
Redis
提供了
flushdb
和
flushall
指令,用来清空数据库,这也是极其缓慢的操作。
Redis 4.0
同样给这两个指令也带来了异步化,在指令后面增加
async
参数就可以将整棵大树
连根拔起,扔给后台线程慢慢焚烧。
> flushall async
OK
AOF Sync
也很慢
Redis
需要每秒一次
(
可配置
)
同步
AOF
日志到磁盘,确保消息尽量不丢失,需要调用
sync
函数,这个操作会比较耗时,会导致主线程的效率下降,所以
Redis
也将这个操作移到
异步线程来完成。执行
AOF Sync
操作的线程是一个独立的异步线程,和前面的懒惰删除线
程不是一个线程,同样它也有一个属于自己的任务队列,队列里只用来存放
AOF Sync
任
务。
更多异步
删除点
Redis
回收内存除了
del
指令和
flush
之外,还会存在于在
key
的过期、
LRU
淘汰、
rename
指令以及从库全量同步时接受完
rdb
文件后会立即进行的
flush
操作。
Redis4.0
为这些删除点也带来了异步删除机制,打开这些点需要额外的配置选项。
1
、
slave-lazy-flush
从
库接受完
rdb
文件后的
flush
操作
2
、
lazyfree-lazy-eviction
内存达到
maxmemory
时进行淘汰
3
、
lazyfree-lazy-expire key
过期删除
4
、
lazyfree-lazy-server-del rename
指令
删除
destKey
拓展
7
:
妙手仁心
——
优雅地使用
Jedis
Java
程序一般都是多线程的应用程序,意味着我们很少直接使用
Jedis
,而是要用到
Jedis
的连接池 ——
JedisPool
。同时因为
Jedis 对象不是线程安全的,当我们要使用 Jedis
对象时,需要从连接池中拿出一个 Jedis 对象独占,使用完毕后再将这个对象还给连接池
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
public class JedisTest {
public static void main(String[] args) {
JedisPool pool = new JedisPool();
try (Jedis jedis = pool.getResource()) { // 用完自动 close
doSomething(jedis);
}
}
private static void doSomething(Jedis jedis) {
// code it here
}
}
拓展
8
:
居安思危
——
保
护
Redis
指令安全
Redis
有一些非常危险的指令,这些指令会对
Redis
的稳定以及数据安全造成非常严重
的影响。比如
keys
指令会导致
Redis
卡顿,
flushdb
和
flushall
会让
Redis
的所有数据全
部清空
Redis
在配置文件中提供了
rename-command
指令用于将某些危险的指令修改成特别的
名称,用来避免人为误操作。比如在配置文件的
security
块增加下面的内容
:
rename-command keys abckeysabc
如果还想执行
keys
方法,那就不能直接敲
keys
命令了,而需要键入
abckeysabc
。 如
果想完全封杀某条指令,可以将指令
rename
成空串,就无法通过任何字符串指令来执行这
条指令了。
rename-command flushall
""
端口安全
Redis
默认会监听
*:6379
运维人员务必在
Redis
的配置文件中指定监听的
IP
地址,避免这样的惨剧发
生。更进一步,还可以增加
Redis
的密码访问限制,客户端必须使用
auth
指令传入正确的
密码才可以访问
Redis
Lua
脚本安全
开发者必须禁止
Lua
脚本由用户输入的内容
(UGC)
生成,这可能会被黑客利用以植入
恶意的攻击代码来得到
Redis
的主机权限。
同时,我们应该让
Redis
以普通用户的身份启动,这样即使存在恶意代码黑客也无法拿
到
root
权限
SSL
代理
Redis
并不支持
SSL
链接,意味着客户端和服务器之间交互的数据不应该直接暴露在公
网上传输,否则会有被窃听的风险。如果必须要用在公网上,可以考虑使用
SSL
代理。
SSL
代理比较常见的有
ssh
,不过
Redis
官方推荐使用
spiped
工具,可能是因为
spiped
的功能相对比较单一,使用也比较简单,易于理解。下面这张图是使用
spiped
对
ssh
通道进行二次加密
(
因为
ssh
通道也可能存在
bug)
拓展
9
:
隔
墙有耳
—— Redis
安全通信
应用部署在
A
机房,存储部署在
B
机房。如果使用普通
tcp
直接访问,因为跨机
房所以传输数据会暴露在公网,这非常不安全,客户端服务器交互的数据存在被窃听的风
险
Redis
本身并不支持
SSL
安全链接,不过有了
SSL
代理软件,我们可以让通信数据透
明地得到加密,就好像
Redis
穿上了一层隐身外套一样。
spiped
就是这样的一款
SSL
代理
软件,它是
Redis
官方推荐的代理软件
spiped
原理
让我们放大细节,仔细观察
spiped
实现原理。
spiped
会在客户端和服务器各启动一个
spiped
进程
每一个
spiped
进程都会有一个监听端口
(server socket)
用来接收数据,同时还会作为一
个客户端
(socket client)
将数据转发到目标地址。
spiped
进程需要成对出现,相互之间需要使用相同的共享密钥来加密消息
源
码
1
:
极度深寒
——
探索
「
字符串
」
内部
结构
Redis
的字符串叫着「
SDS
」,也就是
Simple Dynamic String
。它的结构是一个带长度信
息的字节数组
Redis
规定字符串的长度不得超过
512M
字节。创建字符串时
len
和
capacity
一样
长,不会多分配冗余空间,这是因为绝大多数场景下我们不会使用
append
操作来修改字符
串
embstr vs raw
Redis
的字符串有两种存储方式,在长度特别短时,使用
emb
形式存储
(embeded)
,当
长度超过
44
时,使用
raw
形式存储
扩容策略
字符串在长度小于
1M
之前,扩容空间采用加倍策略,也就是保留
100%
的冗余空
间。当长度超过
1M
之后,为了避免加倍后的冗余空间过大而导致浪费,每次扩容只会多分
配
1M
大小的冗余空间
源
码
2
:
极度深寒
——
探索
「
字典
」
内部
dict
是
Redis
服务器中出现最为频繁的复合型数据结构,除了
hash
结构的数据会用到
字典外,整个
Redis
数据库的所有
key
和
value
也组成了一个全局字典,还有带过期时间
的
key
集合也是一个字典。
zset
集合中存储
value
和
score
值的映射关系也是通过
dict
结
构实现的
渐进式
rehash
大字典的扩容是比较耗时间的,需要重新申请新的数组,然后将旧字典所有链表中的元
素重新挂接到新的数组下面,这是一个
O(n)
级别的操作,作为单线程的
Redis
表示很难承受
这样耗时的过程。步子迈大了会扯着蛋,所以
Redis
使用渐进式
rehash
小步搬迁。虽然慢一
点,但是肯定可以搬完
源
码
3
:
极度深寒
——
探索
「
压缩列表
」
内部
Redis
为了节约内存空间使用,
zset
和
hash
容器对象在元素个数较少的时候,采用压
缩列表
(ziplist)
进行存储。压缩列表是一块连续的内存空间,元素之间紧挨着存储,没有任
何冗余空隙。
增加元素
因为
ziplist
都是紧凑存储,没有冗余空间
(
对比一下
Redis
的字符串结构
)
。意味着插
入一个新的元素就需要调用
realloc
扩展内存。取决于内存分配器算法和当前的
ziplist
内存
大小,
realloc
可能会重新分配新的内存空间,并将之前的内容一次性拷贝到新的地址,也可
能在原有的地址上进行扩展,这时就不需要进行旧内容的内存拷贝。
如果
ziplist
占据内存太大,重新分配内存和拷贝内存就会有很大的消耗。所以
ziplist
不适合存储大型字符串,存储的元素也不宜过多
IntSet
小整数集合
当
set
集合容纳的元素都是整数并且元素个数较小时,
Redis
会使用
intset
来存储结合
元素。
intset
是紧凑的数组结构,同时支持
16
位、
32
位和
64
位整数
源
码
4
:
极度深寒
——
探索
「
快速列表
」
内部
Redis
早期版本存储
list
列表数据结构使用的是压缩列表
ziplist
和普通的双向链表
linkedlist
,也就是元素少时用
ziplist
,元素多时用
linkedlist
为了进一步节约空间,
Redis
还会对
ziplist
进行压缩存储,使用
LZF
算法压缩,可以选择压缩深度
源
码
5
:
极度深寒
——
探索
「
跳
跃列表
」
内部
结构
Redis
的
zset
是一个复合结构,一方面它需要一个
hash
结构来存储
value
和
score
的
对应关系,另一方面需要提供按照
score
来排序的功能,还需要能够指定
score
的范围来获
取
value
列表的功能,这就需要另外一个结构「跳跃列表」
zset
的内部实现是一个
hash
字典加一个跳跃列表
(skiplist)
。
hash
结构在讲字典结构时
已经详细分析过了,它很类似于
Java
语言中的
HashMap
结构
源
码
6
:
极度深寒
——
探索
「
紧凑列表
」
内部
Redis 5.0
又引入了一个新的数据结构
listpack
,它是对
ziplist
结构的改进,在存储空间
上会更加节省,而且结构上也比
ziplist
要精简
源
码
7
:
极度深寒
——
探索
「
基数
树
」
内部
Rax
是
Redis
内部比较特殊的一个数据结构,它是一个有序字典树
(
基数树
Radix
Tree)
,按照
key
的字典序排列,支持快速地定位、插入和删除操作。
Redis
五大基础数据结
构里面,能作为字典使用的有
hash
和
zset
。
hash
不具备排序功能,
zset
则是按照
score
进
行排序的。
rax
跟
zset
的不同在于它是按照
key
进行排序的
参考
资料