1. 发布订阅
1. 发布订阅模型
Redis的“SUBSCRIBE”命令可以让客户端订阅任意数量的频道, 每当有新信息发送到被订阅的频道时, 信息就会被发送给所有订阅指定频道的客户端。下面是“SUBSCRIBE”命令和“PUBLISH”命令的用法。
SUBSCRIBE channel [channel ...]
用于订阅给指定频道的信息。PUBLISH channel message
将信息 message 发送到指定的频道 channel。
作为示例,下图展示了频道chanel1,以及订阅这个频道的3个客户端:client2、client5、client1之间的关系。
当有新消息通过PUBLISH命令发送到频道channel1时,这个消息就会被发送给订阅它的3个客户端。
Redis发布订阅功能最明显的用法就是用作实时消息系统,比如普通的即时聊天,群聊等功能。群聊时所有成员都订阅(SUBSCRIBE )一个主题,然后成员发送群消息时实际上是向该主题发布(PUBLISH )一条消息,从而实现通知。
2. 发布订阅机制的实现
每个Redis服务器进程都维持着一个表示服务器状态的redis.h/redisServer结构,结构的pubsub_channels属性是一个字典,这个字典就用于保存订阅频道的信息:
struct redisServer {
// ...
dict *pubsub_channels;
// ...
}
其中,字典的键为正在被订阅的频道,而字典的值则是一个链表,链表中保存了所有订阅这个频道的客户端。比如说,在下图展示的这个pubsub_channels示例中,client2、client5和client1就订阅了channel1,而其他频道也分别被别的客户端所订阅:
当客户端调用SUBSCRIBE命令时,程序就将客户端和要订阅的频道在pubsub_channels将变成下面这个样子:
了解了pubsub_channels字典的结构之后,解释PUBLISH命令的实现就非常简单了:当调用PUBLISH channel message命令,程序首先根据channel定位到字典的键,然后将信息发送给字典值链表中的所有客户端。
比如说,对于以下这个pubsub_channels实例,如果某个客户端执行命令PUBLISH channel1 “hello moto”,那么client2、client5和client1三个客户端都将接收到”hello moto”信息。
PUBLISH命令的实现可以用以下伪代码来描述:
def PUBLISH(channel,message):
# 遍历所有订阅频道channel的客户端
for client in server.pubsub_channels[channel]:
# 将信息发送给它们
send_message(client,message)
3. 与ActiveMQ的比较
- ActiveMQ支持多种消息协议,包括AMQP,MQTT,Stomp等,并且支持JMS规范,但Redis没有提供对这些协议的支持。
- ActiveMQ提供持久化功能,但Redis无法对消息持久化存储,一旦消息被发送,如果没有订阅者接收,那么消息就会丢失。
- ActiveMQ提供了消息传输保障,当客户端连接超时或事务回滚等情况发生时,消息会被重新发送给客户端,Redis没有提供消息传输保障。
2. Redis事务
Redis事务可以一次执行多个命令, 并且带有以下三个重要的保证:
- 批量操作在发送 EXEC 命令前被放入队列缓存。
- 收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行。
- 在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。
一个事务从开始到执行会经历以下三个阶段:
- 开始事务。
- 命令入队。
- 执行事务。
下面是一个例子:
redis 127.0.0.1:7000> multi
OK
redis 127.0.0.1:7000> set a aaa
QUEUED
redis 127.0.0.1:7000> set b bbb
QUEUED
redis 127.0.0.1:7000> set c ccc
QUEUED
redis 127.0.0.1:7000> exec
1) OK
2) OK
3) OK
单个Redis命令的执行是原子性的,但Redis没有在事务上增加任何维持原子性的机制,所以Redis事务的执行并不是原子性的。
事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。
3. Redis持久化
Redis持久化的目的是为了灾难恢复。Redis主要支持两种方式进行持久化:RDB和AOF:
- RDB:全量备份。是Redis默认的数据备份方式。它会周期性的生成一个数据快照,包含了某个时间点Redis内存中的所有数据,并保存在一个名为dump.rdb的文件中。Redis重启时会将dump.rdb中的数据加载到内存中进行数据恢复。
- AOF:增量备份。将每条写入命令以append-only的方式写入日志。在Redis重启时,可以通过回放AOF中所有写入指令来重新构建整个Redis集群。
Redis需要执行RDB备份的时候服务器会执行以下操作:
- redis调用系统的fork()函数创建一个子进程。
- 子进程将数据集写入一个临时的RDB文件。
- 当子进程完成对临时的RDB文件的写入时,Redis使用原子性的rename系统调用将临时文件重命名为dump.rdb文件。这样在任何时候出现故障,Redis的RDB文件都总是可用的。
同时,Redis的RDB文件也是Redis主从同步内部实现中的一环。第一次Slave向Master请求同步的流程是: Slave首次成为Master的从节点后,会向Master发出全量同步请求,Master先dump出rdb文件,然后将rdb文件全量传输给slave,然后Master把缓存的命令转发给Slave,初次同步完成。
之后每一个写操作,Master都会同步给Slave。除此以外,Slave会定时向Master发送REPLCONF ACK {offset}
命令,其中offset指从节点保存的复制偏移量。REPLCONF ACK命令的作用包括:检测主从服务器的网络连接状态以及Master判断数据丢失(主节点会与自己的offset对比,如果从节点数据缺失,主节点会推送缺失的数据)。
RDB模式优缺点:
- RDB模式对Redis对外提供的读写服务性能影响很小,因为Redis主进程进行RDB备份时,会fork一个子进程来进行磁盘IO操作。
- 相对与AOF模式,使用rdb文件进行数据恢复的速度更快。
- RDB模式由于是周期性的进行数据快照,因此会存在数据丢失的问题。
在Redis配置文件中,通过配置以下内容开启AOF模式:
将
appendonly
设置从“no
”修改为“yes
”。appendonly yes
指定本地AOF数据文件名,默认值为
appendonly.aof。
appendfilename "appendonly.aof"
指定更新日志条件。
appendfsync everysec
- always:同步持久化。每次发生数据变化会立刻写入到磁盘中。性能较差但数据不会丢失。
- everysec:出厂默认设置,每秒异步记录一次(默认值)。
- no:不同步。
AOF是存放每条写命令的,所以会不断的增大。当大到一定程度时,AOF会做rewrite操作,rewrite操作就是基于当时Redis的数据重新构造一个小的AOF文件,然后将大的AOF文件删除。
AOF模式的优缺点:
- AOF模式能够防止数据丢失,一般AOF会每隔1秒,通过一个后台线程执行一次fsync操作,最多丢失1秒钟的数据。
- AOF日志文件以append-only模式写入,所以没有任何磁盘寻址的开销,写入性能非常高,而且文件不容易破损,即使文件尾部破损,也很容易修复。
- AOF日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。因为在rewrite log的时候,会对其中的指令进行压缩,创建出一份需要恢复数据的最小日志出来。在创建新日志文件的时候,老的日志文件还是照常写入。当新的merge后的日志文件ready的时候,再交换新老日志文件即可。
- 对于同一份数据来说,AOF日志文件通常比RDB数据快照文件更大。
AOF开启后,写QPS会比RDB支持的写QPS低,因为AOF一般会配置成每秒fsync一次日志文件。当然,每秒一次fsync,性能也还是很高的。(如果实时写入,那么QPS会大降,Redis性能会大大降低)
4. Redis过期策略
Redis过期策略主要包括两部分:定期删除和惰性删除。
定期删除指的是,Redis默认每隔100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。
由于定期删除策略只是随机挑选一部分key进行检测,因此可能存在大量key过期了仍然没有删除,此时就需要惰性删除机制了。
惰性删除指的是,Redis在返回某个key值时,会检测这个key是否过期,如果过期了就执行删除操作,然后返回空。
此时,仍然可能存在大量key定期删除策略和惰性删除策略都没有覆盖到,此时内存耗尽了,那么Redis就会启用内存淘汰机制。
内存淘汰机制指的是,当内存空间不足时,Redis淘汰key的策略。主要包括以下几种:noeviction: 当内存不足以容纳新写入数据时,新的写入操作直接报错。
- allkeys-lru:**当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。**
- allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 key。
- volatile-lru:**当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。**
- volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
- volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。
5. 分布式锁
https://www.shiyanlou.com/library/advanced-java/docs/distributed-system/distributed-lock-redis-vs-zookeeper6. 缓存雪崩,缓存穿透和缓存击穿
https://www.shiyanlou.com/library/advanced-java/docs/high-concurrency/redis-caching-avalanche-and-caching-penetration7. Redis实现乐观锁
https://my.oschina.net/itommy/blog/17906418. 一致性哈希
https://www.yuque.com/polaris-docs/bigdata/hash参考
博文:Redis 设计与实现(第一版)
https://redisbook.readthedocs.io/en/latest/index.html
https://redisbook.readthedocs.io/en/latest/feature/pubsub.html
CSDN:Redis发布订阅和应用场景
https://blog.csdn.net/fly910905/article/details/78495971
编程迷思:深入学习Redis(3):主从复制
https://www.cnblogs.com/kismetv/p/9236731.html
蓝桥&实验楼:Redis面试题(Redis持久化方式有哪些?)
https://www.shiyanlou.com/library/advanced-java/docs/high-concurrency/redis-persistence