想象以下,我们要执行以下的操作:
1 | a = GET test; |
有没有办法保证原子性呢?如果直接这样顺序执行,多线程场景下,可能会导致数据错误。
为了实现这个功能更,Redis实现了原子操作命令:DECR, INCR
1、DECR, INCR
DECR key
时间复杂度: O(1),将存储在 key 中的数字减一。如果该键不存在,则在执行操作前将其设置为 0。如果键包含错误类型的值或包含不能表示为整数的字符串,则返回错误。此操作仅限于 64 位有符号整数。
返回:递减后key的值。
INCR key
时间复杂度: O(1),将存储在 key 中的数字增加 1。如果该键不存在,则在执行操作前将其设置为 0。如果键包含错误类型的值或包含不能表示为整数的字符串,则返回错误。此操作仅限于 64 位有符号整数。
注意:这是一个字符串操作,因为 Redis 没有专用的整数类型。存储在键中的字符串被转换为64 位有符号10进制整数来执行操作。
返回:递增后key的值。
通过这两个命令,可以保证整数的原子递增和递减。
如果我们要原子执行的是多个Redis命令,那么如何实现呢。这对这种场景,可以使用Lua脚本来实现。
2、Redis中执行Lua脚本实现原子操作
我们先来举个需要原子执行多个Redis命令的例子,可能例子不是很恰当,不过足以说明没有原子性执行一批Redis命令导致的问题。
我们需要在Redis中分别记录4个商品的最后购买人,而在业务逻辑中,4个商品是批量下单更新的,更新完之后,再分别设置了商品的购买人,逻辑如下:
1 | update product set buyer = Jack where id in(1, 2, 3, 4); |
如果此时,有多个线程同时对这些商品执行了库存扣减操作,如果这几行代码不是原子性执行,那么就可能导致4个商品的最后购买人和product表里面的不一致了,如下图:
为了避免这个问题,我们可以通过Lua脚本写一个批量更新商品最近购买人的脚本:
1 | local name = ARGV[1]; |
然后直接执行即可
redis-cli -p 6379 -c --eval batch_update.lua prod:1 prod:2 prod:3 prod:4 , arthinking
最终,可以发现,4个商品的BUYER字段都同时更新了:
1 | ➜ script redis-cli -p 6379 -c --raw |
为了避免每次执行lua脚本,都需要通过网络传递脚本到Redis服务器,我们可以通过SCRIPT LOAD命令把lua脚本加载到Redis中,然后通过EVALSHA命令执行:
1 | 127.0.0.1:6379> script load 'local name = ARGV[1];for k, v in ipairs(KEYS) do redis.call("hset", v, "BUYER", name); end;' |
**SCRIPT LOAD:**将脚本加载到脚本缓存中,但不执行它,并返回脚本的SHA1摘要。除非调用SCRIPT FLUSH,否则脚本会一直存在缓存中。
**EVALSHA:**使用该命令,通过脚本的SHA1进行调用脚本。
除了Redis命令的原子操作的场景,我们面临更多的问题是,在分布式系统中,对业务代码中的一组业务需要保证原子性。这个时候,就只能使用分布式锁了。