Redis分布式锁

136 阅读3分钟

1、redis分布式锁如何实现 使用redis的setnx,结果成功就代表加锁成功,失败就代表加锁失败

2、redis分布式锁可能遇到的问题

//加锁
redis.setNx(key,value);
System.out.println("执行业务逻辑");
// 释放锁
redis.delete(key);

上面实现了加锁,但是在业务中如果出现异常,将会导致加锁失败,所以应该在finally语句块中释放锁

try {
    //加锁
    redis.setNx(key,value);
    System.out.println("执行业务逻辑");
} finally {
    // 释放锁
    redis.delete(key);
}

上面会确保程序在发生异常时也能正常释放锁,但是如果如果加锁后,服务器宕机了,此时还没释放锁,一样会造成锁无法释放,应该给锁设置超时时间

try {
    //加锁
    redis.setNx(key,value);
    redis.expire(key,10, TimeUnit.SECONDS);
    System.out.println("执行业务逻辑");
} finally {
    // 释放锁
    redis.delete(key);
}

上面虽然设置了超时时间,但是如果刚加上锁还没设置超时时间,服务器就宕机了,依然会导致锁无法释放 ,应该保证上锁和设置超时时间的原子性,

try {
    //加锁
    redis.setNx(key,value,10, TimeUnit.SECONDS);
    System.out.println("执行业务逻辑");
} finally {
    // 释放锁
    redis.delete(key);
}

上面保证了原子性,假设现在有两个线程A,B,线程A加锁成功后已经10s了,此时A业务逻辑还没有执行完毕,这时候锁过期了,线程B获得此时进行加锁,可以正常加锁,但是线程A执行完业务逻辑后,执行了释放锁的逻辑,将B线程持有的锁删除了。那么其他线程又能持有这把锁,这样一来,锁并不能控制并发。所以,应该在删除锁前,应该删除这把锁的线程是不是给这把锁上锁的线程。

try {
    //加锁
    value = UUID.randomUUID();
    redis.setNx(key, value, 10, TimeUnit.SECONDS);
    System.out.println("执行业务逻辑");
} finally {
    // 释放锁
    if (redis.get(key) == value) {
        redis.delete(key);
    }
}

上面在删除前做了一个判断,只有value的值是本线程之前设置的值,才能删除这把锁。上面的代码需要注意,value的值不能用线程名,因为多级环境下,不同节点上的线程名是可能一样的。但是由于获取key和删除key的操作不是原子的,当线程A执行完redis.get(key) == value之后,执行redis.delete(key)之前,锁过期了,此时线程B加了锁,线程A依然可能删除线程B持有的锁,所以,判断redis.get(key) == value,redis.delete(key)这两步操作也需要时原子的,这里需要通过lua脚本来保证。

即使通过lua脚本保证了释放锁的原子性,线程A业务执行过程中,锁过期了,线程B还是能够持有这把锁,这会导致并发问题。所以有锁续命的机制,就是另起线程,判断当前线程A的锁是不是快过期了,如果快过期了,就延长锁的过期时间。

从以上的描述可以看出,分布式锁的加锁的逻辑很复杂,普通开发者自己实现很容易出错,所以可以用Redisson分布式锁,已经帮我们解决了以上的问题,且代码十分简单。

try {
    //加锁
    RLock redissonLock = redisson.getLock(key);
    redissonLock.lock();
    System.out.println("执行业务逻辑");
} finally {
    // 释放锁
    redisson.unlock();
}

image.png

以上分布式锁还可能出现问题,就是redis主节点挂了,从节点上并没有锁,然后从节点选举为主节点,之后其他线程仍然可能加锁成功。 image.png redlock 需要超过半数的redis节点加锁成功才算加锁是成功的