1. 发布订阅

1. 发布订阅模型

Redis的“SUBSCRIBE”命令可以让客户端订阅任意数量的频道, 每当有新信息发送到被订阅的频道时, 信息就会被发送给所有订阅指定频道的客户端。下面是“SUBSCRIBE”命令和“PUBLISH”命令的用法。

  • SUBSCRIBE channel [channel ...]用于订阅给指定频道的信息。
  • PUBLISH channel message将信息 message 发送到指定的频道 channel。

作为示例,下图展示了频道chanel1,以及订阅这个频道的3个客户端:client2、client5、client1之间的关系。
image.png
当有新消息通过PUBLISH命令发送到频道channel1时,这个消息就会被发送给订阅它的3个客户端。
image.png
Redis发布订阅功能最明显的用法就是用作实时消息系统,比如普通的即时聊天,群聊等功能。群聊时所有成员都订阅(SUBSCRIBE )一个主题,然后成员发送群消息时实际上是向该主题发布(PUBLISH )一条消息,从而实现通知。

2. 发布订阅机制的实现

每个Redis服务器进程都维持着一个表示服务器状态的redis.h/redisServer结构,结构的pubsub_channels属性是一个字典,这个字典就用于保存订阅频道的信息:

  1. struct redisServer {
  2. // ...
  3. dict *pubsub_channels;
  4. // ...
  5. }

其中,字典的键为正在被订阅的频道,而字典的值则是一个链表,链表中保存了所有订阅这个频道的客户端。比如说,在下图展示的这个pubsub_channels示例中,client2、client5和client1就订阅了channel1,而其他频道也分别被别的客户端所订阅:
image.png
当客户端调用SUBSCRIBE命令时,程序就将客户端和要订阅的频道在pubsub_channels将变成下面这个样子:
image.png
了解了pubsub_channels字典的结构之后,解释PUBLISH命令的实现就非常简单了:当调用PUBLISH channel message命令,程序首先根据channel定位到字典的键,然后将信息发送给字典值链表中的所有客户端。
比如说,对于以下这个pubsub_channels实例,如果某个客户端执行命令PUBLISH channel1 “hello moto”,那么client2、client5和client1三个客户端都将接收到”hello moto”信息。
image.png
PUBLISH命令的实现可以用以下伪代码来描述:

  1. def PUBLISH(channel,message):
  2. # 遍历所有订阅频道channel的客户端
  3. for client in server.pubsub_channels[channel]:
  4. # 将信息发送给它们
  5. send_message(client,message)

3. 与ActiveMQ的比较

  1. ActiveMQ支持多种消息协议,包括AMQP,MQTT,Stomp等,并且支持JMS规范,但Redis没有提供对这些协议的支持
  2. ActiveMQ提供持久化功能,但Redis无法对消息持久化存储,一旦消息被发送,如果没有订阅者接收,那么消息就会丢失。
  3. ActiveMQ提供了消息传输保障,当客户端连接超时或事务回滚等情况发生时,消息会被重新发送给客户端,Redis没有提供消息传输保障

    2. Redis事务

    Redis事务可以一次执行多个命令, 并且带有以下三个重要的保证:
  • 批量操作在发送 EXEC 命令前被放入队列缓存。
  • 收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行。
  • 在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。

一个事务从开始到执行会经历以下三个阶段:

  • 开始事务。
  • 命令入队。
  • 执行事务。

下面是一个例子:

  1. redis 127.0.0.1:7000> multi
  2. OK
  3. redis 127.0.0.1:7000> set a aaa
  4. QUEUED
  5. redis 127.0.0.1:7000> set b bbb
  6. QUEUED
  7. redis 127.0.0.1:7000> set c ccc
  8. QUEUED
  9. redis 127.0.0.1:7000> exec
  10. 1) OK
  11. 2) OK
  12. 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备份的时候服务器会执行以下操作:

  1. redis调用系统的fork()函数创建一个子进程。
  2. 子进程将数据集写入一个临时的RDB文件。
  3. 当子进程完成对临时的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模式:

  1. appendonly设置从“no”修改为“yes”。

    1. appendonly yes
  2. 指定本地AOF数据文件名,默认值为appendonly.aof。

    1. appendfilename "appendonly.aof"
  3. 指定更新日志条件。

    1. 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-zookeeper

    6. 缓存雪崩,缓存穿透和缓存击穿

    https://www.shiyanlou.com/library/advanced-java/docs/high-concurrency/redis-caching-avalanche-and-caching-penetration

    7. Redis实现乐观锁

    https://my.oschina.net/itommy/blog/1790641

    8. 一致性哈希

    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