Redis实现分布式锁

1,203 阅读2分钟

参考网址:

https://juejin.cn/post/6844903543590092814
https://www.jianshu.com/p/533072fa4d52
https://www.cnblogs.com/zhili/p/redisdistributelock.html
https://juejin.cn/post/6844903830442737671#heading-5

1. 分布式锁常见条件

(1)互斥性。临界区任一时刻只能被一个客户端的一个线程所执行。
(2)可重入性。获得锁的线程可以重复获得锁。
(3)获取锁和释放锁必须是相同线程
(4)自动释放锁。获取锁线程崩溃没有主动释放锁,锁仍然可以被其它线程获取。

理解

  • 条件1、2、3要求锁能够记录获取锁的机器+线程
  • 条件2要求锁需要同一个线程加锁次数进行计数
  • 条件4要求锁有过期时间

2. 基于Redis实现分布式锁

2.1 基于Redis实现分布式锁设计(未支持可重入锁)

优缺点

优点:借助redis高性能的特点,实现高性能分布式锁。
缺点:

  • 加锁失败需要业务上多次重试;(zk 只需要注册监听器即可)
  • 锁过期时间难以设置。这是因为不像 zk 一样可以创建临时节点,自动删除锁,所以需要设置过期时间,防止遗漏释放锁的情况。但是设置过期时间的话,假设现在业务程序crash没能释放锁,过期时间过长的话,后续请求无法加锁;过期时间过短的话,如果前一个请求处理事件较长,仍然占用锁,就会导致多个请求同时访问资源,违背互斥性。
  • 集群模式下主从不同步可能导致多个请求获取到锁,因为redis集群不是强一致性的。

加锁逻辑

解锁逻辑

实现设计

(1)key和value设计
key = lock.{resource};
value = {machineId}_{threadId};
(2)加锁和设置超时时间动作

需要保证set动作、expire动作原子性,主要有两种做法:
i. set时同时使用ex和nx

SET key value [EX seconds] [PX milliseconds] [NX|XX]

ii. 使用lua脚本执行set和expire

(3)释放锁操作

需要保证get动作、del动作原子性,依靠lua脚本实现原子性。

3. Redisson实现分布式锁

(1)分布式锁实现(未实现可重入锁和重试机制)

public <V> V executeReadWriteLuaScript(String luaScript, RScript.ReturnType returnType, List<Object> keys,
                                      Object[] values) {
    RScript rScript = getScript();
    return rScript.eval(RScript.Mode.READ_WRITE, luaScript, returnType, keys, values);
}

public boolean lock(String resource, Object machineId, Object threadId, long expireTime) {
    String key = "lock." + resource;
    String value = machineId.toString() + "_" + threadId.toString();
    return lock(key, value, expireTime);
}

public boolean lock(String key, String value, long expireTime) {
    RBucket<String> bucket = getBucket(key);
    return bucket.trySet(value, expireTime, TimeUnit.SECONDS);
}

public void unlock(String resource, Object machineId, Object threadId) {
    String key = "lock." + resource;
    String value = machineId.toString() + "_" + threadId.toString();
    unlock(key, value);
}

public void unlock(String key, String value) {
    String luaScript = "local value = redis.call('GET', KEYS[1]); " +
            "if (value == ARGV[1]) then " +
            "    redis.call('DEL', KEYS[1]); " +
            "end ";
    executeReadWriteLuaScript(luaScript, RScript.ReturnType.VALUE,
            Lists.newArrayList(key),
            Lists.newArrayList(value).toArray());
}

(2)测试代码

@Test
public void testDistributedLock() {
    Long machineId1 = 123L;
    String resource = "resource";

    Runnable r = () -> {
        if (redisServiceWithRetry.lock(resource, machineId1, Thread.currentThread().getId(), 5)) {
            try {
                System.out.println(Thread.currentThread().getName() + " locked");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                redisServiceWithRetry.unlock(resource, machineId1, Thread.currentThread().getId());
                System.out.println(Thread.currentThread().getName() + " unlocked");
            }
        } else {
            System.out.println(Thread.currentThread().getName() + " fail to lock");
        }
    };

    Thread t1 = new Thread(r);
    Thread t2 = new Thread(r);

    t1.start();
    t2.start();

    try {
        t1.join();
        t2.join();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

结果: