Redis专题3:发布订阅模式事务Lua脚本揭秘
Posted 小文学编程
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis专题3:发布订阅模式事务Lua脚本揭秘相关的知识,希望对你有一定的参考价值。
Redis的原理篇
发布订阅模式
列表的局限性
当我们通过队列的rpush和lpop可以实现消息队列(队尾进队头出),但是消费者需要不停的调用lpop查看list中是否有等待处理的消息(比如写一个while循环)。为了减少通信的消耗,可以sleep()一段时间再消费,但是会有两个问题:
-
如果生产者消息的速度远大于消费者消息的速度,List会占用大量的内存。 -
消息的实时性降低
list还提供了一个阻塞命令:blpop,没有任何元素可以弹出的时候,连接会被阻塞。
blpop queue 5
基于list实现的消息队列,不支持一对多的消息分发
发布订阅模式
除了通过list 实现消息队列之外,Redis还提供了一组命令实现发布订阅模式
这种方法,发送者和接收者没有直接关联(实现了解耦),接收者也不需要持续尝试获取消息。
订阅频道
首先,我们会有很多的频道(channel),我们也可以把这个频道理解成queue。订阅者可以订阅一个或者多个频道。消息的发布者(生产者)可以给指定的频道发布消息。只要有消息达到了频道,所有订阅了这个频道的订阅者都会收到这条消息。
需要注意的是,发出去的消息不会被持久化,因为它已经从队列里面移除了,所以消费者只能收到它开始订阅这个频道之后发布的消息。
「发布订阅命令的使用方法」
订阅者订阅频道:可以一次订阅多个,比如这个客户端订阅了3个频道
subscribe channel-1 channel-2 channel-3
发布者可以向指定频道发布消息(并不支持一次向多个频道发送消息):
publish channel-1 2673
取消订阅
unsubscribe channel-1
按照规则订阅频道
支持?和*占位符。?代表一个字符, * 代表0个或者多个字符。
例如:
消费端1,关注运动消息:
psubscribe *sport
消费端2,关注所有的新闻
psubscribe news*
消费端3,关注天气新闻:
psubscribe news-weather
生产者,发布3条消息
publish news-sport yaoming
publish news-music zhoujielun
publish news-weather rain
事务
https://redis.io/topics/transactions/
http://redisdoc.com/topic/transaction.html/
为什么要使用事务
我们知道Redis的单个命令是原子性的,如果涉及到多个命令的时候,需要把多个命令作为一个不可分割的处理序列,就需要用到事务。
例如,setnx 实现分布式锁,我们先set 然后对key 设置expire,防止del 发生异常的时候锁不会被释放,业务处理完了以后再del,这三个动作我们希望他们作为一组命令执行:
Redis 的事务有两个特点:
-
按照进入队列的顺序执行 -
不会受到其他客户端的请求的影响,
Redis 的事务涉及到四个命令:multi(开启事务),exec(实行事务),discard(取消事务),watch(监视)
用法
例如,A和B各有1000元,A向B转账100元
set A 1000
set B 1000
multi
decrby A 100
incrby B 100
exec
get A
get B
通过multi 命令开启事务,事务不能嵌套,多个multi 命令效果是一样的
multi 执行后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当exec 命令被调用时,所有队列中的命令才会被执行。
通过 exec的命令执行事务,如果没有执行exec,所有命令都不会被执行。
如果中途,不想执行事务了,怎么办?
可以调用 discard 可以清空事务队里额,放弃执行。
multi
set k1 1
set k2 2
set k3 3
discard
watch 命令
在Redis 中还提供了一个watch 命令,他可以为Redis 事务提供CAS 乐观锁行为(Check and Set/ Compare and Swap),也就是多个线程更新变量的时候,会跟原值做比较,只有它没有被其他线程修改的情况下,才更新成新的值。
我们可以用watch 监视一个或者多个key,如果开启事务之后,至少有一个被监视key键在exec 执行之前被修改了,那么整个事务都会被取消(key 提前过期除外)。可以用unwatch 取消。
例如:
clientA | clientB |
---|---|
set A 1000 | |
watch A | |
multi | |
incrby | |
decrby A 100 | |
输出 900 | |
exec | |
get A | |
此时事务被取消,输出也是:900 |
事务可能遇到的问题
我们把事务执行遇到的问题分成两种,一种是在执行exec 之前发生的错误,一种是在执行exec 之后发生错误。
-
执行exec 之前发生错误
例如:入队的命令存在语法错误,包括参数数量,参数名等等
在这种情况下事务会被拒绝执行,也就是队列中所有的命令都不会得到执行。
-
在执行exec之后发生错误
比如类型错误,比如对String 使用Hash的命令,这是一种运行时错误。
最后我们发现,set A 1000 居然是成功的,也就是在这种发生了运行时错误的情况下,只有错误的命令没有被执行,但是其他命令没有收到影响。
Lua 脚本
Lua 是一种轻量级的脚本语言,他是用C语言编写的,跟数据的存储过程有点类似。使用Lua 脚本 来执行Redis 命令的好处:
-
一次发送多个命令,减少网络开销。 -
Redis 会将整个脚本作为一个整体执行,不会被其他请求打断,保持原子性。 -
对与复杂的组合命令,我们可以放在文件中,可以实现程序之间的命令集复用。
语法命令
使用 eval 方法 ,语法格式
-
eval 代表执行Lua语言的命令 -
script 代表Lua语言脚本内容 -
numkeys 表示参数中有多少个key,需要注意的是Redis 中key 是从1 开始的,如果没有参数,那么写0 -
[key ……] 是key作为参数传递给 Lua 语言,可以不填,但是需要 和 numkeys 对应起来 -
[arg ……] 这些参数传递给Lua ,他们可以不填
例如:
设置键值对
在Redis 中 调用 Lua 脚本执行Redis 命令
在Redis 中 直接写Lua 脚本不方便,也不能实现编辑和复用,通常我们会把脚本放到文件里面,然后执行这个命令。
在Redis 中调用Lua 脚本文件中的命令
创建 Lua 脚本文件
cd /usr/data/redis
vim test.lua
Lua 脚本内容
redis.call('set','A','lua22222')
return redis.call('get','A')
在Redis客户端中调用Lua脚本
redis-cli --eval test.lua 0
案例:IP限流
需求:在X秒内只能访问Y次
设计思路:用key记录IP,用value 记录访问次数
拿到IP以后,对IP+1,如果是第一次访问,对key设置过期时间(参数1)。否则判断次数,超过限定的次数(参数2),返回0,。如果没有超过次数则返回1。超过时间,key过期以后,可以再次访问。
KEY[1] 是 IP,ARGV[1]是过期时间,ARGV[2]是限制访问的次数Y
-- ip_limit.lua
-- IP限流,对某个IP频率进行限制
local num = redis.call('incr',KEYS[1])
if tonumber(num) == 1 then
redis.call('expire',KEYS[1],KEYS[1])
return 1
elseif tonumber(num) > tonumber(ARGV[2]) then
return 0
else
return 1
end
6 秒钟内限制访问10 次,
./redis-cli --eval 'ip_limit.lua' app:ip:limit:192.168.1.111 , 6 10
-
app:ip:limit:192.168.1.111 是key 值,后面是参数值,中间加上一个空格和一个逗号,再加上一个空格 -
多个参数之间用空格分割。
缓存Lua 脚本
在脚本比较长的情况下,如果每次调用脚本都需要把整个脚本传给Redis服务端,会产生较大的网络开销。为了解决这个问题,Redis 提供了EVALSHA 命令,允许开发者通过脚本内容的SHA1摘要来执行脚本。
「如何缓存」
Redis 在执行 script load 命令会计算脚本的SHA1摘要并记录在脚本缓存中,执行EVALSHA命令时Redis 会根据提供的摘要从脚本缓存中查找对应的脚本内容,如果找到了执行脚本,否则返回错误:“NOSCRIPT No mathing script. Please use EVAL”
脚本超时
Reids 的指令执行本身时单线程的,这个线程还要执行客户端的Lua 脚本,如果Lua 脚本执行超时或者陷入了死循环,是不是没有办法为客户端提供服务了呢?
为了防止某个脚本执行时间超长导致Redis 无法提供服务,Redis 提供了 lua-time-limit 限制脚本最长运行时间,默认为5秒钟
-
lua-time-limit 5000 (在redis.conf配置文件中)
当脚本运行时间超过这个限制之后,Redis 将开始接受其他命令但是不会执行(以确保脚本的原子性,因为此脚本并没有终止),而是会返回“BUSY”错误。
Redis 提供了一个script skill 的命令来终止脚本的执行,新开一个客户端。
script skill
如果当前执行的Lua 脚本对Redis的数据进行了修改,那么通过script skill 命令是不能终止脚本的执行的。
因为要确保脚本运行的原子性,如果脚本执行了一部分终止,那就违背了脚本原子性的要求。最终要保证脚本要么都执行,要么都不执行。
以上是关于Redis专题3:发布订阅模式事务Lua脚本揭秘的主要内容,如果未能解决你的问题,请参考以下文章