本篇博客总结一些redis在实际中的应用实例
如果提到发布订阅模式,我们首先想到的就是消息中间件,消息中间件中有很多比较冗杂的概念。但是redis其实也可以为我们实现一个简易版本的发布订阅模式
redis中可以通过队列的rpush和lpop可以实现消息队列,但是消费者如果采用的普通的pop弹出消息的命令,则需要不断的去轮询消息队列,看是否有消息。为此,redis提供了一个阻塞消费消息的命令,blpop/brpop,如果消费端没有从队列中取出消息则会一直阻塞,如下动图所示,通过rpush+blpop组成的动图实例,右边的为消费者。
上述消息发送模式并不针对一对多,同时blpop/brpop需要指定超时时间
这种方式和消息中间件相差不大,消费者通过订阅指定频道的数据,生产者会往指定频道中发送数据。只有订阅了对应频道的消费者会受到消息,订阅支持正则表达式的通配符订阅。命令:publish/subscribe/psubscribe(正则表达式的方式订阅)
具体实例如下动图所示,右边两个为消费者,左边一个为生产者,在往不同队列中发送消息。
/**
* 生产者
*/
@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事务涉及四个命令: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 为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 百度百科
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可以一次发送多个命令,同时能保证这些命令的原子性。
命令:
eval lua-script 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的命令,通过redis.call即可实现
命令:
redis.call(command, [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-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地址限流的实例。