为了保证数据的争用安全,通常要采用锁机制控制。

如果是单应用部署,直接通过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脚本实现分布式锁的执行流程,大致如下:

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;
    }

更多推荐