简述
说到Redis,我们第一想到的功能就是可以缓存数据,除此之外,Redis因为单进程、性能高的特点,它还经常被用于做分布式锁。
锁我们都知道,在程序中的作用就是同步工具,保证共享资源在同一时刻只能被一个线程访问,Java中的锁我们都很熟悉了,像synchronized 、Lock都是我们经常使用的,但是Java的锁只能保证单机的时候有效,分布式集群环境就无能为力了,这个时候我们就需要用到分布式锁。
分布式锁,顾名思义,就是分布式项目开发中用到的锁,可以用来控制分布式系统之间同步访问共享资源,一般来说,分布式锁需要满足的特性有这么几点:
1、互斥性:在任何时刻,对于同一条数据,只有一台应用可以获取到分布式锁;
2、高可用性:在分布式场景下,一小部分服务器宕机不影响正常使用,这种情况就需要将提供分布式锁的服务以集群的方式部署;
3、防止锁超时:如果客户端没有主动释放锁,服务器会在一段时间之后自动释放锁,防止客户端宕机或者网络不可达时产生死锁;
4、独占性:加锁解锁必须由同一台服务器进行,也就是锁的持有者才可以释放锁,不能出现你加的锁,别人给你解锁了;
业界里可以实现分布式锁效果的工具很多,但操作无非这么几个:加锁、解锁、防止锁超时。
首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
- 互斥性。在任意时刻,只有一个客户端能持有锁。
- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
- 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
- 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
使用场景
分布式锁,是控制分布式系统不同进程共同访问共享资源的一种锁的实现。秒杀下单、抢红包等等业务场景,都需要用到分布式锁,我们项目中经常使用Redis作为分布式锁。
Redisson
Redisson 提供了分布式锁的封装方法,我们只需要调用 api 中的 lock()和 unlock()方法。它帮我们封装锁实现的细节和复杂度 redisson 所有指令都通过 lua 脚本执行并支持 lua 脚本原子性执行,redisson 中有一个 watchdog 的概念,翻译过来就是看门狗,它会在你获取锁之后,每隔 10 秒帮你把 key 的超时时间设为 30s,就算一直持有锁也不会出现key 过期了。“看门狗”的逻辑保证了没有死锁发生。
Redisson 这个开源组件,就提供了分布式锁的封装实现,并且也内置了一个Watch Dog 机制来对 key 做续期。
我认为 Redis 里面这种分布式锁设计已经能够解决 99%的问题了,当然如果在Redis 搭建了高可用集群的情况下出现主从切换导致 key 失效,这个问题也有可能造成多个线程抢占到同一个锁资源的情况,所以 Redis 官方也提供了一个 RedLock的解决办法,但是实现会相对复杂一些。
redis主从问题
在我看来,分布式锁应该是一个 CP 模型,而 Redis 是一个 AP 模型,所以在集群架构下由于数据的一致性问题导致极端情况下出现多个线程抢占到锁的情况很难避免。
比如分布式锁,Redisson就进行了很好的封装,主要实现了5件事情.
第一.基于Redis的发布与订阅 以及Semaphore实现了锁的等待,就是没拿到锁我可以等待一段时间。
第二.实现了可重入锁,基于Redis的Hash数据结构
第三.为了死锁,给锁添加了过期时间
第四.为了防止业务没有执行完,锁被释放,利用时间轮添加了续期机制
第五.因为Redis是属于ap模型,在发生分区容错的时候,优先保证高可用,所以会牺牲一定的数据一
致性。为了防止锁丢失,也提供了联锁的概念。
分布式锁可能存在锁过期释放,业务没执行完的问题。有些小伙伴认为,稍微把锁过期时间设置长一些就可以啦。其实我们设想一下,是否可以给获得锁的线程,开启一个定时守护线程,每隔一段时间检查锁是否还存在,存在则对锁的过期时间延长,防止锁过期提前释放。
当前开源框架Redisson就解决了这个分布式锁问题。我们一起来看下Redisson底层原理
只要线程一加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下,如果线程1还持有锁,那么就会不断的延长锁key的生存时间。因此,Redisson就是使用Redisson解决了锁过期释放,业务没执行完问题。
分布式锁可能存在锁过期释放,业务没执行完的问题。有些小伙伴认为,稍微把锁过期时间设置长一些就可以啦。其实我们设想一下,是否可以给获得锁的线程,开启一个定时守护线程,每隔一段时间检查锁是否还存在,存在则对锁的过期时间延长,防止锁过期提前释放。
只要线程一加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下,如果线程1还持有锁,那么就会不断的延长锁key的生存时间。因此,Redisson就是使用Redisson解决了锁过期释放,业务没执行完问题
Redisson分布式锁原理
(Redisson如何实现可重入、可重试、超时续约)
可重入:利用hash结构记录线程id和重入次数
可重试:利用信号量和发布订阅功能实现等待、唤醒,获取锁失败的重试机制
超时续约:利用watchDog,没隔一段时间(releaseTime/3),重置超时时间
Redisson分布式锁底层基于lua+hash数据结构实现的可重入锁
Redisson基于HashWheelTimer+递归实现了锁续期,
Redis分布式锁为什么要用红锁
红锁一般公司用不起
因为一般的公司不可能单独拿redis 集群单独做红锁
Redis分布式锁使用红锁(RedLock)的原因主要是解决在Redis集群模式下的锁问题。在Redis实现分布式锁时,如果Redis是集群的,比如1主4从,这种主从模式存在延迟问题,可能导致加锁出现问题。此时应该使用红锁方案,即在代码中不依赖于主从,将这5台机器视为平等的,在代码中依次对这5台机器去加锁,只有成功的机器数大于一半就算加锁成功,其他机器也就没必要再去操作了。由于是遍历操作这5台机器,也就不用关心有没有机器挂掉了,因为挂掉了自然算加锁失败。红锁方案要求机器数为奇数,而且从原理上来看,每一个请求都会从前往后的顺序依次去操作这些机器,而不是乱序的,也就不会出现死锁的问题。决业务没有执行完但是锁过期的场景
redisson为什么有联锁,目的是什么
联锁的目的是因为redis是属于ap模式的中间件,会存在数据丢失,那么锁就会失效。
所以联锁主要做的一件事情就是尽可能的去保证数据不丢失,加锁会加在不同的独立集群机器。当满足一半成功就成功。其实主要思想就是把鸡蛋分散到不同的篮子,降低风险。 只要不是超过一半的失败,就是成功的。
应用
public class LockTest {
private static RedissonClient redissonClient;
static {
Config config=new Config();
config.useSingleServer().setAddress("redis://192.168.100.141:6379");
redissonClient= Redisson.create(config);
}
public static void main(String[] args) throws InterruptedException {
RLock rLock=redissonClient.getLock("updateAccount");
// 最多等待100秒、上锁10s以后解锁
if(rLock.tryLock(100,10, TimeUnit.SECONDS)){
System.out.println("获取锁成功");
}
Thread.sleep(20000);
rLock.unlock();
System.out.println("解锁成功");
redissonClient.shutdown();
}
}