Redis分布式锁

分布式锁设计要考虑哪些

当我们在设计分布式锁的时候,我们应该考虑分布式锁至少要满足的一些条件,同时考虑如何高效地设计分布式锁,这里我认为以下几点是必须要考虑的。

互斥

在分布式高并发的条件下,我们最需要保证,同一时刻只能有一个线程获得锁,这是最基本的一点。

防止死锁

在分布式高并发的条件下,比如有个线程获得锁的同时,还没有来得及去释放锁,就因为系统故障或者其它原因使它无法执行释放锁的命令,导致其它线程都无法获得锁,造成死锁。
所以分布式非常有必要设置锁的有效时间,确保系统出现故障后,在一定时间内能够主动去释放锁,避免造成死锁的情况。

性能

对于访问量大的共享资源,需要考虑减少锁等待的时间,避免导致大量线程阻塞。

所以在锁的设计时,需要考虑两点:

  • 锁的颗粒度要尽量小。比如你要通过锁来减库存,那这个锁的名称你可以设置成是商品的ID,而不是任取名称。这样这个锁只对当前商品有效,锁的颗粒度小。
  • 锁的范围尽量要小。比如只要锁2行代码就可以解决问题的,那就不要去锁10行代码了。

可重入

我们知道ReentrantLock是可重入锁,那它的特点就是:同一个线程可以重复拿到同一个资源的锁。重入锁非常有利于资源的高效利用。

Jedis客户端实现分布式锁

jedis版本不同实现分布式锁的方式也有些不同。

redis 2.10.2

加锁代码

public class RedisTool {

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";

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

        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

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

    }

}

可以看到,我们加锁就一行代码:jedis.set(String key, String value, String nxxx, String expx, int time),这个set()方法一共有五个形参:

详情参考:Redis分布式锁的正确实现方式(Java版)

Redisson实现分布式锁

用锁的一般步骤:

(1) 获取锁实例(只是获得一把锁的引用,并不是占有锁)

(2) 通过锁实例加锁(占有了这把锁)

(3) 通过锁实例释放锁

Redisson提供很多种类型的锁,其中最常用的就是可重入锁(Reentrant Lock)了。

获取锁实例

RLock lock = redissonClient.getLock(String lockName);

获取的锁实例实现了RLock接口,而该接口扩展了JUC包中的Lock接口,以及异步锁接口RLockAsync。

通过锁实例加锁

从同步与异步特性来区分,加锁方法可分为同步加锁和异步加锁两类。异步加锁方法的名称一般是在相应的同步加锁方法后加上“Async”后缀。
从阻塞与非阻塞特性来区分,加锁方法可分为阻塞加锁和非阻塞加锁两类。非阻塞加锁方法的名称一般是“try”开头。

阻塞加锁的方法:

  • void lock(): (JUC中Lock接口定义的方法)如果当前锁可用,则加锁成功,并立即返回;如果当前锁不可用,则阻塞等待直至锁可用,然后返回。
  • void lock(long leaseTime, TimeUnit unit): 加锁机制与void lock()相同,只是增加了锁的有效(租赁)时长leaseTime。加锁成功后,可以在程序中显式调用unlock()方法进行释放;如果未显式释放,则经过leaseTime时间,该锁会自动释放。如果leaseTime传入-1,则会一直持有,直至调用unlock()。

非阻塞加锁的方法:

  • boolean tryLock(): (JUC中Lock接口定义的方法)调用该方法会立刻返回。返回值为true则表示锁可用,加锁成功;返回值为false则表示锁不可用,加锁失败。
  • boolean tryLock(long time, TimeUnit unit): (JUC中Lock接口定义的方法)如果锁可用则立刻返回true,否则最多等待time长的时间(如果time<=0,则不会等待)。在time时间内锁可用则立刻返回true,time时间之后返回false。如果在等待期间线程被其他线程中断,则会抛出nterruptedException 异常。
  • boolean tryLock(long waitTime, long leaseTime, TimeUnit unit): 与boolean tryLock(long time, TimeUnit unit)类似,只是增加了锁的使用(租赁)时长leaseTime。

通过锁实例释放锁

void unlock(): 释放锁。如果当前线程是锁的持有者(即在该锁实例上加锁成功的线程),则会释放成功,否则会抛出异常。

锁的一般编程范式

同步阻塞锁

String lockName = ...
RLock lock = redissonClient.getLock(lockName);
// 阻塞式加锁
lock.lock();
try {
    // 操作受锁保护的资源

} finally {
    // 释放锁
    lock.unlock();
}

同步非阻塞锁:

String lockName = ...
RLock lock = redissonClient.getLock(lockName);
if (lock.tryLock()) {
    try {
        // 操作受锁保护的资源
    } finally {
        lock.unlock();
    }
} else {
    // 执行其他业务操作
}

分布式锁类型

可重入锁(Reentrant Lock)

Redisson的分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.Lock接口,同时还支持自动过期解锁。

public void testReentrantLock(RedissonClient redisson){ 
    RLock lock = redisson.getLock("anyLock"); 
    try{ 
        // 1. 最常见的使用方法 
        //lock.lock(); 
        // 2. 支持过期解锁功能,10秒钟以后自动解锁, 无需调用unlock方法手动解锁 
        //lock.lock(10, TimeUnit.SECONDS); 
        // 3. 尝试加锁,最多等待3秒,上锁以后10秒自动解锁 
        boolean res = lock.tryLock(3, 10, TimeUnit.SECONDS); 
        if(res){ //成功 
            // do your business 
        } 
    } catch (InterruptedException e) { 
        e.printStackTrace(); 
    } finally { 
        lock.unlock(); 
    } 
}

