0%

redis事务lua脚本和pipeline

1. 事务

Redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。准确的讲,Redis 事务包含两种模式 : 事务模式 和 Lua 脚本。

1.1 Redis 的 ACID

  • 原子性atomicity

首先通过上文知道 运行期的错误是不会回滚的,很多文章由此说Redis事务违背原子性的;而官方文档认为是遵从原子性的。

Redis官方文档给的理解是,Redis的事务是原子性的:所有的命令,要么全部执行,要么全部不执行。而不是完全成功。

  • 一致性consistency

redis事务可以保证命令失败的情况下得以回滚,数据能恢复到没有执行之前的样子,是保证一致性的,除非redis进程意外终结。

  • 隔离性Isolation

redis事务是严格遵守隔离性的,原因是redis是单进程单线程模式(v6.0之前),可以保证命令执行过程中不会被其他客户端命令打断。

但是,Redis不像其它结构化数据库有隔离级别这种设计。

  • 持久性Durability

redis事务是不保证持久性的,这是因为redis持久化策略中不管是RDB还是AOF都是异步执行的,不保证持久性是出于对性能的考虑。

1.2 事务的使用

  • MULTI :开启事务,redis会将后续的命令逐个放入队列中,然后使用EXEC命令来原子化执行这个命令系列。
  • EXEC:执行事务中的所有操作命令。
  • DISCARD:取消事务,放弃执行事务块中的所有命令。
  • WATCH:监视一个或多个key,如果事务在执行前,这个key(或多个key)被其他命令修改,则事务被中断,不会执行事务中的任何命令。
  • UNWATCH:取消WATCH对所有key的监视。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 11
QUEUED
127.0.0.1:6379> set k2 22
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) OK
127.0.0.1:6379> get k1
"11"
127.0.0.1:6379> get k2
"22"
127.0.0.1:6379>

1.3 事务错误处理

img

大概的意思是,作者不支持事务回滚的原因有以下两个:

  • 他认为 Redis 事务的执行时,错误通常都是编程错误造成的,这种错误通常只会出现在开发环境中,而很少会在实际的生产环境中出现,所以他认为没有必要为 Redis 开发事务回滚功能;

  • 不支持事务回滚是因为这种复杂的功能和 Redis 追求的简单高效的设计主旨不符合。

这里不支持事务回滚,指的是不支持事务运行时错误的事务回滚。

举例说明:

  • 语法错误(编译器错误),开启事务后,修改k1值为11,k2值为22,但k2语法错误,最终导致事务提交失败,k1、k2保留原值。
  • Redis类型错误(运行时错误),开启事务后,修改k1值为11,k2值为22,但将k2的类型作为List,在运行时检测类型错误,最终导致事务提交失败,此时事务并没有回滚,而是跳过错误命令继续执行, 结果k1值改变、k2保留原值。
  1. 命令入队时报错, 会放弃事务执行,保证原子性(回滚)。
  2. 命令入队时正常,执行 EXEC 命令后报错,不保证原子性(不回滚)。

1.4 分布式锁能用事务吗

setnxexpire是两条指令而不是原子指令。如果这两条指令可以一起执行就不会出现问题。

也许你会想到用Redis事务来解决。但是这里不行,因为expire是依赖于setnx的执行结果的,如果setnx的执行结果的,如果setnx没抢到锁,expire是不应该执行的。redis事务里没有if-else分支逻辑,事务的特点是一口气执行,要么全部执行要么一个都不执行。

Redis2.8版本中作者加入了set指令的扩展参数,使得setnxexpire组合在一起的原子指令一起执行,它就是redis分布式锁的原理;

1
SET key uuid NX EX seconds

2. lua 脚本

Redis对Lua脚本的支持是从Redis 2.6.0版本开始引入的,Redis服务器以原子方式执行Lua脚本,在执行完整个Lua脚本及其包含的Redis命令之前,Redis服务器不会执行其他客户端发送的命令或脚本,因此被执行的Lua脚本天生就具有原子性。

Lua脚本的另一个好处是它能够在保证原子性的同时,一次在脚本中执行多个Redis命令:对于需要在客户端和服务器之间往返通信多次的程序来说,使用Lua脚本可以有效地提升程序的执行效率。虽然使用流水线加上事务同样可以达到一次执行多个Redis命令的目的,但Redis提供的Lua脚本缓存特性能够更为有效地减少带宽占用。

2.1 使用

Lua脚本的强大之处在于它可以让用户直接在脚本中执行Redis命令,这一点可以通过在脚本中调用redis.call()函数或者redis.pcall()函数来完成:

1
2
3
4
5
redis> EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 "message" "hello world"
OK

redis> GET "message"
"hello world"

脚本中的redis.call(‘SET’, KEYS[1], ARGV[1])表示被执行的是Redis的SET命令,而传给命令的两个参数则分别是KEYS[1]和ARGV[1],其中KEYS[1]为”message”,而ARGV[1]则为”hello world”。

2.2 原子性

Redis的Lua脚本与Redis的事务一样,都是以原子方式执行的:在Redis服务器开始执行EVAL命令之后,直到EVAL命令执行完毕并向调用者返回结果之前,Redis服务器只会执行EVAL命令给定的脚本及其包含的Redis命令调用,至于其他客户端发送的命令请求则会被阻塞,直到EVAL命令执行完毕为止。

3. 管道 Pipeline

3.1 使用

redis的管道命令,允许client将多个请求依次发给服务器,过程中而不需要等待请求的回复,在最后再一并读取结果即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
"github.com/go-redis/redis"
"fmt"
)

func main() {
client := redis.NewClusterClient(&redis.ClusterOptions{
Addrs: []string{"192.168.120.110:6379"},
ReadOnly: true,
RouteRandomly: true,
})

pipe := client.Pipeline()
pipe.HGetAll("1")
pipe.HGetAll("2")
pipe.HGetAll("3")
cmders, err := pipe.Exec()
if err != nil {
fmt.Println("err", err)
}
for _, cmder := range cmders {
cmd := cmder.(*redis.StringStringMapCmd)
strMap, err := cmd.Result()
if err != nil {
fmt.Println("err", err)
}
fmt.Println("strMap", strMap)
}
}

3.2 和事务结合

pipeline不保证原子性。

很多Redis客户端都会使用流水线去包裹事务命令,并将入队的命令缓存在本地,等到用户输入EXEC命令之后,再将所有事务命令通过流水线一并发送至服务器,这样客户端在执行事务时就可以达到“打包发送,打包执行”的最优效果。

4. 头脑风暴

  • redis事务有命令和lua模式,原子是全部执行或全部不执行,并不一定保证成功。

5. 参考资料

给作者打赏,可以加首页微信,咨询作者相关问题!