前言

最近在工作中使用到了分布式锁,所以在本文中记录一下如何在Java中使用Redis实现分布式锁

四个特性

在使用分布式锁的时候,我们需要满足以下四个条件:

  1. 互斥性 在同一时间只能有一个客户端持有锁。
  2. 容错性 客户端可以实现加锁和解锁。
  3. 可靠性 即使在客户端崩溃后,无法主动释放锁的情况下, 也可以保证后续客户端持有该锁。
  4. 一致性 加锁和解锁需是同一客户端,必须避免当前客户端的锁被其他客户端解锁。

具体实现

加锁

直接上代码,加锁其实并不复杂。

private ThreadLocal<String> threadLocal = new ThreadLocal<>();
private static final String LOCK_SUCCESS = "OK";
private static final String LOCK_KEY = "redisLockTest";
    public void test() {
        Jedis jedis = getJedis();
        String uuid = UUID.randomUUID().toString();
        threadLocal.set(uuid);
        boolean result = tryLock(jedis,LOCK_KEY,uuid,10);
        if (result) {
            //......具体业务实现
        }
    }

    /**
     * 获取分布式锁
     *
     * @param jedis      Redis客户端
     * @param lockKey    锁
     * @param requestId  请求标识
     * @param expireTime 过期时间
     * @return 是否获取成功
     *
     */
    public static boolean tryLock(Jedis jedis, String lockKey, String requestId, int expireTime) {

        String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);

        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;

    }

具体核心代码其实就是这一行:

String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
  • 第一个参数为Key,锁的名称,并且是全局唯一。
  • 第二个参数为value,这里是使用的一串随机字符,存入当前线程本地里面。value具体的作用是要在解锁的时候,判断加锁解锁是否为一个客户端。
  • 第三个参数为“NX” ,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作。
  • 第四个参数为"PX",这个参数是过期时间单位,意思是我们要给这个key加一个过期时间,如果客户端在操作的时候突然宕机,Redis在过期后进行自动删除,避免长时间持有锁造成死锁。
  • 第五个参数为expireTime,具体的过期时间。

解锁

 private ThreadLocal<String> threadLocal = new ThreadLocal<>();
 private static final String LOCK_SUCCESS = "OK";
 private static final String LOCK_KEY = "redisLockTest";
 private static final Long RELEASE_SUCCESS = 1L;
 
 public void test() {
    try {
    Jedis jedis = getJedis();
    boolean result = releaseLock(jedis, LOCK_KEY, threadLocal.get());
    if (result) {
        //......具体业务实现
    }
    }finally {
        threadLocal.remove();
    }
}

/**
 * 释放分布式锁
 * @param jedis Redis客户端
 * @param lockKey 锁
 * @param requestId 请求标识
 * @return 是否释放成功
 */
public static boolean releaseLock(Jedis jedis, String lockKey, String requestId) {

    String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));

    if (RELEASE_SUCCESS.equals(result)) {
        return true;
    }
    return false;

}

对于解锁来说,我们需要使用lua脚本来实现解锁,因为我们需要在解锁时判断当前锁是否是当前客户端所持有,所以我们要先进行get判断key和value是否一致,如果一致我们进行删除。这个地方我们不能使用平常的redis命令来进行操作,因为我们要执行两个命令,需要保证原子性,需要使用lua脚本来保证原子性。

简单来说,就是在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。

ps:如果不明白lua脚本的相关知识请谷歌学习。

总结

其实实现一个单机Redis版的分布式锁并不是很难,只需要保证其四个特性即可。

如果文章中有错误地方,还请大家指出,共同进步。


神秘杰克
765 声望382 粉丝

Be a good developer.