首页 > 代码库 > 架构设计:系统存储(16)——Redis事件订阅和持久化存储

架构设计:系统存储(16)——Redis事件订阅和持久化存储

接上文《架构设计:系统存储(15)——Redis基本概念和安装使用》

3-4、事件功能和配置项

Redis从2.X版本开始,就支持一种基于非持久化消息的、使用发布/订阅模式实现的事件通知机制。所谓基于非连接保持,是因为一旦消息订阅者由于各种异常情况而被迫断开连接,在其重新连接后,其离线期间的事件是无法被重新通知的(一些Redis资料中也称为即发即弃)。而其使用的发布/订阅模式,意味着其机制并不是由订阅者周期性的从Redis服务拉取事件通知,而是由Redis服务主动推送事件通知到符合条件的若干订阅者。

Redis中的事件功能可以提供两种不同的功能。一类是基于Channel的消息事件,这一类消息和Redis中存储的Keys没有太多关联,也就是说即使不在Redis中存储任何Keys信息,这类消息事件也可以独立使用。另一类消息事件可以对(也可以不对)Redis中存储的Keys信息的变化事件进行通知,可以用来向订阅者通知Redis中符合订阅条件的Keys的各种事件。Redis服务的事件功能在实际场景中虽然使用得不多,不过还是可以找到案例,例如服务治理框架DUBBO默认情况下使用Zookeeper作为各节点的服务协调装置,但可以通过更改DUBBO的配置,将Zookeeper更换为Redis。

3-4-1、publish和subscribe

我们先从比较简单的publish命令和subscribe命令开始介绍,因为这组命令所涉及到的Channel(通道)和Redis中存储的数据相对独立。publish命令由发送者使用,负责向指定的Channel发送消息;subscribe命令由订阅者使用,负责从指定的一个或者多个Channel中获取消息。

技术分享

以下是publish命令和subscribe命令的使用示例:

// 该命令向指定的channel名字发送一条消息(字符串)
PUBLISH channel message 
// 例如:向名叫FM955的频道发送一条消息,消息信息为“hello!”
PUBLISH FM955  "hello!"
// 再例如:向名叫FM900的频道发送一条消息,消息信息为“ doit!”
PUBLISH FM900 "doit!"
// 该命令可以开始向指定的一个或者多个channel订阅消息
SUBSCRIBE channel [channel ...]
// 例如:向名叫FM955的频道订阅消息
SUBSCRIBE FM955
// 再例如:向名叫FM955、FM900的两个频道订阅消息
SUBSCRIBE FM955 FM900

如果您使用需要使用publish命令和subscribe命令,您并不需要对Redis服务的配置信息做任何更改。以下示例将向读者展示两个命令的简单使用方式——前提是您的Redis服务已经启动好了:

  • 由客户端A充当订阅者,在ChannelA和ChannelB两个通道上订阅消息
-- 我们使用的Redis服务地址为192.168.61.140,端口为默认值
[root@kp2 ~]# redis-cli -h 192.168.61.140
192.168.61.140:6379> SUBSCRIBE ChannelA ChannelB
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "ChannelA"
3) (integer) 1
1) "subscribe"
2) "ChannelB"
3) (integer) 2
  • 有客户端B从当订阅者,通过ChannelB发送消息给所有订阅者。
-- 连接到Redis服务器后,直接运行PUBLISH命令,发送信息
[root@kp1 ~]# redis-cli -h 192.168.61.140
192.168.61.140:6379> PUBLISH ChannelB "hello"
(integer) 1
  • 以下是订阅者客户端A所受到的message信息:
......
-- 这时订阅者收到消息如下:
1) "message"
2) "ChannelB"
3) "hello"

从以上示例中可以看到,客户端A确实收到了客户端B所发送的消息信息,并且收到三行信息。这三行信息分别表示消息类型、消息通道和消息内容。注意,以上介绍的这组publish命令和subscribe命令的操作过程并没有对Redis服务中已存储的任何Keys信息产生影响

3-4-2、模式订阅psubscribe

