在多个服务间保证同一时刻同一时间段内只有一个用户能获取到锁
- 基于数据库的悲观锁:X锁
- 基于数据库的乐观锁:基于版本号
- 基于Redis的分布式锁
- 基于Zookeeper的分布式锁
- 死锁问题:线程A获取到了锁,但是程序执行异常,导致锁未及时释放。可通过try catch finally来在finally中执行程序异常释放锁的问题;另外,通过
set key value px nx
仅当key不存在的时候设置key,并设置过期时间(建议毫秒)来保证程序执行超时,锁自动释放。 - 释放锁问题:线程A获取到了锁,线程A在某个操作上长时间执行,导致锁过期,自动释放;线程B获取到了这个锁;线程A执行完毕,准备释放锁,因为设置的
value
值一样,所以就释放了线程B的锁。这就要求释放锁的线程必须是加锁的线程,也就是说要给线程加标记,保证锁的唯一性,其实是区分线程,可以使用ThreadLocal+UUID来保证,使用lua脚本删除key,保证释放锁的原子操作。 - 锁丢失 - 集群下的故障转移问题:Redis在进行主从复制时是异步完成的,线程A在master获取到了锁,但是在复制数据到slave的过程中master挂了,导致这个锁没有复制到slave中;然后redis选举一个升级为master,那么这个新的master中没有线程A的那个锁,这时候其他线程是可以获取到锁的,导致互斥失效。思路:原master上有线程A的锁,现master上有线程B的锁,那怎么办呢?如果线程B是在线程A获取的锁过期后获取的,就不存在这个互斥问题,或者线程B在px毫秒之后再获取锁,也不存在互斥问题。红锁RedLock
- 多节点redis实现的分布式锁算法(RedLock): 有效防止单点故障。思路就是在线程A尝试去这N个节点拿锁,每次去一个节点拿锁的时间不能超过M毫秒,当拿到锁的个数超过总个数/2+1个,就认为拿锁成功。(简单来说是过半机制)
- 锁续期 - 如何合理设置px过期时间,太短-逻辑还没走完,就过期了,太长-造成其他线程不必要的等待。太短-可以使用锁续期(redission的watchdog),太长-finally中主动设置过期
- 互斥性:在任意时刻 ,只有一个客户端能持有锁。
- 不会发生死锁:即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁成功。
- 具有容错性:只要大部分的Redis节点运行正常,客户端就可以加锁和解锁。
- 解铃还须系铃人:加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
- 锁不能自己失效-续期策略:正常执行程序的过程中,锁不能因为某些原因失效。(控制锁的时间)
- SET EX|PX NX + 校验唯一值,再释放锁 优点:保证加锁的原子性,使用LUA脚本释放锁时,通过判断唯一值(如线程ID+时间戳),锁不会被其他线程释放 缺点:锁没有自动续期机制,锁无法支持重入
- 开源框架Redisson 优点:锁支持自动续期。只要线程一加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下,如果线程1还持有锁,那么就会不断的延长锁key的生存时间。 缺点:主从模式可能造成锁丢失
- 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;
- 数据库悲观锁X锁 排它锁,又叫写锁,又叫X锁,如果事务T对A加了X锁,则其他事务不能对A加任何类型的锁 用法 SELECT * FOR UPDATE 可利用主键唯一性来达到加锁的目的,此方法并发性能低,同时对锁的过期时间需要额外处理
- 基于数据库的乐观锁:基于版本号
- 工作线程1,获取锁,并设置了超时淘汰时长
- jvm gc垃圾回收时,会暂停工作线程,即STW
- 当工作线程1恢复工作的时候,由于STW的时长稍长,可能锁已经超时淘汰了,但是该线程还不知道,此时工作线程2去获取,也是能获取到的,导致出现多个线程获取同一个锁的异常问题 这个问题的思路不能放在解决锁的互斥性上,要解决GC。watchdog也解决不了这类问题