前言
Redis 的事务模式具备如下特点:
- 原子性:Redis 事务中的命令可以顺序执行,只有单条命令具备原子性,整体不具备原子性,不支持回滚。
- 一致性:Redis 先检测语法错误,保证了一致性(有分歧)。
- 隔离性:Redis 以单线程的方式执行事务,按顺序串行化一条一条执行,保证了隔离性。
- 持久性:Redis 持久化策略 RDB 和 AOF 保证持久性。
事务实现
例如:当姓名为“xyc”且年龄为“20”的时候,将他们分别改为“neil”和“18”
127.0.0.1:6379> get name # 查询数据
"xyc"
127.0.0.1:6379> get age # 查询数据
"20"
# 满足修改的条件
127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> set name neil # 添加数据
QUEUED
127.0.0.1:6379> set age 18 # 添加数据
QUEUED
127.0.0.1:6379> exec # 执行事务
1) (integer) 1
2) (integer) 1
127.0.0.1:6379> get name # 查询数据
"neil"
127.0.0.1:6379> get age # 查询数据
"18"
潜在问题及规避方式:
问题1:查询判断满足修改条件后开启事务修改数据,这个过程中其他客户端对是可以 name 和 age 进行修改的。此时事务中是无感知的,最终会造成不满足修改条件的情况下也修改了。
- 乐观锁:watch 监听key,有被修改的话就执行 discard 丢弃指令,执行 exec 后不管结果如何都会 unwatch 取消监听。实现如下:
127.0.0.1:6379> watch name age OK 127.0.0.1:6379> multi OK 127.0.0.1:6379> set name xyc QUEUED 127.0.0.1:6379> set age 20 QUEUED 127.0.0.1:6379> set age 19 # 此时其他客户端对 age 进行修改成功 OK 127.0.0.1:6379> exec (nil) 127.0.0.1:6379> get name "neil" 127.0.0.1:6379> get age "19"
- 悲观锁:判断和修改数据之前加上分布式锁,将整个代码块串行化
问题2:修改姓名和修改年龄两条命令在事务中是没法保证原子性的。有可能姓名修改成功而年龄没修改成功,这样就产生脏数据了。
- 可以使用 lua 脚本保证原子性:[EVAL] [脚本内容] [key参数的数量] [key …] [arg …]
127.0.0.1:6379> eval "local name = redis.call('get', KEYS[1]) local age = redis.call('get', KEYS[2]) if name == ARGV[3] and age == ARGV[4] then redis.call('set', KEYS[1], ARGV[1]) local result = redis.call('set', KEYS[2], ARGV[2]) return result else return nil end" 2 name age neil 18 xyc 20
OK
题外话:lua 常用来保证自增和过期的原子性:对 key 自增并设置1秒过期
127.0.0.1:6379> eval "local num = redis.call('incr', KEYS[1]) if tonumber(num) == 1 then redis.call('expire', KEYS[1], ARGV[1]) return tonumber(num) else return tonumber(num) end" 1 key 1
(integer) 1
multi 与 pipeline 提交指令的差别
- 缓冲位置不同:pipeline 选择客户端缓冲,multi 选择服务端缓冲。
- 请求次数不同:multi 需要每个命令都发送一次给服务端,pipeline最后一次性发送给服务端。