Redis集群状态下的发布订阅
在Redis的几个基本数据结构介绍中,有讲过List数据结构,可以使用List的阻塞特性实现订阅消费,关于Redis的底层数据结构可以参考我的这篇博客:Redis第六讲 Redis之List底层数据结构实现
底层数据结构基本操作可以看我的这篇博客,Redis第十五讲 Redis常用数据结构的基本操作
Redis发布订阅机制,在这种机制下,消息发布者向指定频道(channel)发布消息,消息订阅者可以收到指定频道的消息,同一个频道可以有多个消息订阅者,如下图:

Redis发布订阅/生产者消费者
在Redis中,发布订阅相关命令有:
- 发布消息
- 订阅频道
- 取消订阅
- 按照模式订阅
- 按照模式取消订阅
- 查询订阅信息
- 发布消息的命令是publish,语法是:
publish 频道名称 消息
192.168.36.128:6382> publish channel:shanghai "tody is sunny"
(integer)
返回的结果是订阅者的个数,上例中没有订阅者,所以返回结果为0。
- 订阅消息的命令是subscribe,订阅者可以订阅一个或者多个频道,语法是:
192.168.36.128:6382> subscribe channel:shanghai
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "channel:shanghai"
3) (integer) 1
返回结果中有3条,分别表示:返回值的类型(订阅成功)、订阅的频道名称、目前已订阅的频道数量。当订阅者接受到消息时,就会显示:
1) "message"
2) "channel:shanghai"
3) "tody is sunny"
同样也是3条结果,分别表示:返回值的类型(信息)、消息来源的频道名称、消息内容。
新开启的订阅者,是无法收到该频道之前的历史消息的,因为Redis没有对发布的消息做持久化。
- 取消订阅的命令是unsubscribe,可以取消一个或者多个频道的订阅,语法是:
unsubscribe [频道名称 [频道名称 ...]]
- 按模式订阅消息的命令是psubscribe,订阅一个或多个符合给定模式的频道,语法是:
psubscribe 模式 [模式 ...]
每个模式以 * 作为匹配符,比如 channel* 匹配所有以 channel 开头的频道,命令如下:
> psubscribe channel:*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "channel*"
3) (integer) 1
返回结果中有3条,分别表示:返回值的类型(按模式订阅成功)、订阅的模式、目前已订阅的模式数量。当订阅者接受到消息时,就会显示:
1) "pmessage"
2) "channel*"
3) "channel:one-more-study:demo"
4) "I am One More Study."
- 按模式取消订阅的命令是punsubscribe,可以取消一个或者多个模式的订阅,语法是
punsubscribe [模式 [模式 ...]]
每个模式以 * 作为匹配符,比如 channel:* 匹配所有以 channel 开头的频道,命令如下:
查询订阅信息
- 查看活跃频道
活跃频道指的是至少有一个订阅者的频道,语法是:
pubsub channels [模式]
- 查看频道订阅数
pubsub numsub [频道名称 ...]
- 查看模式订阅数
> pubsub numpat
(integer) 1
发布订阅模式原理
频道的发布订阅理
Redis将所有频道的订阅关系都保存在服务器状态的 pubsub_channels 字典,字典的键是某个被订阅的频道,而对应值则是一个链表,链表里记录了所有订阅这个频道的客户端。
struct redisServer{ //...
// 保存所有频道订阅关系 dict *pubsub_channels; //...}

- 建立订阅关系执行分两种情况:
1)该频道已有其他订阅者
该频道在 pubsub_channels 字典中存在订阅者链表,将此客户端添加至订阅者链表末尾即可;
2)该频道暂无订阅者
该频道在 pubsub_channels 字典中不存在订阅者链表,首先在字典中为频道创建一个键,并将这个键的值设置为空链表,然后将客户端添加到链表,成为链表的第一个元素。
当客户端退订某个或某些频道的时候,服务器将从 pubsub_channels 中解除客户端与被退订频道之间的关联。
- 解除订阅关系执行过程:
1)根据被退订频道的名字,在 pubsub_channels 字典中找到频道对应的订阅者链表,然后从订阅者链表中删除退订客户端的信息;
2)假如删除退订客户端后,频道的订阅者链表变成了空链表,那么说明这个频道已无任何订阅者了,将从 pubsub_channels 字典中删除频道对应的键。
模式的发布订阅原理
模式与频道的区别,简单理解模式是多个频道的组合。Redis将所有模式的订阅关系都保存在服务器状态的 pubsub_patterns 链表,链表的每个节点都包含着一个 pubsub Pattern 结构,这个结构的 pattern 属性记录了被订阅的模式,而 client 属性则记录了订阅模式的客户端。
struct redisServer{
//... // 保存所有模式订阅关系 dict *pubsub_patterns; //...}

- 当客户端执行 PSUBSCRIBE 命令订阅某个或某些模式的时候,服务器会对每个被订阅的模式执行以下两个操作:
1)新建一个 pubsubPattern结果,将结构的 pattern 属性设置为被订阅的模式,client 属性设置为订阅模式的客户端;
2)将pubsubPattern结构添加到 pubsub_patterns 链表的尾部。
- 当客户端退订某个或某些模式的时候,服务器将从 pubsub_patterns 链表中查找并删除那些 pattern 属性为被退订模式,并且client 属性为执行退订命令的客户端的 pubsubPattern 结构。
简单理解即:查找client、pattern 均相同的 pubsubPattern 并删除。
Redis集群事务
redis集群不支持事务,但内部单节点是支持事务的。而且Redis没有行锁的概念也没有事务回滚,但提供了事务撤销命令
-
redis事务常用命令
Watch: 可以监听一个或者多个key,在提交事务之前是否有发生了变化 如果发生边了变化就不会提交事务,
没有发生变化才可以提交事务 版本号 乐观锁
Multi: 开启事务
EXEC: 提交事务
Discard: 取消提交事务 -
watch和exec区别
watch 是监听key,watch实际上用的是版本号乐观锁概念,一旦有变动,版本号+1
mult 是开启事务 执行exec的时候,如果用了watch发现版本号变了就不会提交,如果没用watch,就直接提交事务。
String key = "transaction-key";
jedis.set(key, "20");
jedis.watch(key);//注册key,此后key将会被监控,如果在事务执行前被修改,则导致事务被DISCARD。
jedis.incr(key);//此key被修改,即使是自己,也会导致watch在事务中执行失效
jedis.unwatch();//取消注册
jedis.watch(key);//重新注册,在重新注册前,必须unwatch
Transaction tx = jedis.multi();//开启事务,开启事务前进行watch
-
Multi开启事务的特点
Multi开启事务的时候,不和mysql一样拥有行锁的功能,也就是没有隔离性,多个事务可以同时对一个key进行操作,且每次都会覆盖,最后提交的事务就是key的最后值。 -
取消事务跟回滚有什么区别
回滚对事务取消和行锁都会撤销
Redis没有回滚 单纯取消事务(不提交事务) 不上锁,事务取消只会取消当前提交,不会影响其他提交(已经提交的),而mysql如果在同一个事务中有一个错误其他提交都需要回滚
Redis事务底层原理
Redis中,如果一个事务被提交,那么事务中的所有操作将会被顺序执行(放在一个队列里面),且在事务执行期间,其他client的操作将会被阻塞;Redis采取了这种简单而“粗鲁”的方式来确保事务的执行更加的快速和更少的外部干扰因素。
EXEC指令将会触发事务中所有的操作被写入AOF文件(如果开启了AOF),然后开始在内存中实施这些数据变更操作;Redis将会尽力确保事务中所有的操作都能够执行,如果redis环境故障,有可能导致事务未能成功执行,那么需要在redis重启后增加额外的校验工作。
如果在EXEC指令被提交之前,Redis-server即检测到提交的某个指令存在语法错误,那么此事务将会被提前标记DISCARD,此后事务提交也将直接被驳回;但是如果在EXEC提交后,在实施数据变更时(Redis将不会预检测数据类型,比如你对一个“非数字”类型的key执行INCR操作),某个操作导致了ERROR,那么redis仍然不会回滚此前已经执行成功的操作,而且也不会中断ERROR之后的其他操作继续执行。对于开发者而言,你务必关注事务执行后返回的结果(结果将是一个集合,按照操作提交的顺序排列,对于执行失败的操作,结果将是一个ERROR)。
Redis的事务之所以如此设计,它为了确保本身的性能,同时不引入“关系型数据库”的设计复杂度;你不能完全希望Redis能为你交付完美的事务操作,只能说,你选择了错误的工具。
public void transaction(){
String key = "transaction-key";
jedis.set(key, "20");
jedis.watch(key);
Transaction tx = jedis.multi();
tx.incr(key);
tx.incr(key);
tx.incr(key);
List<Object> result = tx.exec();
if(result == null || result.isEmpty()){
System.out.println("Transaction error...");//可能是watch-key被外部修改,或者是数据操作被驳回
return;
}
for(Object rt : result){
System.out.println(rt.toString());
}
}
Redis 在接收到 MULTI 命令后便会开启一个事务,这之后的所有读写命令都会保存在队列中但并不执行,直到接收到 EXEC 命令后,Redis 会把队列中的所有命令连续顺序执行,并以数组形式返回每个命令的返回结果
本文详细介绍了Redis集群环境下的发布订阅模式,包括频道和模式的订阅与取消订阅,以及其底层原理。同时,文章阐述了Redis集群不支持的事务特性,解释了单节点事务的命令如Watch、Multi、EXEC和Discard,以及Redis事务的执行特点和潜在风险。
5233

被折叠的 条评论
为什么被折叠?



