掘金 后端 ( ) • 2024-05-17 13:25

Redis事务的定义

Redis的事务可以一次执行多个命令,并保证2个特性:

  1. 事务中所有的命令都会被序列化并按顺序执行,执行期间不会被其他命令打断
  2. 事务是原子操作,要么执行全部命令,要么不执行

简而言之:一个队列中,一次性、顺序性、排他性的执行一系列Redis命令。

工作原理

Redis事务相关命令

Redis使用MULTI, EXEC, DISCARD ,UNWATCH和 WATCH 命令来实现事务功能。

  • MULTI: 用于标记一个事务块的开始。
  • EXEC: 用于执行事务队列内的所有命令。
  • DISCARD: 取消事务,放弃执行事务队列内的所有命令。
  • WATCH: 命令用于标记要监视的key(可以多个),一旦其中有一个键被修改(或删除),之后的事务就不会执行。
  • UNWATCH: 取消WATCH对所有key的监视。

Redis事务使用

事务正常提交

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK

事务取消提交

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> discard
OK

事务提交命令存在语法错误(编译器错误)

开启事务后,出现sets语法错误,导致事务提交失败,k1、k2值写入失败。

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> sets k2 v2
(error) ERR unknown command 'sets'
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k1
(nil)

事务提交出现运行时错误

开启事务后,set k2命令入参数量不正确,事务提交并没有失败,事务执行时,跳过错误命令继续执行。

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> exec
1) OK
2) (error) ERR syntax error
3) OK
127.0.0.1:6379> get k1
"v1"
127.0.0.1:6379> get k2
(nil)
127.0.0.1:6379> get k3
"v3"

watch监视数据

watch监视数据的数据发生变化后,事务回滚,队列中命令不生效。

127.0.0.1:6379> watch k1
OK
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v11
QUEUED
127.0.0.1:6379> set k2 v22
QUEUED
127.0.0.1:6379> exec
(nil)
127.0.0.1:6379> get k1
"v1"
127.0.0.1:6379> get k2
(nil)

unwatch取消监视,逻辑类似从multi开始处理,事务不受影响。

127.0.0.1:6379> watch k1
OK
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> unwatch
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v11
QUEUED
127.0.0.1:6379> set k2 v22
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
127.0.0.1:6379> get k1
"v11"
127.0.0.1:6379> get k2
"v22"

细说watch命令的使用场景及原理

使用场景:

假设我们在Redis有一个标志位,根据查得的不同标志位,进行不同的逻辑处理。如果标志位发生改变,程序应该感知到变化,并且做重试或者失败处理。(例如分布式锁、限流器等)。

实现原理:

db-redis-trans-2.png

事务深入理解

为什么Redis不支持回滚?

Redis事务特殊的地方在于事务中的命令允许失败,但是Redis会继续执行其它的命令而不是回滚所有命令。这么做的原因有两点:

  1. Redis 命令只在两种情况失败

    • 语法错误的时候才失败(在命令输入的时候不检查语法)。
    • 要执行的key数据类型不匹配:这种错误实际上是编程错误,这应该在开发阶段被测试出来,而不是生产上。
  2. 不考虑回滚,实现简单并高效。

Redis事务VS数据库事务

原子性

Redis的事务不保证原子性,也就是不保证所有指令同时成功或同时失败,只有决定是否开始执行全部指令的能力,没有执行到一半进行回滚的能力。

一致性

redis事务可以保证命令失败的情况下得以回滚,数据能恢复到没有执行之前的样子。但是命令运行时异常的情况,会被丢弃,不会回滚。

隔离性

Redis会保证一个事务内的命令依次执行,而不会被其它命令插入。

持久性

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

Lua脚本

前面讲到watch例子进行扩充

  1. 第一步,redis中查询k1 得到v1
  2. 第二步,代码操作v1
  3. 第三步,redis赋值 k1 newV1

看上去很简单的逻辑,但是存在严重的并发问题,第三步赋值的时候,如果v1的值被其他线程修改了,第三步赋值结果 就不一定正确了!!!

由此Redis引入了Lua脚本支持:

向服务器发送 lua 脚本来执行连续逻辑操作,获取脚本的响应数据。并且Redis服务器会单线程原子性执行 lua 脚本,保证 lua 脚本在处理的过程中不会被其它请求打断。

Lua脚本的基本语法可参考:菜鸟教程

Redis执行Lua脚本

命令不多,就下面这几个:

  • EVAL
  • EVALSHA
  • SCRIPT LOAD - SCRIPT EXISTS
  • SCRIPT FLUSH
  • SCRIPT KILL

执行LUA脚本

  • 参数1: EVAL 执行命令
  • 参数2: "xxx" lua脚本字符串
  • 参数3: redis key的个数 key后面是value个数
127.0.0.1:6379> eval "return 'hello world!'" 0
"hello world!"
127.0.0.1:6379> eval "return  {KEYS[1],ARGV[1]}" 1 key value
1) "key"
2) "value"
127.0.0.1:6379> eval "redis.call('SET',KEYS[1], ARGV[1]); redis.call('EXPIRE',KEYS[1],ARGV[2]); return 1" 1 k1 10 60
(integer) 1

Lua脚本缓存

SCRIPT LOAD 将脚本 script 添加到Redis服务器的脚本缓存中,并返回给定脚本的 SHA1 校验和。

EVALSHA使用脚本的 SHA1 校验和来调用这个脚本。

SCRIPT EXISTS使用脚本的 SHA1 校验和来判断脚本是否存在。

SCRIPT FLUSH清除Redis服务端所有 Lua 脚本缓存。

SCRIPT KILLS杀死当前正在运行的 Lua 脚本,当且仅当这个脚本没有执行过任何写操作时,这个命令才生效。

127.0.0.1:6379> SCRIPT LOAD "redis.call('SET', KEYS[1], ARGV[1]);redis.call('EXPIRE', KEYS[1], ARGV[2]); return 1;"
"6aeea4b3e96171ef835a78178fceadf1a5dbe345"
127.0.0.1:6379> EVALSHA 6aeea4b3e96171ef835a78178fceadf1a5dbe345 1 k2 10 60
(integer) 1
127.0.0.1:6379> SCRIPT EXISTS 6aeea4b3e96171ef835a78178fceadf1a5dbe345
1) (integer) 1
127.0.0.1:6379> SCRIPT FLUSH
OK
127.0.0.1:6379> SCRIPT EXISTS 6aeea4b3e96171ef835a78178fceadf1a5dbe345
1) (integer) 0

总结:

  1. Lua脚本主要用于实现Redis命令无法实现的场景(例如多命令原子操作)
  2. Redis使用Lua脚本的几个优点:减少网络开销、原子性、复用

最后

本文是Redis系列的第四篇,这个系列会系统全面的梳理Redis的知识体系,如果有遗漏或者错误,欢迎留言沟通交流。

如果你都看到这了,在文章左侧有个点赞按钮,点赞支持弥金吧。