Redis中还支持一种模式订阅,它主要依靠psubscribe命令向技术人员提供订阅功能。模式订阅psubscribe最大的特点是,它除了可以通过Channel订阅消息以外,还可以配合配置命令来进行Keys信息变化的事件通知。

模式订阅psubscribe的Channel订阅和subscribe命令类似,这里给出一个命令格式,就不再多做介绍了(可参考上文对subscribe命令的介绍):

// 该命令可以开始向指定的一个或者多个channel订阅消息
// 具体使用示例可参见SUBSCRIBE命令
PSUBSCRIBE channel [channel ...]

模式订阅psubscribe对Keys变化事件的支持分为两种类型:keyspace(键空间通知)和keyevent(键事件通知),这两类事件都是依靠Key的变化触发的,而关键的区别在于事件描述的焦点,举例说明:

当Redis服务中0号数据库的MyKey键被删除时,键空间和键事件向模式订阅者分别发送的消息格式如下:

// 以下命令可订阅键空间通知
// 订阅0号数据库任何Key信息的变化
192.168.61.140:6379> psubscribe __keyspace@0__:*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "__keyspace@0__:*"
3) (integer) 1
// 出现以上信息,说明订阅成功
// 当其他客户端执行 set mykey 123456 时,该订阅可收到以下信息
1) "pmessage"
2) "__keyspace@0__:*"
3) "__keyspace@0__:mykey"
4) "set"

以上收到的订阅信息,其描述可以概括为:“mykey的键空间发生了事件,事件为set”。这样的事件描述着重于key的名称,并且告诉客户端key的事件为set。我们再来看看订阅键事件通知时,发生同样事件所得到的订阅信息:

// 以下命令可订阅键事件通知
// 订阅0号数据库任何事件的变化
192.168.61.140:6379> psubscribe __keyevent@0__:*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "__keyevent@0__:*"
3) (integer) 1
// 出现以上信息,说明订阅成功
// 当其他客户端执行 set mykey 123456 时,该订阅可收到以下信息
1) "pmessage"
2) "__keyevent@0__:*"
3) "__keyevent@0__:set"
4) "mykey"

以上收到的订阅信息中事件是主体,其信息可以概括为:“0号数据库发生了set事件,发生这个事件的key信息为mykey”。

3-4-3、模式订阅的配置

a、配置和通配符

使用psubscribe命令进行键事件的订阅,就首先需要在Redis的主配置文件中对模式订阅进行设定。注意,如果您只是使用psubscribe命令通过Channel发送消息到订阅者,或者更单纯的使用publish命令和subscribe命令组合通过Channel发送和接收消息,就不需要进行这样的配置

默认情况下Redis服务下的键空间通知和键事件通知都是关闭的。在redis.conf文件下,有专门的“EVENT NOTIFICATION”区域进行设定,设置的格式为:

......
notify-keyspace-events [通配符]
......

通配符的定义描述如下:

  • K:启用keyspace键空间通知,客户端可以使用__keyspace@<db>__为前缀的格式使用订阅功能。
  • E:启用keyevent键事件通知,客户端可以使用__keyevent@<db>__为前缀的格式使用订阅功能。
  • g:监控一般性事件,包括但不限于对del,expire,rename事件的监控。
  • $:启用对字符串格式(即一般K-V结构)命令的监控。
  • l:启用对List数据结构命令的监控。
  • s:启用对Set数据结构命令的监控。
  • h:启用对Hash数据结构命令的监控。
  • z:启用对ZSet数据结构命令的监控。
  • x:启用对过期事件的监控。
  • e:启用对驱逐事件的监控,当某个键因maxmemory达到设置时,使用策略进行内存清理,会产生这个事件。
  • A:g$lshzxe通配符组合的别名,也就是说”AKE”这样的通配符组合,意味着所有事件。

以下的几个实例说明了配置格式中通配符的用法:

// 监控任何数据格式的所有事件,包括键空间通知和键事件通知
notify-keyspace-events "AKE"

// 只监控字符串结构的所有事件,包括键空间通知和键事件通知
notify-keyspace-events "g$KExe"

// 只监控所有键事件通知
notify-keyspace-events "AE"

// 只监控Hash数据解构的键空间通知
notify-keyspace-events "ghKxe"

