redis拾遗(三)——发布订阅,事务和lua脚本

# 前言

本篇博客总结一些redis在实际中的应用实例

发布订阅模式

如果提到发布订阅模式,我们首先想到的就是消息中间件,消息中间件中有很多比较冗杂的概念。但是redis其实也可以为我们实现一个简易版本的发布订阅模式

基于list实现的简易队列

redis中可以通过队列的rpush和lpop可以实现消息队列,但是消费者如果采用的普通的pop弹出消息的命令,则需要不断的去轮询消息队列,看是否有消息。为此,redis提供了一个阻塞消费消息的命令,blpop/brpop,如果消费端没有从队列中取出消息则会一直阻塞,如下动图所示,通过rpush+blpop组成的动图实例,右边的为消费者。

在这里插入图片描述
上述消息发送模式并不针对一对多,同时blpop/brpop需要指定超时时间

订阅频道

这种方式和消息中间件相差不大,消费者通过订阅指定频道的数据,生产者会往指定频道中发送数据。只有订阅了对应频道的消费者会受到消息,订阅支持正则表达式的通配符订阅。命令:publish/subscribe/psubscribe(正则表达式的方式订阅)

具体实例如下动图所示,右边两个为消费者,左边一个为生产者,在往不同队列中发送消息。

在这里插入图片描述

jedis对应的操作

生产者

/**
 * 生产者
 */
@Slf4j
public class PublishTest {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("192.168.72.128", 6379);
        jedis.publish("news-music", "JayZhou");
        jedis.publish("news-games", "JayZhouPlayGames");
        log.info("消息发送完毕");
    }
}

消费者

/**
 * 消费者
 */
@Slf4j
public class ConsumerTest {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("192.168.72.128", 6379);
        final MyListener listener = new MyListener();
        // 使用模式匹配的方式设置频道
        // 会阻塞
        jedis.psubscribe(listener, new String[]{"news-*"});//这里会阻塞,下一行日志永远不会被打印
        log.info("一轮消息接受完毕");
    }
}

事件监听处理器

/**
 * 监听的处理方式,需要继承JedisPubSub
 */
public class MyListener extends JedisPubSub {
    // 取得订阅的消息后的处理
    public void onMessage(String channel, String message) {
        System.out.println(channel + "=" + message);
    }

    // 初始化订阅时候的处理
    public void onSubscribe(String channel, int subscribedChannels) {
        // System.out.println(channel + "=" + subscribedChannels);
    }

    // 取消订阅时候的处理
    public void onUnsubscribe(String channel, int subscribedChannels) {
        // System.out.println(channel + "=" + subscribedChannels);
    }

    // 初始化按表达式的方式订阅时候的处理
    public void onPSubscribe(String pattern, int subscribedChannels) {
        // System.out.println(pattern + "=" + subscribedChannels);
    }

    // 取消按表达式的方式订阅时候的处理
    public void onPUnsubscribe(String pattern, int subscribedChannels) {
        // System.out.println(pattern + "=" + subscribedChannels);
    }

    // 取得按表达式的方式订阅的消息后的处理
    public void onPMessage(String pattern, String channel, String message) {
        System.out.println(pattern + "=" + channel + "=" + message);
    }
}

运行的时候,先运行消费者,再运行生产者。

redis事务

基本的三个命令

什么是事务,这里不再赘述,redis事务涉及四个命令:multi(开启事务),exec(执行事务),discard(取消事务),watch(监视),前三个命令是事务的基本命令,几乎只要提到事务这个概念,就会有前三个,只是最后一个可能对我们有些陌生

简单的命令实例

tom给jack转账200(凡是提到事务,转账似乎是永远绕不开的一个实例)

正常的事务执行命令

127.0.0.1:6379> set tom 1000
OK
127.0.0.1:6379> set jack 1000
OK
127.0.0.1:6379> multi ## 开启redis事务
OK
127.0.0.1:6379> decrby tom 200	
QUEUED ## redis 命令被缓存
127.0.0.1:6379> incrby jack 200
QUEUED
127.0.0.1:6379> exec ## 批量执行
1) (integer) 800
2) (integer) 1200
127.0.0.1:6379> mget tom jack
1) "800"
2) "1200" ## 金额正常减少

discard丢弃事务

127.0.0.1:6379> set tom 1000
OK
127.0.0.1:6379> set jack 1000
OK
127.0.0.1:6379> multi ## 开启事务
OK
127.0.0.1:6379> decrby tom 200
QUEUED
127.0.0.1:6379> incrby jack 200
QUEUED
127.0.0.1:6379> discard ## 丢弃事务
OK
127.0.0.1:6379> mget tom jack ## 金额未变化
1) "1000"
2) "1000"

需要注意的是redis的事务命令是不能嵌套的,在一个multi中不能开启另一个multi

watch

watch 为redis提供了一种CAS乐观锁行为(何为CAS就不解释了)。可以通过watch监视一个或者多个key ,如果开启事务之后,至少有一个被监视。被监视的key键在exec执行之前被修改了,那么整个事务都会被取消( key提前过期除外)。可以用unwatch取消对指定key值的监控。

