我觉得 Redis 的原子性是一个非常重要的话题,尤其是我们在设计高并发场景时,往往会依赖 Redis 来做分布式锁、库存扣减等操作。那么,除了 Lua 脚本,还有没有其他办法可以做到类似的效果呢?
Redis 的事务原子性
在 Redis 中,通过
MULTI
和
EXEC
关键字可以实现事务操作。
MULTI
开启事务,后续的所有命令会被放入一个队列中,直到调用
EXEC
时,Redis 才会一次性执行所有的命令。这种机制可以保证命令的顺序执行,但不能保证全局的原子性。
比如,事务中的一个命令出错,Redis 并不会回滚事务,这就意味着事务中某些命令可能已经生效,而另一些命令因为错误未能执行。这是 Redis 事务设计中的一个限制,也是开发者需要注意的地方。
让我们用一个代码示例来说明这个问题:
// 假设我们在 Redis 中有以下键值
// a:stock 是一个 String 类型值
// b:stock 是一个 Integer 类型值
// Redis CLI 命令
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> LPOP a:stock // 错误操作,a:stock 是 String 类型,LPOP 无法操作
QUEUED
127.0.0.1:6379> DECR b:stock // 减少库存
QUEUED
127.0.0.1:6379> EXEC // 执行事务
1) (error) WRONGTYPE Operation against a key holding the wrong kind of value
2) (integer) 8
从上面的例子可以看出,当
LPOP
发生错误时,
DECR
仍然被执行了。这说明 Redis 的事务并不支持自动回滚。
使用 Lua 脚本实现原子性
Lua 脚本是 Redis 中非常强大的工具,它的核心特点是能够保证脚本中的所有操作是原子性的。因为 Redis 在执行 Lua 脚本时是单线程执行的,所以整个脚本不会被其他命令打断,这就确保了原子性。
以下是一个使用 Lua 脚本扣减库存的例子:
String luaScript = "if (redis.call('GET', KEYS[1]) >= tonumber(ARGV[1])) then " +
" return redis.call('DECRBY', KEYS[1], ARGV[1]) " +
"else " +
" return -1 " +
"end";
Jedis jedis = new Jedis("localhost", 6379);
Object result = jedis.eval(luaScript, Arrays.asList("item:stock"), Arrays.asList("1"));
if (Integer.parseInt(result.toString()) >= 0) {
System.out.println("库存扣减成功,剩余库存:" + result);
} else {
System.out.println("库存不足,扣减失败");
}
Lua 脚本的执行保证了两个操作(判断库存是否足够和扣减库存)在同一事务中完成,确保了原子性。
除了 Lua 脚本,还有其他方法吗?
当然有!以下是几种常用的方式:
1、
使用 Redis 的原子命令
Redis 提供了一些原子性命令,比如
INCR
,
DECR
,
SETNX
等,这些命令本身在执行时是原子的。对于简单的场景,可以直接使用这些命令来避免使用事务或 Lua 脚本。
示例:
Jedis jedis = new Jedis("localhost", 6379);
long stock = jedis.decrBy("item:stock", 1);
if (stock >= 0) {
System.out.println("库存扣减成功,剩余库存:" + stock);
} else {
System.out.println("库存不足,扣减失败");
}
注意,这种方式适用于逻辑比较简单的场景,比如单纯的自增、自减等。
2、
分布式锁
在复杂场景中,可以借助 Redis 的分布式锁来保证操作的原子性。通过获取锁来确保同一时刻只有一个线程能够操作某些关键资源。
示例:
String lockKey = "lock:item:stock";
String lockValue = UUID.randomUUID().toString();
try (Jedis jedis = new Jedis("localhost", 6379)) {
// 获取锁,过期时间为 10 秒
if ("OK".equals(jedis.set(lockKey, lockValue, "NX", "EX", 10))) {
// 执行操作
int stock = Integer.parseInt(jedis.get("item:stock"));
if (stock > 0) {
jedis.decr("item:stock");
System.out.println("库存扣减成功");
} else {
System.out.println("库存不足");
}
} else {
System.out.println("获取锁失败,操作被其他线程占用");
}
} finally {
// 释放锁
if (lockValue.equals(jedis.get(lockKey))) {
jedis.del(lockKey);
}
}
分布式锁的核心是确保锁的获取和释放逻辑是可靠的,比如通过
SET NX EX
来保证锁的自动过期。
而最优解通常是
Lua 脚本
,因为它既能保证复杂操作的原子性,又不需要引入额外的锁机制,是 Redis 官方推荐的最佳实践。
最后,我为大家打造了一份deepseek的入门到精通教程,完全免费:
https://www.songshuhezi.com/deepseek
同时,也可以看我写的这篇文章《
DeepSeek满血复活,直接起飞!
》来进行本地搭建。