// 只监控Set数据结构的键事件通知
notify-keyspace-events "gsExe"

注意,在Redis主配置文件中进行事件通知的配置,其配置效果是全局化的。也就是说所有连接到Redis服务的客户端都会使用这样的Key事件通知逻辑。但如果单独需要为某一个客户端会话设置独立的Key事件通知逻辑,则可以在客户端成功连接Redis服务后,使用类似如下的命令进行设置:

......
192.168.61.140:6379> config set notify-keyspace-events KEA  
OK 

b、键事件订阅

完成键事件的配置后,就可以使用psubscribe命令在客户端订阅消息通知了。这个过程还是需要使用通配符参数,才能完成订阅指定。通配符格式如下所示:

psubscribe __[keyspace|keyevent]@<db>__:[prefix]

// 例如:
// 订阅0号数据库中,所有的键变化事件,进行键空间通知
psubscribe __keyspace@0__:*

// 订阅0号数据库,所有的键变化事件,进行键空间通知和键事件通知
psubscribe __key*@0__:*

注意,就如上文所提到的那样,客户端能够进行键信息变化事件订阅的前提是Redis服务端或者这个客户端会话本身开启了相应配置。以下举例说明psubscribe命令中参数的使用方式:

// 注意,Redis服务上的配置信息如下
// notify-keyspace-events "gsExe"
// 即是说只允许监控Set结构的所有事件,并且之启用了键事件通知,没有启用键空间通知。

// 客户端使用以下命令开始订阅Key的变化事件
192.168.61.140:6379> psubscribe __key*@0__:*
// 以上命令订阅了0号数据库所有键信息的变化通知,包括键事件通知和键空间通知
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "__key*@0__:*"
3) (integer) 1

// 接着,已连接到Redis服务上的另一个客户端执行了如下命令
// > sadd mysetkey rt
// 那么收到的消息通知为
1) "pmessage"
2) "__key*@0__:*"
3) "__keyevent@0__:sadd"
4) "mysetkey"

以上实例操作中有两个问题需要单独进行说明:

  • 当客户端使用psubscribe命令进行订阅时(psubscribe __key*@0__:*),实际上是连同keyspace(键空间通知)和keyevent(键事件通知)一起订阅了。那么按照上文介绍的内容来说,这个订阅者本该收到两条事件消息。一条消息的描述重点在key上,另一条消息的描述重点在sadd事件上。但实际情况是,这个订阅者只收到了以描述重点在事件上的键事件通知。这是因为在以上实例中特别说明的一点:Redis服务端只开启键事件通知的配置。所以无论客户端如何订阅键空间通知,也收不到任何消息

  • 另外,包括Redis官方资料在内的资料都在阐述这样一个事实,既是通过sadd命令对一个Set结构中的元素进行变更和直接通过“PUBLISH __keyevent@0__:sadd mysetkey”这样的命令向订阅者发送消息,在消息订阅者看来效果都是一样。但是这两种不同的操作过程对于Redis存储的Key数据,则是完全不一样的。前者的操作方式会改变Redis中存储的数据状况,但后者则不会。

3-4-4、Redis订阅/发布功能的不足

Redis提供的订阅/发布功能并不完美,更不能和ActiveMQ/RabbitMQ提供的订阅/发布功能相提并论。

  • 首先这些消息并没有持久化机制,属于即发即弃模式。也就是说它们不能像ActiveMQ中的消息那样保证持久化消息订阅者不会错过任何消息,无论这些消息订阅者是否随时在线。

  • 由于本来就是即发即弃的消息模式,所以Redis也不需要专门制定消息的备份和恢复机制。

  • 也是由于即发即弃的消息模式,所以Redis也没有必要专门对使用订阅/发布功能的客户端连接进行识别,用来明确该客户端连接的ID是否在之前已经连接过Redis服务了。ActiveMQ中保持持续通知的功能的前提,就是能够识别客户端连接ID的历史连接情况,以便确定哪些订阅消息这个客户端还没有处理。

  • Redis当前版本有一个简单的事务机制,这个事务机制可以用于PUBLISH命令。但是完全没有ActiveMQ中对事务机制和ACK机制那么强的支持。而在我写作的“系统间通讯”专题中,专门讲到了ActiveMQ的ACK机制和事务机制。

  • Redis也没有为发布者和订阅者准备保证消息性能的任何方案,例如在大量消息同时到达Redis服务是,如果消息订阅者来不及完成消费,就可能导致消息堆积。而ActiveMQ中有专门针对这种情况的慢消息机制。

