基于 Redis + Lua 脚本实现分布式锁,确保操作的原子性
·
为了保证数据的争用安全,通常要采用锁机制控制。
如果是单应用部署,直接通过synchronized关键字修改方法,就能解决,但是如果是分布式的部署
该方法就不能解决这个问题啦,此时就引出了一个分布式锁的概念。
常用的分布式锁的实现方式有三种:
- 基于数据库乐观锁(CAS)来实现;
- 基于 Redis 来实现;
- 基于 ZooKeeper 来实现。
本文介绍:基于 Redis 和 Lua 脚本实现分布式锁功能。
一、为什么使用Lua
Lua脚本是高并发、高性能的必备脚本语言,大部分的开源框架(如:redission)中的分布式锁组件,都是用纯lua脚本实现的。
那么,为什么要使用Lua语言来实现分布式锁呢?我们从一个案例看起:
public void wrongReleaseLock(Jedis jedis, String lockKey, String requestId) {
// 1.判断加锁与解锁是不是同一个客户端,是则解锁
if (requestId.equals(jedis.get(lockKey))) {
// 2.若在此时,这把锁突然不是这个客户端的,则会误解锁
jedis.del(lockKey);
}
}
假设:Trans1在进行equal判断后执行结束,自动释放锁并让Trans2获得锁,那么当Trans1继续执行时,就会出现误解锁的情况。
所以,只有确保判断锁和删除锁是一步操作时,才能避免上面的问题,才能确保原子性。
Lua 脚本就可以实现这样的效果,再看一段Lua代码:
@AllArgsConstructor
public class JedisCommandLock {
private static final Long RELEASE_SUCCESS = 1L;
/**
* 功能:释放分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
// Lua 脚本,将判断锁和释放锁变为一步操作
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
// eval()方法,是将Lua代码交给Redis服务端执行
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
return RELEASE_SUCCESS.equals(result);
}
}
那么这段 Lua 代码的功能是什么呢?
其实很简单,首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。虽然看似做了两件事,但是却只有一个完整的原子操作。
- 第一行代码,我们写了一个简单的 Lua 脚本代码;
- 第二行代码,我们将Lua代码传到
jedis.eval()方法里,并使参数 KEYS[1] 赋值为 lockKey,ARGV[1] 赋值为 requestId,eval() 方法是将Lua代码交给 Redis 服务端执行。
二、执行流程
加锁和删除锁的操作,使用纯 Lua 进行封装,保障其执行时候的原子性。
基于纯Lua脚本实现分布式锁的执行流程,大致如下:
1.加锁的Lua脚本: getLock.lua
--- -1 failed
--- 1 success
--- getLock key
local key = KEYS[1]
local requestId = KEYS[2]
local ttl = tonumber(KEYS[3])
local result = redis.call('setnx', key, requestId)
if result == 1 then
--PEXPIRE:以毫秒的形式指定过期时间
redis.call('pexpire', key, ttl)
else
result = -1;
-- 如果value相同,则认为是同一个线程的请求,则认为重入锁
local value = redis.call('get', key)
if (value == requestId) then
result = 1;
redis.call('pexpire', key, ttl)
end
end
-- 如果获取锁成功,则返回 1
return result
2.解锁的Lua脚本: releaseLock.lua
--- -1 failed
--- 1 success
-- releaseLock key
local key = KEYS[1]
local requestId = KEYS[2]
local value = redis.call('get', key)
if value == requestId then
redis.call('del', key);
return 1;
end
return -1
3.将资源文件放在资源文件夹下

4.Java中调用lua脚本
// 简单加锁
public static boolean getLock(String key, String requestId, String expireTime) {
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("script/getLock.lua")));
Object result = redisTemplate.execute(redisScript,argsSerializer,resultSerializer, Collections.singletonList(key),requestId,expireTime);
if(EXEC_RESULT.equals(result)) {
return true;
}
return false;
}
// 简单解锁
public static boolean releaseLock(String key, String requestId) {
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("script/releaseLock.lua")));
Object result = redisTemplate.execute(redisScript,argsSerializer,resultSerializer,Collections.singletonList(key),requestId);
if(EXEC_RESULT.equals(result)) {
return true;
}
return false;
}
更多推荐


所有评论(0)