前言
笔记内容涉及:①Redis优秀的性能;②过期键失效策略;③持久化;④分布式锁;⑤高可用模型;⑥事务。
Redis优秀的性能
Redis在高性能方面做了很多的优化工作,主要有以下几个方面:
基于内存+单线程模型
完全基于内存操作的特性使得Redis从根本上会比基于磁盘的数据库要快。通常情况下CPU不会成为Redis的瓶颈,而Redis最有可能的瓶颈是机器的内存大小或者网络带宽。既然CPU不会成为瓶颈,所以也就很顺理成章地采用了单线程方案。此外,单线程也避免了上下文的切换、资源加锁等带来的额外的性能消耗。
I/O多路复用模型
Redis采用了 epoll + 自己实现的简单事件框架 并发处理连接,以此提高在IO方面的性能。
多路即为多个socket连接,复用则为复用一个线程。基于epoll实现的多路复用模型,其原理为让内核不再监控应用程序本身(socket),而是监控应用程序的文件描述符(fd,读、写、连接、关闭等操作)。即内核会一直监听 socket 上的连接请求或者数据请求,一旦有请求到达就交给 Redis 线程处理。
注:关于I/O模型分类可以自行学习
多种数据结构+根据实际类型选择合适的数据编码
Redis提供了丰富的数据类型,其中常用的5种数据类型及应用场景为:
-
String:缓存、计数器、分布式锁等
-
List:链表、队列、时间轴等
-
Hash:用户信息,哈希表等
-
set:去重、赞、踩、共同好友等
-
zset:排行榜等
针对如上5种数据结构,每种又根据实际数据内容,选用不同的数据结构。具体总结体现如下图:
过期键失效策略
针对过期键的处理,Redis也是十分小心。考虑一个问题,如果同一时间有过多的key失效,而对于单线程的Redis来说,势必会造成卡顿。
过期的key集合+过期策略
首先,设置了过期时间的key放到了一个独立的字典中,所以失效动作只需要扫描这个字典。过期键的失效策略大致分为:定时删除、惰性删除、定期删除,目前Redis采用惰性删除和定期删除策略。
定时删除:在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作。对内存最友好,对 CPU 时间最不友好。
惰性删除:放任键过期不管,但是每次获取键时,都检査键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。对 CPU 时间最优化,对内存最不友好。
定期删除:Redis默认会进行每秒10次(100ms/次)过期扫描,过期扫描并不会遍历过期字典中的所有的key,而是采用了一种简单的贪心策略。
-
从过期字典中随机20个key;
-
删除这20个键中已过期的key;
-
如果过期的key比率超过1/4,则重复步骤1;
同时,为了保证过期扫描不会出现循环过度,导致线程卡死现象,算法还增加了扫描时间的上限,默认不会超过25ms。
持久化
RDB
命令:
save 或者 bgsave。save会阻塞主进程,不建议直接使用该命令;bgsave通过fork子进程来生成RDB文件,不会直接阻塞主进程,但是如果redis占用的内存很大,那么在fork子进程时,则会出现明显的停顿现象。另外需要注意的是,已过期的键不会被保存到新创建的RDB文件中。
开启:
save
save 900 1 #900秒内有1个key发生了变化,则触发保存RDB文件
save 300 10 #300秒内有10个key发生了变化,则触发保存RDB文件
save 60 10000 #60秒内有10000个key发生了变化,则触发保存RDB文件
关闭:
save "" #关闭RDB持久化
优点:
-
RDB文件为二进制文件,所以相对占用空间较小。
-
适合定期备份和灾难恢复
-
写时复制技术(COW - copy-on-write),处理在持久化过程中的写命令,即针对get操作,主进程和子进程操作的都是同一份主内存中的数据,而针对set操作,主线程会在操作完主内存数据之后,在共享内存中复制出一份副本(无需等待子进程同步),然后fork子进程可以在执行该数据的读取动作时使用。
缺点:
-
全量数据的快照耗时过长,因此不能频繁执行。通常情况下会设置至少5分钟保存一次快照,如果出现宕机情况,则意味着最多可能丢失5分钟数据。
-
耗时动作主要体现在两个方面:①RDB文件写磁盘;②fork动作的过程会阻塞主进程(大约10ms per GB)。
-
极端情况下,如果所有的页都需要修改,COW的方式会导致内存占用变为原来的2倍。
AOF
开启:
AOF 持久化默认是关闭的,可以通过配置:appendonly yes 开启。
关闭:
使用配置 appendonly no 可以关闭 AOF 持久化。
持久化功能实现步骤:
分为三个步骤:命令追加、文件写入、文件同步
命令追加:
当AOF持久化功能打开时,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾。
文件写入:
Linux 操作系统中为了提升性能,使用了页缓存(page cache)。当我们将 aof_buf 的内容写到磁盘上时,此时数据并没有真正的落盘,而是在 page cache 中。
文件同步(刷盘):
需要执行 fsync / fdatasync 命令来强制刷盘,将 page cache 中的数据真正落盘。Redis根据时间事件serverCron触发,通过配置appendfsync来控制。
参数选择:
-
always:每处理一个命令都将 aof_buf 缓冲区中的所有内容写入并同步到AOF 文件,即每个命令都刷盘。
-
everysec:将 aof_buf 缓冲区中的所有内容写入到 AOF 文件,如果上次同步 AOF 文件的时间距离现在超过一秒钟, 那么再次对 AOF 文件进行同步, 并且这个同步操作是异步的,由一个后台线程专门负责执行,即每秒刷盘1次。
-
no:将 aof_buf 缓冲区中的所有内容写入到 AOF 文件, 但并不对 AOF 文件进行同步, 何时同步由操作系统来决定。即不执行刷盘,让操作系统自己执行刷盘。
重写:
AOF 重写由两个参数共同控制,auto-aof-rewrite-percentage 和 auto-aof-rewrite-min-size,同时满足这两个条件,则触发 AOF 后台重写 BGREWRITEAOF。
# 当前AOF文件比上次重写后的AOF文件大小的增长比例超过100
auto-aof-rewrite-percentage 100
# 当前AOF文件的文件大小大于64MB
auto-aof-rewrite-min-size 64mb
优点:
-
AOF 比 RDB可靠。你可以设置不同的 fsync 策略:no、everysec 和 always。默认是 everysec,发生宕机最多丢失1秒数据。
-
易读。AOF 文件有序地保存了对数据库执行的所有写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂(如set mykey hello, 将持久化成*3 $3 set $5 mykey $5 hello, 第一个数字代表这条语句有多少元,其他的数字代表后面字符串的长度。这样的设计,使得即使在写文件过程中突然关机导致文件不完整,也能自我修复,执行redis-check-aof即可)。
-
当 AOF文件太大时,Redis 会自动在后台进行重写:重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。
缺点:
-
文件相对于RBD文件更大。
-
认每秒刷盘的情况下,AOF的速度可能会比RDB慢。
混合持久化
混合持久化并不是一种全新的持久化方式,而是对已有方式的优化。混合持久化只发生于 AOF 重写过程。使用了混合持久化,重写后的新 AOF 文件前半段是 RDB 格式的全量数据,后半段是 AOF 格式的增量数据。
开启:
aof-use-rdb-preamble yes # 配置即可开启混合持久化
关闭:
aof-use-rdb-preamble no # 配置即可关闭混合持久化
分布式锁
加锁与解锁
加锁:
set key value PX milliseconds NX
解锁:
解锁需要两步操作:
查询当前“锁”是否还是我们持有,因为存在过期时间,所以可能等你想解锁的时候,“锁”已经到期,然后被其他线程获取了,所以我们在解锁前需要先判断自己是否还持有“锁”
如果“锁”还是我们持有,则执行解锁操作,也就是删除该键值对,并返回成功;否则,直接返回失败。
通常是使用 Lua 脚本来执行解锁操作,Redis 会保证脚本里的内容执行是一个原子操作。
if redis.call("get",KEYS[1]) == ARGV[1]
then
return redis.call("del",KEYS[1])
else
return 0
end
锁过期处理策略
通常情况下,设置过期时间要结合业务场景去考虑,尽量设置一个比较合理的值,就是理论上正常处理的话,在这个过期时间内一定能处理完毕。
在上述前提下,考虑锁过期的处理方案,通常有两种:
守护线程:
额外起一个线程,定期检查线程是否还持有锁,如果有则延长过期时间。Redisson 里面就实现了这个方案,使用“看门狗”定期检查(每1/3的锁时间检查1次),如果线程还持有锁,则刷新过期时间。
超时回滚:
当我们解锁时发现锁已经被其他线程获取了,说明此时我们执行的操作已经是“不安全”的了,此时需要进行回滚,并返回失败
RedLock
考虑上述守护线程方案在极端情况下的问题。
master节点成功获锁之后突然宕机,此时的slave并未同步到该key,触发故障转移后,其中的一个slave升级成为新的master,此时另一个线程在新的master上也成功获锁。此时也就意味着两个线程获取到了同一把锁。
RedLock
放弃主从架构,而选择多实例相互独立架构。
假设我们有 N 个 Redis 主节点,例如 N = 5,这些节点是完全独立的,我们不使用复制或任何其他隐式协调系统,为了取到锁,客户端应该执行以下操作:
- 获取当前时间,以毫秒为单位
- 依次尝试从5个实例,使用相同的 key 和随机值(例如UUID)获取锁。当向Redis
请求获取锁时,客户端应该设置一个超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在 5-50
毫秒之间。 - 客户端通过当前时间减去步骤1记录的时间来计算获取锁使用的时间。当且仅当从大多数(N/2+1,这里是3个节点)的Redis节点都取到锁,并且获取锁使用的时间小于锁失效时间时,锁才算获取成功。
- 如果取到了锁,其真正有效时间等于初始有效时间减去获取锁所使用的时间(步骤3计算的结果)。
- 如果由于某些原因未能获得锁(无法在至少N/2+1个Redis实例获取锁、或获取锁的时间超过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)。
可以看出,该方案为了解决数据不一致的问题,直接舍弃了异步复制,只使用 master 节点,同时由于舍弃了 slave,为了保证可用性,引入了 N 个节点,官方建议是 5。
缺点:
-
严重依赖机器时钟,确保N个单例机器始终一致。
-
5个实例,线程1在其中3个获锁,此时这3个中的一个突然宕机重启,此时线程2又在空闲的3台机器上获锁成功。
-
成本较大。
高可用模型
目前,云服务主要提供两种模式的Redis集群:一主多从(Master-Slave)集群、Cluster分片集群。其中,一主多从(Master-Slave)集群内存大小20G以内,适用于数据量不大、流量较高、读多写少的场景,当读流量非常高时,可以通过扩容从库分担读压力。Cluster分片集群由多个一主一从的分片组成,每个分片内存大小20G以内,集群总内存等于各个分片内存相加,适用于数据量较大、流量较高、写多读少(或读写相当)的场景,当内存不够用时,可以扩容分片数量。同集群的所有实例分布在不同机器上。
主从模式
通常一主多从(Master-Slave)集群包含的实例有1个Master、1个HA_Slave(与Master同机房,本质也是Slave,作为Master的灾备)、若干个Slave(每机房不少于2个)。
开启主从复制
在 slave 直接执行命令:slaveof
在 slave 配置文件中加入:slaveof
使用启动命令:–slaveof
注:在 Redis 5.0 之后,slaveof 相关命令和配置已经被替换成 replicaof,为了兼容旧版本,通过配置的方式仍然支持 slaveof,但是通过命令的方式则不行了。
当执行slaceof之后,slave会清掉自己所有的数据,执行一次全同步:master通过bgsave出自己的一个RDB文件,发送给slave。然后再增量同步:master作为一个普通的client连入slave,将所有的写操作转发给slave。
哨兵模式
在主从的基础上提供自动Fail Over的支持。
心跳检测:
每秒对所有的master、slave和其他的sentinel执行ping,被ping节点需要响应+PONG或者-LOADING或-MASTERDOWN.
主观下线:
master如果30s(可配置:down-after-miliseconds)内没有收到正确的应答,则会认为其为主观下线的状态(flag:SRI_S_DOWN)。
客观下线:
标记主观下线的同时会向其他的sentinel节点询问当前master节点的状态(SENTINEL is-master-down-by-addr),如果quonum(默认2)台在5秒内同样认为它下线了,则标记为客观下线状态(flag:SRI_O_DOWN)
FAIL OVER选主:
在失效主服务器属下的从服务器当中, 那些被标记为主观下线、已断线、或者最后一次回复 PING 命令的时间大于五秒钟的从服务器都会被淘汰。
在失效主服务器属下的从服务器当中, 那些与失效主服务器连接断开的时长超过 down-after 选项指定的时长十倍的从服务器都会被淘汰。
在经历了以上两轮淘汰之后剩下来的从服务器中, 我们选出复制偏移量(replication offset)最大的那个从服务器作为新的主服务器; 如果复制偏移量不可用, 或者从服务器的复制偏移量相同, 那么带有最小运行 ID 的那个从服务器成为新的主服务器。
推荐阅读:Redis命令参考-sentinel相关
集群模式
主要是针对水平扩展进行优化。
关键内容:
-
16384个槽(slot),按master平分。
-
使用CRC16算法计算key所属槽:CRC16(key,keylen) & 16383。
-
一个Cluster分片集群至少由3个分片组成,每个分片一主一从(一Master一HA_slave)。
-
理论上客户端只需配置一个master节点即可访问整个集群。通常情况下为了保证HA,需要客户端配置所有master节点。Master承担读写流量,HA_slave仅作为Master的备库不提供访问。
-
理论上为了保证容灾性,需要将主从交叉部署在不同机房。
事务
MULTI、 EXEC、 DISCARD 和 WATCH 是 Redis 事务的基础。
-
事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
-
事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。
MULTI 命令用于开启一个事务,MULTI 执行之后, 客户端可以继续向服务器发送任意多条命令(返回QUEUED), 这些命令不会立即被执行, 而是被放到一个队列中, 当 EXEC 命令被调用时, 所有队列中的命令才会被执行(返回一个数组,包含每条命令执行的结果)。同时也可以调用DISCARD,客户端可以清空事务队列,并放弃执行事务。
开启事务后,如果存在错误命令(非执行错误,即入队失败),服务器端会主动放弃当前事务。
EXEC执行后,如果当前队列中某条或者某些语句执行失败了(执行失败),事务中的其他不会受到影响。即Redis不会停止执行事务中的命令
WATCH 命令可以为 Redis 事务提供 check-and-set (CAS)行为(即:WATCH 使得 EXEC 命令需要有条件地执行,事务只能在所有被监视键都没有被修改的前提下执行, 如果这个前提不能满足的话,事务就不会被执行。)。如果存在(至少一条)被监视的键被改动过,则整个事务将会被取消,此时执行EXEC命令后, 将会返回空多条批量回复(null multi-bulk reply)来表示事务已经失败。
CAS示例(此时如果其他的客户端在WATCH之后,EXEC之前修改了myKey,则当前客户端事务就会失败):
WATCH mykey
val = GET mykey
val = val + 1
MULTI
SET mykey $val
EXEC
可以通过UNWATCH取消监控。
推荐阅读:Redis事务相关