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)-高级数据结构和其它特性

深入了解 RedisRedis 高级特性

5Redis高级特性(慢查询Pipeline事务Lua)

5Redis高级特性(慢查询Pipeline事务Lua)

redis 高级特性 不要太好用

Redis高级实用特性