如下实例

客户端1 客户端2
127.0.0.1:6379> set account 10000
OK
127.0.0.1:6379> watch account
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby account 1000
QUEUED
127.0.0.1:6379>
127.0.0.1:6379> incrby account 5000
(integer) 15000
127.0.0.1:6379> exec
(nil)
127.0.0.1:6379> get account
"15000"

上述实例可以看到,客户端1的事务最终并未执行成功。

需要说明的是,redis的事务不支持回滚,如果在事务的几个命令中有错误的,在错误命令之前的命令均会正常执行。redis官网针对这个问题是如下解释的——鉴于没有任何机制能避免程序员自己造成的错误, 并且这类错误通常不会在生产环境中出现, 所以 Redis 选择了更简单、更快速的无回滚方式来处理事务。因此我们没有办法来利用redis的事务机制保证原子性和数据一致性

lua脚本

lua是什么——lua 百度百科

linux下四行命令安装lua

curl -R -O http://www.lua.org/ftp/lua-5.4.0.tar.gz
tar zxf lua-5.4.0.tar.gz
cd lua-5.4.0
make all test

安装完成之后,直接敲lua,会出现如下提示信息表示安装成功

在这里插入图片描述

为什么redis中要执行lua脚本,前面我们说了,redis的事务机制无法满足原子性,那么批量命令的原子性需要通过lua来完成,lua与redis的关系,某种程度上来说像一个存储过程和数据库的关系。lua可以一次发送多个命令,同时能保证这些命令的原子性。

redis中执行lua脚本

命令:

eval lua-script key-num [key1 key2 key3 ....] [value1 value2 value3 ....]
  • eval redis执行lua脚本的命令
  • lua-script 表示lua脚本的内容
  • key-num 表示参数的个数
  • [key1 key2 key3 …] 表示参数列表
  • [value1 value2 value3 …] 表示对应的值的列表

实例

127.0.0.1:6379> eval "return 'hello this is lua script'" 0
"hello this is lua script" ###直接输出lua脚本的结果

lua中执行redis脚本

在lua中可以直接调用redis的命令,通过redis.call即可实现

命令:

redis.call(command, [KEYS1 KEYS2 KEYS3 ....] [ARGV1 ARGV2 ARGV3 ....])
  • command redis的命令
  • [KEYS1 KEYS2 KEYS3 …] 形参名列表
  • [ARGV1 ARGV2 ARGV3 …] 实参列表

实例:

127.0.0.1:6379> eval "return redis.call('set',KEYS[1],ARGV[1])" 1 lua luatest
OK
127.0.0.1:6379> get lua
"luatest"

redis调用lua文件

命令:

redis-cli --eval [lua文件路径] [KEYS列表] , [ARGV列表]

通过启动时执行lua脚本文件

编辑一个lua文件,内容如下

redis.call('set','luakey','luaRedisScript')
return redis.call('get','luakey')

在启动redis客户端的时候,指定脚本路径即可

[root@localhost bin]# redis-cli --eval /usr/local/self_lua_script/luaone.lua
"luaRedisScript"

实例

这里还是用比较常见的实例,ip地址访问次数限流

指定的ip地址,在5秒内只能访问10次。准备的lua脚本如下:

-- KEYS[1]ip地址,ARGV[1] 过期时间 ,ARGV[2]访问次数限制
local visitNum=redis.call('incr',KEYS[1])
if tonumber(visitNum) == 1 then
        redis.call('expire',KEYS[1],ARGV[1])
        return 1
elseif tonumber(visitNum)>tonumber(KEYS[2]) then
        return 0
else
        return 1
end

执行指定的lua脚本

redis-cli --eval /usr/local/self_lua_script/ip_limit.lua app:ip:limit:192.168.72.128 , 5 10 ## KEYS列表和ARGV列表逗号两头都要有空格

执行之后,可以看到相关效果,如果返回0,表示不允许访问。

最后说一点

如果lua脚本不安全,怎么搞?

eval 'while(true) do end' 0

如果某个客户端执行了上述的脚本,则redis服务端无法给其他客户端提供服务了,那如何避免出现这种问题?为了防止某个脚本执行时间过长导致Redis无法提供服务,Redis提供了lua-time-limit参数限制脚本的最长运行时间,默认为5秒钟。

在客户端中有一个script kill 命令,可以暴力中断上述脚本的执行

但如果执行如下脚本

eval "redis.call('set','testKey','testValue') while(true) do end"

则 script kill命令也不管用了。遇到这种情况,只能通过 shutdown nosave 命令来强行终止 redis。 shutdown nosave 和 shutdown 的区别在于 shutdown nosave 不会进行持久化操作,意味着发生在上一次快照后的数据库修改都会丢失。

总结

本篇博客总结了redis中一些复制的使用操作,基于list的简易生产消费,基于publish/subscribe和psubscribe的订阅消息操作。redis的事务。以及最后的lua脚本,同时提供了一个ip地址限流的实例。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章