使用lua实现检查和删除分布式锁的原子性
很多时候出现并发问题的根本原因在于检查和操作不是同一个操作,不具有原子性,所以中间会被其他线程插一脚。所以我们需要有一种工具保证这两种操作的原子性
lua脚本为我们提供了这种原子性。
为什么redis执行lua脚本时具有原子性?
使用lua操作redis时的注意事项
- 先在redis中测试操作的可行性
- 将redis的操作转换为lua代码并添加逻辑控制
- 在java中执行lua代码
具体实践
我们最终的操作都要集成到java代码中,所以出现了java代码 执行 lua 脚本 , lua 脚本控制redis的流程。
也就是java不直接操作redis,而是添加了一个中间件,由它代为操作redis。
但是我们编程流程则正好相反,所以我们反向编写代码。
1 redis操作
# 由我们的java代码可知,我们对redis只有以下两种操作,分别是
# 取出key所对应的线程id
# 删除对应的key
# 于是我们测试对应的命令为
get key
delete key
2 lua操作
首先要清楚当有人要执行lua脚本时,可以外部传递参数,避免硬编码的问题
// 将当前线程id作为参数传入
local threadId = ARGV[1]
// 将锁名称作为key值传入
local key = KEYS[1]
local lockId = redis.call("get" , key)
if(threadId == lockId) thenreturn redis.call("del" , key)
then
return 0
上述代码优化后为 :
if(ARGV[1]== redis.call("get" , KEYS[1])) thenreturn redis.call("del" , KEYS[1])
end
return 0
3 java代码中使用调用lua脚本操作redis
方法为 :
stringRedisTemplate.execute(LUA_SCRIPT , KEYS , ARGVS)
理解起来记忆就是 : 对于redis要进行何种操作呢 ?哦,就是进行lua脚本里的操作。
但是这个lua脚本我们不能写成硬编码的形式,也就是将lua代码直接插入到java代码中,会导致我们后期难以维护。
所以应该将lua代码单独写成一个文件,然后java代码去加载这个文件,然后执行这个脚本文件
private final static DefaultRedisScript<LONG> UNLOCK_SCRIPT;
static {UNLOCK_SCRIPT = new DefaultRedisScipt<LONG>();UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));UNLOCK_SCRIPT.setResultType(Long.class);
} // 写在static中可以增加代码的可读性,也方便控制逻辑// 在方法中直接调用stringRedisTemplate即可
stringRedisTemplate.execute(LUA_SCRIPT , Collections.singletonList(KEY_PREFIX + name),ID_PREFIX + Thread.currentThread().getId());
4 为什么使用Collections.singletonList()?
因为我们方法需要一个list类型的对象,所以我们要将string放入列表中返回。Collections.singletonList只是对这个过程进行了简化。
5.使用lua实现检查和删除分布式锁的原子性
由上可知,我们出现并发问题的根本原因在于检查和操作不是同一个操作,不具有原子性,所以中间会被其他线程插一脚。
但是lua脚本为我们提供了这种原子性。
为什么redis执行lua脚本时具有原子性?
使用lua操作redis时的注意事项
但是我们最终的操作都要集成到java代码中,所以出现了
java代码 执行 lua 脚本 , lua 脚本控制redis的流程
也就是java不直接操作redis,而是添加了一个中间件,由它代为操作redis。
但是我们编程流程则正好相反
- 先在redis中测试操作的可行性
- 将redis的操作转换为lua代码并添加逻辑控制
- 在java中执行lua代码
5.1 redis操作
# 由我们的java代码可知,我们对redis只有以下两种操作,分别是
# 取出key所对应的线程id
# 删除对应的key
# 于是我们测试对应的命令为
get key
delete key
5.2 lua操作
首先要清楚当有人要执行lua脚本时,可以外部传递参数,避免硬编码的问题
(https://img2024.cnblogs.com/blog/3489813/202503/3489813-20250303222210338-616996731.png)
// 将当前线程id作为参数传入
local threadId = ARGV[1]
// 将锁名称作为key值传入
local key = KEYS[1]
local lockId = redis.call("get" , key)
if(threadId == lockId) thenreturn redis.call("del" , key)
then
return 0
上述代码优化后为 :
if(ARGV[1]== redis.call("get" , KEYS[1])) thenreturn redis.call("del" , KEYS[1])
end
return 0
5.3 java代码中使用调用lua脚本操作redis
方法为 :
stringRedisTemplate.execute(LUA_SCRIPT , KEYS , ARGVS)
理解起来记忆就是 : 对于redis要进行何种操作呢 ?哦,就是进行lua脚本里的操作。
但是这个lua脚本我们不能写成硬编码的形式,也就是将lua代码直接插入到java代码中,会导致我们后期难以维护。
所以应该将lua代码单独写成一个文件,然后java代码去加载这个文件,然后执行这个脚本文件
private final static DefaultRedisScript<LONG> UNLOCK_SCRIPT;
static {UNLOCK_SCRIPT = new DefaultRedisScipt<LONG>();UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));UNLOCK_SCRIPT.setResultType(Long.class);
} // 写在static中可以增加代码的可读性,也方便控制逻辑// 在方法中直接调用stringRedisTemplate即可
stringRedisTemplate.execute(LUA_SCRIPT , Collections.singletonList(KEY_PREFIX + name),ID_PREFIX + Thread.currentThread().getId());
5.4 为什么使用Collections.singletonList()?
因为我们方法需要一个list类型的对象,所以我们要将string放入列表中返回。Collections.singletonList只是对这个过程进行了简化。