Skip to content

Latest commit

 

History

History
102 lines (93 loc) · 7.13 KB

分布式锁.md

File metadata and controls

102 lines (93 loc) · 7.13 KB

目录

分布式锁需求

在多个服务间保证同一时刻同一时间段内只有一个用户能获取到锁

分布式锁演进

  1. 基于数据库的悲观锁:X锁
  2. 基于数据库的乐观锁:基于版本号
  3. 基于Redis的分布式锁
  4. 基于Zookeeper的分布式锁

面临的挑战

  1. 死锁问题:线程A获取到了锁,但是程序执行异常,导致锁未及时释放。可通过try catch finally来在finally中执行程序异常释放锁的问题;另外,通过set key value px nx仅当key不存在的时候设置key,并设置过期时间(建议毫秒)来保证程序执行超时,锁自动释放。
  2. 释放锁问题:线程A获取到了锁,线程A在某个操作上长时间执行,导致锁过期,自动释放;线程B获取到了这个锁;线程A执行完毕,准备释放锁,因为设置的value值一样,所以就释放了线程B的锁。这就要求释放锁的线程必须是加锁的线程,也就是说要给线程加标记,保证锁的唯一性,其实是区分线程,可以使用ThreadLocal+UUID来保证,使用lua脚本删除key,保证释放锁的原子操作。
  3. 锁丢失 - 集群下的故障转移问题:Redis在进行主从复制时是异步完成的,线程A在master获取到了锁,但是在复制数据到slave的过程中master挂了,导致这个锁没有复制到slave中;然后redis选举一个升级为master,那么这个新的master中没有线程A的那个锁,这时候其他线程是可以获取到锁的,导致互斥失效。思路:原master上有线程A的锁,现master上有线程B的锁,那怎么办呢?如果线程B是在线程A获取的锁过期后获取的,就不存在这个互斥问题,或者线程B在px毫秒之后再获取锁,也不存在互斥问题。红锁RedLock
  4. 多节点redis实现的分布式锁算法(RedLock): 有效防止单点故障。思路就是在线程A尝试去这N个节点拿锁,每次去一个节点拿锁的时间不能超过M毫秒,当拿到锁的个数超过总个数/2+1个,就认为拿锁成功。(简单来说是过半机制)
  5. 锁续期 - 如何合理设置px过期时间,太短-逻辑还没走完,就过期了,太长-造成其他线程不必要的等待。太短-可以使用锁续期(redission的watchdog),太长-finally中主动设置过期

分布式锁的5个特性

  1. 互斥性:在任意时刻 ,只有一个客户端能持有锁。
  2. 不会发生死锁:即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁成功。
  3. 具有容错性:只要大部分的Redis节点运行正常,客户端就可以加锁和解锁。
  4. 解铃还须系铃人:加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
  5. 锁不能自己失效-续期策略:正常执行程序的过程中,锁不能因为某些原因失效。(控制锁的时间)

实现方式

  1. SET EX|PX NX + 校验唯一值,再释放锁 优点:保证加锁的原子性,使用LUA脚本释放锁时,通过判断唯一值(如线程ID+时间戳),锁不会被其他线程释放 缺点:锁没有自动续期机制,锁无法支持重入
  2. 开源框架Redisson 优点:锁支持自动续期。只要线程一加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下,如果线程1还持有锁,那么就会不断的延长锁key的生存时间。 缺点:主从模式可能造成锁丢失
  3. Redis集群实现分布式锁Redlock - 红锁 优点:锁支持自动续期,同时能有效防止锁丢失 缺点:需要多台redis机器,极端情况下会造成两个线程同时获取锁(如5个节点,线程A拿到了1,2,3的锁,过半,加锁成功,1,2宕机,线程B去拿到了4,5的锁,也过半了,此时线程A,B同时持有锁)。实际项目中很少使用Redlock,因为红锁会影响并发环境下的性能,且耗费服务器 简化的实现步骤:
    按顺序向5个master节点请求加锁
    根据设置的超时时间来判断,是不是要跳过该master节点。
    如果大于等于三个节点加锁成功,并且使用的时间小于锁的有效期,即可认定加锁成功啦。
    如果获取锁失败,解锁!

使用Redisson实现Redlock。 在Redisson框架中,实现了红锁的机制,Redisson的RedissonRedLock对象实现了Redlock介绍的加锁算法。该对象也可以用来将多个RLock对象关联为一个红锁,

每个RLock对象实例可以来自于不同的Redisson实例。当红锁中超过半数的RLock加锁成功后,才会认为加锁是成功的,这就提高了分布式锁的高可用。

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

参考代码

// 释放锁
if redis.call('get',KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1]);
else
    return 0;
end;
  1. 数据库悲观锁X锁 排它锁,又叫写锁,又叫X锁,如果事务T对A加了X锁,则其他事务不能对A加任何类型的锁 用法 SELECT * FOR UPDATE 可利用主键唯一性来达到加锁的目的,此方法并发性能低,同时对锁的过期时间需要额外处理
  2. 基于数据库的乐观锁:基于版本号

分布式锁常见问题

java gc STW (stop the word)导致的锁过期问题

  1. 工作线程1,获取锁,并设置了超时淘汰时长
  2. jvm gc垃圾回收时,会暂停工作线程,即STW
  3. 当工作线程1恢复工作的时候,由于STW的时长稍长,可能锁已经超时淘汰了,但是该线程还不知道,此时工作线程2去获取,也是能获取到的,导致出现多个线程获取同一个锁的异常问题 这个问题的思路不能放在解决锁的互斥性上,要解决GC。watchdog也解决不了这类问题

参考文章