Redis

洞悉Redis技术内幕:缓存,数据结构,并发,集群与算法
帅旋
关注
充电
IT宅站长,技术博主,共享单车手,全网id:arthinking。

Redis应用之原子操作

发布于 2021-06-16 | 更新于 2024-03-03

想象以下,我们要执行以下的操作:

1
2
3
a = GET test;
a ++;
SET test a

有没有办法保证原子性呢?如果直接这样顺序执行,多线程场景下,可能会导致数据错误。

为了实现这个功能更,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
2
3
4
5
update product set buyer = Jack where id in(1, 2, 3, 4);
HSET prod:1 BUYER Jack;
HSET prod:2 BUYER Jack;
HSET prod:3 BUYER Jack;
HSET prod:4 BUYER Jack;

如果此时,有多个线程同时对这些商品执行了库存扣减操作,如果这几行代码不是原子性执行,那么就可能导致4个商品的最后购买人和product表里面的不一致了,如下图:

image-20211010140104660

为了避免这个问题,我们可以通过Lua脚本写一个批量更新商品最近购买人的脚本:

1
2
3
4
local name = ARGV[1];
for k, v in ipairs(KEYS) do
redis.call("hset", v, "BUYER", name);
end;

然后直接执行即可

redis-cli -p 6379 -c --eval batch_update.lua prod:1 prod:2 prod:3 prod:4 , arthinking

最终,可以发现,4个商品的BUYER字段都同时更新了:

1
2
3
4
5
6
7
8
9
➜  script redis-cli -p 6379 -c --raw
127.0.0.1:6379> HGET prod:1 BUYER
arthinking
127.0.0.1:6379> HGET prod:2 BUYER
arthinking
127.0.0.1:6379> HGET prod:3 BUYER
arthinking
127.0.0.1:6379> HGET prod:4 BUYER
arthinking

为了避免每次执行lua脚本,都需要通过网络传递脚本到Redis服务器,我们可以通过SCRIPT LOAD命令把lua脚本加载到Redis中,然后通过EVALSHA命令执行:

1
2
3
4
5
6
7
8
9
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;'
526b107ee608d0695e33f34f9358d5a18858400d

127.0.0.1:6379> EVALSHA 526b107ee608d0695e33f34f9358d5a18858400d 2 prod:1 prod:2 itzhai

127.0.0.1:6379> hget prod:1 BUYER
itzhai
127.0.0.1:6379> hget prod:2 BUYER
itzhai

**SCRIPT LOAD:**将脚本加载到脚本缓存中,但不执行它,并返回脚本的SHA1摘要。除非调用SCRIPT FLUSH,否则脚本会一直存在缓存中。

**EVALSHA:**使用该命令,通过脚本的SHA1进行调用脚本。

除了Redis命令的原子操作的场景,我们面临更多的问题是,在分布式系统中,对业务代码中的一组业务需要保证原子性。这个时候,就只能使用分布式锁了。

References

本文作者: 帅旋

本文链接: https://www.itzhai.com/columns/redis/atomic-operations.html

版权声明: 版权归作者所有,未经许可不得转载,侵权必究!联系作者请加公众号。

×
IT宅

关注公众号及时获取网站内容更新。