3-5、Redis持久化存储

从严格意义上说,Redis服务提供四种持久化存储方案:RDB、AOF、虚拟内存(VM)和DISKSTORE。虚拟内存(VM)方式,从Redis Version 2.4开始就被官方明确表示不再建议使用,Version 3.2版本中更找不到关于虚拟内存(VM)的任何配置范例,Redis的主要作者Salvatore Sanfilippo还专门写了一篇论文,来反思Redis对虚拟内存(VM)存储技术的支持问题。

至于DISKSTORE方式,是从Redis Version 2.8版本开始提出的一个存储设想,到目前为止Redis官方也没有在任何stable版本中明确建议使用这用方式。在Version 3.2版本中同样找不到对于这种存储方式的明确支持。从网络上能够收集到的各种资料来看,DISKSTORE方式和RDB方式还有这一些千丝万缕的联系,不过各位读者也知道,除了官方文档以外网络资料很多就是大抄。

最关键的是目前官方文档上能够看到的Redis对持久化存储的支持明确的就只有两种方案(https://redis.io/topics/persistence):RDB和AOF。所以本文也只会具体介绍这两种持久化存储方案的工作特定和配置要点。

3-5-1、RDB

RDB中文名为快照/内存快照,它的过程很好理解,就是Redis按照一定的时间周期将目前服务中的所有数据全部写入到磁盘中。但这个过程说起简单,实际上呢有很多细节需要被处理。Redis主配置文件的“REPLICATION”部分,放置了对这个过程的配置选项。在我们后续文章中讲解Redis支持的主从复制时,也可以看到RDB的影子。

技术分享

上图反映了内存快照的大致过程,由于生产环境中我们为Redis开辟的内存区域都比较大(例如6GB),那么将内存中的数据同步到硬盘的过程可能就会持续比较长的时间,而实际情况是这段时间Redis服务一般都会收到数据写操作请求。那么如何保证数据一致性呢?RDB中的核心思路是Copy-on-Write,来保证在进行快照操作的这段时间,需要压缩写入磁盘上的数据在内存中不会发生变化。在正常的快照操作中,一方面Redis主进程会fork一个新的快照进程专门来做这个事情,这样保证了Redis服务不会停止对客户端包括写请求在内的任何响应。另一方面这段时间发生的数据变化会以副本的方式存放在另一个新的内存区域,待快照操作结束后才会同步到原来的内存区域。

另一个问题是,在进行快照操作的这段时间,如果发生服务崩溃怎么办?很简单,在没有将数据全部写入到磁盘前,这次快照操作都不算成功。如果出现了服务崩溃的情况,将以上一次完整的RDB快照文件作为恢复内存数据的参考。也就是说,在快照操作过程中不能影响上一次的备份数据。Redis服务会在磁盘上创建一个临时文件进行数据操作,待操作成功后才会用这个临时文件替换掉上一次的备份。

以下是Redis中关于内存快照的主要配置信息:

  • 快照周期:内存快照虽然可以通过技术人员手动执行SAVE或BGSAVE命令来进行,但生产环境下多数情况都会设置其周期性执行条件。Redis中默认的周期新设置如下:
# 周期性执行条件的设置格式为
save <seconds> <changes>
# 默认的设置为:
save 900 1
save 300 10
save 60 10000
# 以下设置方式为关闭RDB快照功能
save ""

以上三项默认信息设置代表的意义是:如果900秒内有1条Key信息发生变化,则进行快照;如果300秒内有10条Key信息发生变化,则进行快照;如果60秒内有10000条Key信息发生变化,则进行快照。读者可以按照这个规则,根据自己的实际请求压力进行设置调整。

  • stop-writes-on-bgsave-error:上文提到的在快照进行过程中,主进程照样可以接受客户端的任何写操作的特性,是指在快照操作正常的情况下。如果快照操作出现异常(例如操作系统用户权限不够、磁盘空间写满等等)时,Redis就会禁止写操作。这个特性的主要目的是使运维人员在第一时间就发现Redis的运行错误,并进行解决。一些特定的场景下,您可能需要对这个特性进行配置,这时就可以调整这个参数项。该参数项默认情况下值为yes,如果要关闭这个特性,指定即使出现快照错误Redis一样允许写操作,则可以将该值更改为no。

  • rdbcompression:该属性将在字符串类型的数据被快照到磁盘文件时,启用LZF压缩算法。Redis官方的建议是请保持该选项设置为yes,因为“it’s almost always a win”。

  • rdbchecksum:从RDB快照功能的version 5 版本开始,一个64位的CRC冗余校验编码会被放置在RDB文件的末尾,以便对整个RDB文件的完整性进行验证。这个功能大概会多损失10%左右的性能,但获得了更高的数据可靠性。所以如果您的Redis服务需要追求极致的性能,就可以将这个选项设置为no。

  • dbfilename:RDB文件在磁盘上的名称。

  • dir:RDB文件的存储路径。默认设置为“./”,也就是Redis服务的主目录。

3-5-2、AOF

由于是周期性的同步,所以RDB存在的最大问题就是在Redis异常崩溃,需要从最近一次RDB文件恢复数据时,常常出现最近一批更新的数据丢失,而且根据快照的周期设置,这批数据的总量还可能比较大。另外,虽然使用专门的快照进程进行快照数据同步的方式,本身不会造成Redis服务出现卡顿。但如果需要快照的数据量特别大,操作系统基本上会将CPU资源用到快照操作上去,这可能间接造成包括Redis主进程在内的其它进程被挂起。所以,以一个较大的时间周期全部同步Redis数据状态的快照方式,在非常高并发的情况下并不是最好的解决方法

虽然RDB快照基本上可以应付我们遇到的大多数业务场景,也可以满足至少80%业务系统设计时的预想性能压力,但为了尽可能解决RDB的工作缺陷,Redis还是提供了另一种数据持久化方式——AOF。AOF全称是Append Only File,从字面上理解就是“只进行增加的文件”。在本专题中,我们在介绍InnoDB的工作过程时,也介绍了类似“只进行增加的文件”,就是InnoDB中最关键的重做日志。

在物理磁盘的操作无论是机械磁盘还是固态磁盘,使用顺序读写都将获得比随机读写好得多的I/O性能。所以我们可以看到无论是关系型数据库、NoSQL数据库还是之前的业务案例,无一例外都追求在物理磁盘上尽可能进行顺序读写操作

AOF方式的核心设计思量可以总结为一句话:忠实记录Redis服务启动成功后的每一次影响数据状态的操作命令,以便在Redis服务异常崩溃的情况出现时,可以按照这些操作命令恢复数据状态。既然要记录每次影响数据状态的操作命令,就意味着AOF文件会越来越大!这是必然的。还好Redis为AOF提供了一种重写AOF文件的功能,保证了AOF文件中可以存储尽可能少的操作命令就能保证数据恢复到最新状态,这个功能被称为日志重写功能(请注意这可不是我们在讲解InnoDB时提到的重做日志)。

举个例子,操作人员在Redis中设置了一个K-V结构:mykey3 = yinwenjie,之后有删除了这个Key信息。那么AOF文件中记录的动作可能如下所示(AOF文件中的内容可以直接通过各种文本编辑工具直接查看):

# cat ./appendonly.aof
......
*3
$3
SET
$6
mykey3
$12
yinwenjie111
......
*2
$3
del
$6
mykey3
......

可以看到以上AOF文件中的内容,如实记录了这两个操作:设置Key和删除Key。但是这种日志记录过程对恢复Key的信息没有任何帮助,因为“mykey3”这个Key信息注定在最新的Redis内存中是不存在的。所以一旦我们运行“重写日志”命令(可以是设定的条件也可以直接运行“BGREWRITEAOF”命令),那么整理后的AOF文件的内容可能就是如下所示了:

# cat ./appendonly.aof
......
......

在AOF文件中对mykey3这个Key信息的操作过程记录消失了!这不但缩小了AOF文件还没有对数据恢复过程造成任何困扰。Redis主配置文件中关于AOF功能的设定可以在“APPEND ONLY MODE”部分找到:

  • appendonly:默认情况下AOF功能是关闭的,将该选项改为yes以便打开Redis的AOF功能。

  • appendfilename:这个参数项很好理解了,就是AOF文件的名字。

  • appendfsync:这个参数项是AOF功能最重要的设置项之一,主要用于设置“正真执行”操作命令向AOF文件中同步的策略。什么叫“正真执行”呢?还记得我们在本专题中介绍的Linux操作系统对磁盘设备的操作方式吗? 为了保证操作系统中I/O队列的操作效率,应用程序提交的I/O操作请求一般是被放置在Linux Page Cache中的,然后再由Linux操作系统中的策略自行决定正在写到磁盘上的时机。而Redis中有一个fsync()函数,可以将Page Cache中待写的数据真正写入到物理设备上,而缺点是频繁调用这个fsync()函数干预操作系统的既定策略,可能导致I/O卡顿的现象频繁 。如果您想继续了解操作系统上的工作的块存储技术,可以参看笔者另外几篇文章《架构设计:系统存储(4)——块存储方案(4)》

    appendfsync参数项可以设置三个值,分别是:always、everysec、no,默认的值为everysec。

    always参数值,会使得AOF对数据的保存非常稳健。其设置意义是只要有一个写操作命令执行成功,就执行一次fsync函数调用。所以很显然always的设定值,就是三个选项值中处理效率最慢的。

    no参数值,这个设置值表示Redis不会将执行成功的操作命令正真刷入AOF文件,而是完成操作系统级别的写操作后就认为AOF文件记录成功了,后续的I/O操作完全依赖于操作系统的设定,一般30秒会刷一次。

    everysec参数值,这是默认的设置值,也是可以在数据稳健性和性能上平衡较好策略。它表示每秒钟都做一次fsync函数调用,正真做AOF文件的写入操作。

  • no-appendfsync-on-rewrite:always和everysec的设置会使正真的I/O操作高频度的出现,甚至会出现长时间的卡顿情况,这个问题出现在操作系统层面上,所有靠工作在操作系统之上的Redis是没法解决的。为了尽量缓解这个情况,Redis提供了这个设置项,保证在完成fsync函数调用时,不会将这段时间内发生的命令操作放入操作系统的Page Cache(这段时间Redis还在接受客户端的各种写操作命令)。

  • auto-aof-rewrite-percentage:上文说到在生产环境下,技术人员不可能随时随地使用“BGREWRITEAOF”命令去重写AOF文件。所以更多时候我们需要依靠Redis中对AOF文件的自动重写策略。Redis中对触发自动重写AOF文件的操作提供了两个设置:auto-aof-rewrite-percentage表示如果当前AOF文件的大小超过了上次重写后AOF文件的百分之多少后,就再次开始重写AOF文件。例如该参数值的默认设置值为100,意思就是如果AOF文件的大小超过上次AOF文件重写后的1倍,就启动重写操作。

  • auto-aof-rewrite-min-size:参考auto-aof-rewrite-percentage选项的介绍,auto-aof-rewrite-min-size设置项表示启动AOF文件重写操作的AOF文件最小大小。如果AOF文件大小低于这个值,则不会触发重写操作。注意,auto-aof-rewrite-percentage和auto-aof-rewrite-min-size只是用来控制Redis中自动对AOF文件进行重写的情况,如果是技术人员手动调用“BGREWRITEAOF”命令,则不受这两个限制条件左右。

3-5-3、持久化存储的性能建议

关于持久化存储的性能建议,我们将结合后文介绍的Redis集群方案一起进行分析

3-7、后文介绍

后文开始,我们将继续介绍Redis支持的数据结构和内部原理。

<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    架构设计:系统存储(16)——Redis事件订阅和持久化存储