搞懂了 Redis 的订阅、发布与Lua,Redisson的加锁原理就好理解了

687 阅读8分钟

开心一刻

我找了个女朋友,长相很普通的那种
昨晚,我带她逛超市,听到有两个人在我们背后小声嘀咕:看咱前面,想不到这么丑都有人要
女朋友听后,羞的满脸通红,我想女朋友虽然长得一般,但是对我很好,我不会嫌弃她的
后面两个人继续嘀咕:是啊,那男人真丑!

开心一刻

写在前面

除了 Redis 自己提供的命令行工具:redis-cli,还有各种针对不同编程语言的客户端:Clients

Java 实现的 Redis 客户端有很多,推荐使用的有:JedisLettuceRedisson,而 Redisson 就是本文的主角之一

基于 Redisson 3.13.6 进行讲解,不同的版本,功能、特性还是有所不同的,这点需要注意

Redis 订阅发布

官方文档:Redis Pub/Sub

Redis 提供了基于 发布 / 订阅 模式的消息机制,此种模式下,消息发布者和订阅者不进行直接通信,发布者向指定的频道发布消息,订阅该频道的每个客户端都可以收到该消息,发布订阅模型如下:

redis 发布订阅

四个角色:发布者(Pub)、订阅者(Sub)、对两者解耦的中间方(Channel)、消息(Message);Sub 订阅 Channel,Pub 向 Channel 发布消息(Message),Sub 就能收到 Pub 发布的消息了。以公众号为例,我们(Sub)订阅某个公众号(Channel),公众号作者(Pub)在公众号每发表一篇文章(Message),就会向我们推送这篇文章,我们就可以浏览这篇文章了,当我们取消订阅了,它就不会再向我们推送这篇文章了;只要这个公众号一直在运行,就会一直有人订阅它或者取消订阅

可以将 发布/订阅 理解成分布式版的观察者模式,关于观察者模式,大家可以查看:设计模式之观察者模式 → 事件机制的底层原理;很多的 MQ 产品中都存在发布/订阅模式,只是各自的实现有细微差别

Redis 中发布/订阅相关的命令只有 6 个,我们在 redis-cli 下一个一个来看

  1. SUBSCRIBE

    通过该命令,客户端可以订阅一个或多个频道

    基本语法: subscribe channel [channel ...]

    假设我们订阅频道 channel:1,可以如下操作

    subscribe

    关于订阅命令(subscribe、psubscribe)有两点需要注意:

    1. 客户端在执行订阅命令后进入了订阅状态,只能接收 subscribe、psubscribe、unsubscribe、punsubscribe 这四个命令

      在 redis-cli 下更是表现为阻塞状态,只能接收消息,不能输入任何命令,但是我们要明白,redis 客户端除了 redis-cli,还很多针对不同编程语言的客户端;实际应用中,redis-cli 用的非常少,用的多的还是各种编程语言的 Redis 客户端

    2. 新开启的订阅客户端,无法接收到该频道之前的消息,因为 Redis 不会持久化发布的消息

  2. PUBLISH

    通过该命令,客户端可以向某个频道发布一条消息

    基本语法:publish channel message

    假设我们向频道 channel:1 发布消息,可以如下操作

    publish

    返回值 (integer) 1 表示有 1 个订阅者收到了消息,我们再看看之前的订阅客户端,收到了发布的消息

    pubsub
  3. UNSUBSCRIBE

    通过此命令,客户端可以取消对指定频道的订阅,取消成功后不再接收该频道发布的消息

    基本语法: unsubscribe [channel [channel ...]]

    我们取消对频道 channel:1 的订阅,可以如下操作

    unsubscribe
  4. PSUBSCRIBE

    按照模式订阅,可以理解成正则匹配订阅;subscribe 只能订阅一个或多个具体的频道,不能按正则匹配订阅,而此命令正好弥补这个空缺

    基本语法: psubscribe pattern [pattern ...]

    我们订阅以 channel:u 开头的所有频道,可以如下操作

    psubscribe

    此时,我们向频道 channel:user 发布消息,那么此客户端也能收到消息

    psubscribe_sub
  5. PUNSUBSCRIBE

    按照模式取消订阅,可以理解成正则匹配取消订阅;unsubscribe 只能对一个或多个具体的频道取消订阅,不能按正则匹配来取消订阅,而此命令正好弥补这个空缺

    基本语法: punsubscribe [pattern [pattern ...]]

    我们对 channel:r 开头的所有频道取消订阅,可以如下操作

    punsubscribe

    我们可以将 psubscribe、punsubscribe 与 subscribe、unsubscribe 进行类比,便于理解

  6. PUBSUB

    该命令用于查看订阅与发布系统状态,它由数个不同格式的子命令组成

    基本语法: pubsub subcommand [argument [argument ...]]

    该命令用法比较灵活,常用的功能有如下几个

    • 查看活跃的频道

      活跃的频道指的是当前频道至少有一个订阅者

      基本语法: pubsub channels [pattern] ,其中 [pattern] 是可以指定具体的模式

      查看所有活跃的频道,可以如下操作

      pubsub_all

      查看符合某种模式的活跃频道,可以如下操作

      pubsub_pattern
    • 查看频道订阅数

      基本语法: pubsub numsub [channel ...]

      pubsub_numsub

      channel:1 频道的订阅数是 1,channel:user 频道的订阅数也是 1

    • 查看模式订阅数

      基本语法: pubsub numpat

      pubsub_numpat

      返回的不是订阅模式的客户端的数量, 而是客户端订阅的所有模式的数量总和

  7. Redisson 订阅发布

    上面讲了那么多,其实都是在 redis-cli 下自嗨,如何在实际项目中应用起来了,我们基于 Redisson 来实现个简单示例

    • 订阅端

      Sub
    • 发布端

      Pub

    完整代码:pubsub,执行结果如下

    pubsub

至此,相信大家对 Redis 的发布/订阅有了一定的了解了

Redis 的 Lua

官方文档:Redis Lua scripting;Lua 语法比较简单,基本都能看懂,感兴趣的可以去看它的官方文档:Lua Documentation,本文不作详细介绍

Redis 提供了一系列的命令供我们使用:Redis Commands,基本上能满足我们的绝大部分需求;但是,总有一些特殊的需求游离在三界之外,不在五行之中,不能通过其中的某个命令直接实现。有人可能就会说了:一个命令不行,那就多个命令组合实现嘛,但是我们需要考虑到

多个命令组合能保证原子性吗,如果有逻辑处理又该怎么办?

Redis 早已替我们想好了解决办法,那就是 Lua;在 Redis 中执行 Lua 脚本有两种方法:evalevalsha

  1. eval

    基本语法: eval script numkeys key [key ...] arg [arg ...]

    其中 script 表示 Lua 脚本,numkeys 表示 key 个数,我们来看个案例

    eval

    其中表示 .. 表示连接两个字符串

    如果 Lua 脚本太长,还可以使用 redis-cli --eval 直接执行文件

    基本语法: redis-cli --eval script key [key...] , arg [arg ...]

    注意:key 与 arg 之间是 , ,英文逗号前后都有一个空格

    eval_file

    hello.lua 文件内容:

    return 'hello '..KEYS[1]..ARGV[1] 
    
  2. evalsha

    除了 eval,Redis 还提供了 evalsha 来执行 Lua 脚本

    基本语法: evalsha sha1 numkeys key [key ...] arg [arg ...]

    使用 evalsha 之前需要将 Lua 脚本加载到 Redis 服务端,得到该脚本的 SHA1 校验和,然后将 SHA1 作为 evalsha 的入参执行对应的 Lua 脚本;脚本会常驻 Redis 服务端,客户端执行脚本时不需要每次都传递脚本到服务端,使得脚本得以复用,降低了参数传递的开销

    加载脚本基本语法: redis-cli script load script

    load

    得到 SHA1: 5a8bcaa0ac71ab25ea5c504d61964859fffc20ce ,再执行 evalsha 命令

    evalsha

如果 Lua 脚本中需要使用 Redis 的命令,该怎么办呢,Redis 提供了 API;Lua 可以使用 redis.call 函数实现对 Redis 命令的调用,例如:

redis_call

另外还可以使用 redis.pcall 函数实现对 Redis 命令的调用;redis.call 和 redis.pcall 的区别在于,如果 redis.call 执行失败,那么脚本执行结束会直接返回错误,而 redis.pcall 会忽略错误继续执行脚本

使用 Lua 带来了什么好处了,我给大家总结下

  1. Lua 脚本在 Redis 中是原子执行的,执行过程中不会插入其他命令
  2. 通过 Lua 脚本,我可以创造出自己定制的命令,并可以将这些命令常驻在内存,实现复用
  3. Lua 脚本可以将多条命令一次性打包,有效减少网络开销

基于 Redisson,如何使用 Lua 呢?我们来看个案例

redisson_lua

完整代码:LuaDemo,执行结果如下

lua

LuaDemo.java 中有个方法 distLockTest ,有兴趣的可以看看,对理解 Redisson 分布式锁的实现有帮助

八股面题

给大家遗留两个经典面试题,欢迎评论区留言

  1. 客户端未主动取消订阅,而是直接断开连接,Redis 服务端会如何处理该客户端订阅的那些频道
  2. Lua 脚本保证的是执行该脚本的过程中,不能有其他命令插入,但是如果脚本中的某个命令出错了,Redis 会如何处理

总结

  1. Redis 发布订阅模式可以类比观察者模式,便于理解
  2. Lua 在 Redis 中非常灵活,相当于给我们留了一个自定义命令的接口
  3. Redis 客户端有很多,我们不能只局限于 redis-cli,Redisson 就是比较常用的客户端之一