公平锁(Fair Lock)

Redisson分布式可重入公平锁也是实现了java.util.concurrent.locks.Lock接口的一种RLock对象。在提供了自动过期解锁功能的同时,保证了当多个Redisson客户端线程同时请求加锁时,优先分配给先发出请求的线程。

public void testFairLock(RedissonClient redisson){ 
    RLock fairLock = redisson.getFairLock("anyLock"); 
    try{ 
        // 最常见的使用方法 
        fairLock.lock(); 
        // 支持过期解锁功能, 10秒钟以后自动解锁,无需调用unlock方法手动解锁 
        fairLock.lock(10, TimeUnit.SECONDS); 
        // 尝试加锁,最多等待100秒,上锁以后10秒自动解锁 
        boolean res = fairLock.tryLock(100, 10, TimeUnit.SECONDS); 
    } catch (InterruptedException e) { 
        e.printStackTrace(); 
    } finally { 
        fairLock.unlock(); 
    } 
}

联锁(MultiLock)

Redisson的RedissonMultiLock对象可以将多个RLock对象关联为一个联锁,每个RLock对象实例可以来自于不同的Redisson实例。

public void testMultiLock(RedissonClient redisson1,RedissonClient redisson2, RedissonClient redisson3){ 
    RLock lock1 = redisson1.getLock("lock1"); 
    RLock lock2 = redisson2.getLock("lock2"); 
    RLock lock3 = redisson3.getLock("lock3"); 
    RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3); 
    try { 
        // 同时加锁:lock1 lock2 lock3, 所有的锁都上锁成功才算成功。 
        lock.lock(); 
        // 尝试加锁,最多等待100秒,上锁以后10秒自动解锁 
        boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS); 
    } catch (InterruptedException e) { 
        e.printStackTrace(); 
    } finally { 
        lock.unlock(); 
    } 
}

红锁(RedLock)

Redisson的RedissonRedLock对象实现了Redlock介绍的加锁算法。该对象也可以用来将多个RLock对象关联为一个红锁,每个RLock对象实例可以来自于不同的Redisson实例。

public void testRedLock(RedissonClient redisson1,RedissonClient redisson2, RedissonClient redisson3){ 
    RLock lock1 = redisson1.getLock("lock1"); 
    RLock lock2 = redisson2.getLock("lock2"); 
    RLock lock3 = redisson3.getLock("lock3"); 
    RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3); 
    try { 
        // 同时加锁:lock1 lock2 lock3, 红锁在大部分节点上加锁成功就算成功。 
        lock.lock(); 
        // 尝试加锁,最多等待100秒,上锁以后10秒自动解锁 
        boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS); 
    } catch (InterruptedException e) { 
        e.printStackTrace(); 
    } finally { 
        lock.unlock(); 
    } 
}

读写锁(ReadWriteLock)

Redisson的分布式可重入读写锁RReadWriteLock,Java对象实现了java.util.concurrent.locks.ReadWriteLock接口。同时还支持自动过期解锁。该对象允许同时有多个读取锁,但是最多只能有一个写入锁。

RReadWriteLock rwlock = redisson.getLock("anyRWLock"); 
// 最常见的使用方法 
rwlock.readLock().lock(); 
// 或 
rwlock.writeLock().lock(); 
// 支持过期解锁功能 
// 10秒钟以后自动解锁 
// 无需调用unlock方法手动解锁 
rwlock.readLock().lock(10, TimeUnit.SECONDS); 
// 或 
rwlock.writeLock().lock(10, TimeUnit.SECONDS); 
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁 
boolean res = rwlock.readLock().tryLock(100, 10, TimeUnit.SECONDS); 
// 或 
boolean res = rwlock.writeLock().tryLock(100, 10, TimeUnit.SECONDS); 
... 
lock.unlock();

信号量(Semaphore)

Redisson的分布式信号量(Semaphore)Java对象RSemaphore采用了与java.util.concurrent.Semaphore相似的接口和用法。

RSemaphore semaphore = redisson.getSemaphore("semaphore"); 
semaphore.acquire(); 
//或 
semaphore.acquireAsync(); 
semaphore.acquire(23); 
semaphore.tryAcquire(); 
//或 
semaphore.tryAcquireAsync(); 
semaphore.tryAcquire(23, TimeUnit.SECONDS); 
//或 
semaphore.tryAcquireAsync(23, TimeUnit.SECONDS); 
semaphore.release(10); 
semaphore.release(); 
//或 
semaphore.releaseAsync(); 

可过期性信号量(PermitExpirableSemaphore)

Redisson的可过期性信号量(PermitExpirableSemaphore)实在RSemaphore对象的基础上,为每个信号增加了一个过期时间。每个信号可以通过独立的ID来辨识,释放时只能通过提交这个ID才能释放。

RPermitExpirableSemaphore semaphore = redisson.getPermitExpirableSemaphore("mySemaphore"); 
String permitId = semaphore.acquire(); 
// 获取一个信号,有效期只有2秒钟。 
String permitId = semaphore.acquire(2, TimeUnit.SECONDS); 
// ... 
semaphore.release(permitId); 

闭锁(CountDownLatch)

Redisson的分布式闭锁(CountDownLatch)Java对象RCountDownLatch采用了与java.util.concurrent.CountDownLatch相似的接口和用法。

RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch"); 
latch.trySetCount(1); 
latch.await(); 
// 在其他线程或其他JVM里 
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch"); 
latch.countDown(); 

 

posted @ 2022-01-17 11:48  残城碎梦  阅读(344)  评论(0编辑  收藏  举报