03. Redis 高级特性
Posted IT BOY
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了03. Redis 高级特性相关的知识,希望对你有一定的参考价值。
05 Redis 高级特性
Pt1 发布订阅
介绍LIST的时候我们说过,可以使用LIST实现生产者消费者队列,但是如果要实现一对多的发布订阅模式,则无法实现。为此Redis提供了基于channel的发布订阅功能。
消息的发布者可以向指定的channel发布消息,订阅者可以订阅一个或者多个channel,只要消息到达了channel,所有订阅者都会受到这条消息。
127.0.0.1:6379> subscribe channel1
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "channel1"
3) (integer) 1
1) "message"
2) "channel1"
3) "hahah"
Redis还支持正则匹配的方式订阅channel,可以使用?和*模糊匹配。
-
?代表一个字符
-
*代表0个或多个字符
# 订阅者使用psubscribe模糊匹配
127.0.0.1:6379> psubscribe chn*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "chn*"
3) (integer) 1
# 以下是发布消息后的处理
1) "pmessage"
2) "chn*"
3) "chnn"
4) "hahah"
# 发布消息
127.0.0.1:6379> publish chnn hahah
(integer) 1
Pt2 事务
Redis请求命令是单线程的,单个命令是原子的,要么成功要么失败,不存在并发的数据一致性问题。但是,多个命令或者多个客户端并发操作命令时,就需要通过处理来解决并发产生的问题。Redis提供了事务的功能,可以把一组命令一起执行,事务有3个特点:
-
按进入队列的顺序执行命令
-
不会受到其他客户端的请求影响
-
事务不能嵌套,多个multi命令效果一样
Redis的事务涉及到四个命令:
-
multi 开启事务:通过multi命令开启事务,客户端可以继续向服务端发送任意多条命令,这些命令不会立即被执行,而是放到一个队列中;
-
exec 执行任务:队列中的命令被有序执行,不执行exec,所有命令都不会被执行;
-
discard 取消事务:取消队列中命令执行;
-
watch 监视事务执行:监视key的修改,后面会详说;
但是Redis的事务没有那么完美,和关系型数据库事务并不相同。我们知道,事务是保证一组命令执行的原子性,要么全部成功,要么全部失败,部分成功的命令必须回滚以保证原子性,但是Redis没有这么做,它没有回滚。
-
当exec命令执行前发生异常时,将取消事务执行,队列中所有命令都不会得到执行;
-
当exec命令执行后发生异常时,已经执行的命令不会被回滚,错误的命令将不会被执行;
# 场景1:exec命令执行前发生异常
# 开启事务
127.0.0.1:6379> multi
OK
# 执行命令
127.0.0.1:6379> set name lucas
QUEUED
127.0.0.1:6379> set age 30
QUEUED
# hset少一个value参数,所以命令是错误的
127.0.0.1:6379> hset job huifu
(error) ERR wrong number of arguments for 'hset' command
# 执行事务显示因为命令错误事务已经被取消
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
# 场景2:exec命令执行后发生异常
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set name lucas
QUEUED
# 命令和上一个key值冲突了
127.0.0.1:6379> hset name chinese chen
QUEUED
127.0.0.1:6379> set age 30
QUEUED
# 执行结果
127.0.0.1:6379> exec
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
3) OK
所以Redis的事务和通常理解的不太一致,我们很难使用Redis的事务机制来实现原子性,保证数据一致性。Redis官方也对此做出了解释:
However there are good opinions for this behavior:
Redis commands can fail only if called with a wrong syntax (and the problem is not detectable during the command queueing), or against keys holding the wrong data type: this means that in practical terms a failing command is the result of a programming errors, and a kind of error that is very likely to be detected during development, and not in production.
Redis is internally simplified and faster because it does not need the ability to roll back.
主要意思就是说,在上面两种场景下,都是代码产生的问题,可以在生产上线前解决,Redis不愿意为这种错误做出牺牲,希望保持内部命令执行的简单和快速。
Watch命令
当多个客户端更新变量的时候,数据可能会被别的客户端修改,带来非预期的结果,所以Redis提供了Watch命令。
Watch命令和CAS乐观锁行为一致,当使用watch时,会记录key对应的值,在更新key值时,会和watch时的数据进行比较,只有相同的时候才能更新成功。
如果开启事务之后,至少有一个被监视的key在exec执行之前被修改,整个事务都会被取消。
Pt3 Lua脚本
Lua是一种轻量级脚本语言,和存储过程有点类似。使用Lua脚本执行Redis有以下好处:
-
一次发送多个命令,减少网络开销;
-
Redis将整个脚本作为整体执行,不会被其他请求打断,具备原子性;
-
复杂的组合命令,可以放在文件中,后续还可以复用;
Pt3.1 在Redis中调用Lua脚本
使用eval语法可以在Redis客户端执行Lua命令,命令格式如下:
eval luaScript keyNum [key1 key2 key3 ...][value1 value2 value3]
-
eval代表执行Lua语言的命令;
-
luaScript代表Lua语言脚本的内容;
-
key-num表示参数中有多少个key,需要注意的是Redis中key是从1开始的,如果没有key的参数,那么写0。
-
[key1 key2 key3…]是key作为参数传递给Lua语言,也可以不填,但是需要和key-num的个数对应起来。
-
[value1 value2 value3]这些参数传递给Lua语言,他们是可填可不填的。
127.0.0.1:6379> eval "return 'Hello world'" 0
"Hello world"
Pt3.2 在Lua脚本中执行Redis命令
使用redis.call在Lua脚本中执行Redis命令,语法如下:
redis.call(command, key, [param1, param2...])
-
command是命令,包括set,get..
-
key是被操作的键
-
param1,param2 代表给key的参数
127.0.0.1:6379> eval "return redis.call('set', 'name','lucas')" 0
OK
127.0.0.1:6379> get name
"lucas"
当然直接执行脚本看起来没有什么优势,而且很复杂,我们可以将多个命令放到文件中执行:
# 查看Lua文件内容
root@fa0050d94745:/data# ls
appendonly.aof appendonly.aof.aaa backup.db dump.rdb redis.lua root zzh
root@fa0050d94745:/data# cat redis.lua
redis.call('set', 'age', '45')
return redis.call('get', 'age')
# 执行Redis Lua文件
root@fa0050d94745:/data# redis-cli --eval redis.lua 0
"45"
Pt3.3 Redis+Lua案例
用Lua脚本实现一个简单的限流操作,限制每个用户在X秒内只能访问Y次
脚本如下:
root@fa0050d94745:/data# cat redisIpLimit.lua
local num = redis.call('incr', KEYS[1])
if tonumber(num) == 1 then
redis.call('expire',KEYS[1],ARGV[1])
return 1
elseif tonumber(num) > tonumber(ARGV[2]) then
return 0
else
return 1
end
测试结果(6秒钟不超过5次):
root@fa0050d94745:/data# redis-cli --eval redisIpLimit.lua app:ip:limit:127.0.0.1 , 6 5
(integer) 1
root@fa0050d94745:/data# redis-cli --eval redisIpLimit.lua app:ip:limit:127.0.0.1 , 6 5
(integer) 1
root@fa0050d94745:/data# redis-cli --eval redisIpLimit.lua app:ip:limit:127.0.0.1 , 6 5
(integer) 1
root@fa0050d94745:/data# redis-cli --eval redisIpLimit.lua app:ip:limit:127.0.0.1 , 6 5
(integer) 1
root@fa0050d94745:/data# redis-cli --eval redisIpLimit.lua app:ip:limit:127.0.0.1 , 6 5
(integer) 1
root@fa0050d94745:/data# redis-cli --eval redisIpLimit.lua app:ip:limit:127.0.0.1 , 6 5
(integer) 0
root@fa0050d94745:/data# redis-cli --eval redisIpLimit.lua app:ip:limit:127.0.0.1 , 6 5
(integer) 0
Pt3.4 Lua脚本缓存
如果Lua脚本过大,每次调用脚本都需要把整个脚本传给Redis服务端,网络开销会非常大。为了解决这个为题,Redis可以缓存Lua脚本并声称SHA1摘要码,以后可以直接通过摘要码来执行Lua脚本。
-
通过script load命令生成摘要
-
evalsha 执行缓存的摘要脚本
127.0.0.1:6379> script load "return 'Hello World'"
"470877a599ac74fbfda41caa908de682c5fc7d4b"
127.0.0.1:6379> evalsha "470877a599ac74fbfda41caa908de682c5fc7d4b" 0
"Hello World"
以上是关于03. Redis 高级特性的主要内容,如果未能解决你的问题,请参考以下文章
golang常用库包:缓存redis操作库go-redis使用(03)-高级数据结构和其它特性