Lua 解决 Redis 缓存原子性问题
文章目录
Redis 事务局限性
redis的缺点主要体现在并不支持完整的ACID事务,redis虽然提供事务功能,但redis的事务和关系数据库的事务不可相类比,redis的事务只能保证隔离性和⼀致性,无法保证原子性与持久性。
持久性之前说了,我们还可以通过使用Redis持久化操作将数据丢失的时间窗口限制在1秒之内。
而对于原子性来说:在Redis中单条命令是原子性,这是由 redis 单线程保障的,但是多条命令能否用 multi + exec
来保证其原子性呢?
原子性探究
Redis 中 multi + exec
并不支持回滚,例如有初始数据如下
set a 1000 set b 1000 set c a
执行
multi decr a incr b incr c exec
执行 incr c
时,由于字符串不支持自增导致此条命令失败,但之前的两条命令并不会回滚
更为重要的是,multi + exec
中的读操作没有意义,因为读的结果并不能赋值给临时变量,用于后续的写操作,既然 multi + exec
中读没有意义,就无法保证读 + 写的原子性,例如有初始数据如下
set a 1000 set b 1000
假设 a 和 b 代表的是两个账户余额,现在获取旧值,执行转账 500 的操作:
get a /* 存入客户端临时变量 */ get b /* 存入客户端临时变量 */ /* 客户端计算出 a 和 b 更新后的值 */ multi set a 500 set b 1500 exec
但如果在 get 与 multi 之间其它客户端修改了 a 或 b,会造成丢失更新
解决原子性问题
为了解决上面的问题,我们可以应用乐观锁或者lua脚本
乐观锁保证原子性
watch 命令,用来盯住 key(一到多个),如果这些 key 在事务期间:
- 没有被别的客户端修改,则 exec 才会成功
- 被别的客户端改了,则 exec 返回 nil
还是上一个例子
get a /* 存入客户端临时变量 */ get b /* 存入客户端临时变量 */ /* 客户端计算出 a 和 b 更新后的值 */ watch a b /* 盯住 a 和 b */ multi set a 500 set b 1500 exec
此时,如果其他客户端修改了 a 和 b 的值,那么 exec 就会返回 nil,并不会执行两条 set 命令,此时客户端可以进行重试
lua 脚本保证原子性
Redis 支持 lua 脚本,能保证 lua 脚本执行的原子性,可以取代 multi + exec
例如要解决上面的问题,可以执行如下命令
eval "local a = tonumber(redis.call('GET',KEYS[1]));local b = tonumber(redis.call('GET',KEYS[2]));local c = tonumber(ARGV[1]); if(a >= c) then redis.call('SET', KEYS[1], a-c); redis.call('SET', KEYS[2], b+c); return 1;else return 0; end" 2 a b 500
- eval 用来执行 lua 脚本
- 2 表示后面用空格分隔的参数中,前两个是 key,剩下的是普通参数
- 脚本中可以用 keys[n] 来引用第 n 个 key,用 argv[n] 来引用第 n 个普通参数
其中双引号内部的即为 lua 脚本,格式化如下
local a = tonumber(redis.call('GET',KEYS[1])); local b = tonumber(redis.call('GET',KEYS[2])); local c = tonumber(ARGV[1]); if(a >= c) then redis.call('SET', KEYS[1], a-c); redis.call('SET', KEYS[2], b+c); return 1; else return 0; end