分布式锁介绍
在实际开发场景中,我们在完成基本crud等功能后,往往还需要考虑到线程并发等问题,特别是在大企业中,高并发的场景更是层出不穷,今天我们来探讨大厂中应用非常广泛的一类锁叫做分布式锁。
不同于我们在juc中学习到的synchronized关键字、Reentrantlock(lock,unlock)可重入锁等,他们仅仅需要通过给某个对象上锁便可以实现对于多线程并发问题的解决,分布式锁要解决的核心还是多台服务器之间的对于某一个公共资源的争夺,下图便很好展现了分布式锁与普通juc锁的区别:
如果对于这个还是不能理解,我们再看几个例子就能更好地理解了
应用场景
1、优惠券超卖问题
假如你是商场工作人员,你在网站上发放了一些优惠券,这时用户会进行抢购,你希望一个用户只能抢到一张优惠券,但是如果有用户同时在手机和电脑上抢优惠券,这时由于普通锁只能解决多线程问题的局限性,这个用户就会抢到好几张优惠券,导致超卖
2、缓存预热更新问题
加入你是一个网站的维护人员,每天12点你需要对网站数据进行更新维护,为了提高网站访问性能,你把更新内容存储到缓存中,并在多台服务器上部署了更新任务,但是你只希望一台服务器完成缓存预热问题,但是由于没有设置锁,使得多台服务器都在执行预热功能,使得网站那时候效率低下
3、数据幂等性问题
幂等性定义为:用户对于同一个按钮或者功能使用多次,一直返回一样的结果
其实这种例子非常常见,假设你是一个队伍的队长,有人在网站上看到了你的队伍信息要加入队伍,按了多次加入队伍,但是由于没有加锁导致这个人同时加入了三次,使得你的战队少了几个人,这当然不是你希望发生的
使用分布式锁
综上所述,为了解决以上问题,我们应该使用分布式锁来解决因为多个设备系统的同时操作导致的情况,那么我们又如何使用分布式锁呢,分布式锁的实现场景有很多,大多使用场景下包括以下几种:
(1)基于数据库
使用数据库的唯一索引来实现锁,通过插入记录来获取锁,适用于简单场景但可能由于数据库的查询较慢或者建立连接成为性能瓶颈
(2)基于ZooKeeper
使用ZooKeeper的临时顺序节点来实现锁,具有高可用性和一致性,但实现较为复杂
(3)基于Etcd
Etcd作为一个分布式键值存储系统,提供了强一致性和高可用性,适用于实现分布式锁
(4)基于Redis
利用Redis的原子命令如SETNX来实现锁,具有高性能,但需要注意锁的超时和续期问题 。
而现在在企业中最经常使用的便是基于Redis实现的分布式锁,为什么我们会使用Redis实现分布式锁呢?
使用setnx创建分布式锁
Redis中有一个设置键值对的命令叫setnx,翻译就是当且仅当key不存在的时候,将key的值设为value,但是如果key已经存在了,将不会做任何操作。
因此通过setnx可以确保锁只有一个服务器可以获取到,其他的服务器再次设置key值时,发现key值已经设置过了,获取失败就会陷入等待,直到获取到锁的线程释放key值才能再次获取,但是,这样做真的好吗?
这时,我们就要拿出评判一个锁的几个标准了:
1、避免锁滥用,减少不必要的锁操作。
如果当前获取到锁的主机挂掉了,导致这个key值没有办法释放掉,后续的其他服务器没有办法获取到当前锁,就会陷入死锁中
2、引入超时机制,避免死锁。
针对上面的情况,我们可以在设置key值时便加上过期时间
指令格式:set <lock.key> <lock.value> nx ex <expireTime>
这样就可以保证即使拿到锁的主机宕机了,后面的其他服务器也能够拿到当前锁
3、使用心跳机制,确保锁的持有者能够续期。
但是如果当前主机执行任务的时间大于设置的过期时间又怎么办呢,这时如果其他服务器想要获取锁,就要判断获取到锁的主机任务是否完成,如果没有完成就要随时更新过期时间,也就是所谓的心跳机制
在执行判断 更新key值等操作时,还需要考虑到主机本环境中的多线程安全,这里可以使用lua脚本完成,lua脚本确保了代码执行的原子性
上述方案显然可以完成分布式环境下的安全问题,但是现在还有几个疑问:
(1)如果当前主机想要再次获取锁执行任务,是否能够满足可重入?
也许我们想到了可以在设置value值时存储当前主机对应的唯一标识,例如存储的时间,可以通过判断缓存的时间与value值是否一样来判断,但是如果当前主机的某个任务执行完毕了,直接将key值释放了,那么其他的客户端任务不就会直接终止了,导致危害更大吗?
(2)其他未获取到锁的服务器一直等待,是否会一直消耗cpu资源
如果其他服务器一直等待消耗cpu资源,势必会使得其他服务效率下降,能否以一种消息队列的形式来实现通知,即持有锁的主机释放锁后立即通知其他服务器来获取锁
(3)能否有一种更好的续期方式,能够实现自动续期?
一直通过在应用程序中来判断续期可用性不高,能否有一种自动续期机制满足这一点?
接下来我们就要请出分布式锁的主角:Redisson
使用Redisson实现分布式锁
Redisson实现上述需求主要通过以下几个特性:
1、可重入机制
Redis采用hash结构存储了键值对应value与count。当同一个客户端向同一个服务器发送请求后,会判断当前是否有该对象,如果没有就创建,如果有就将count值加一。在释放该键时,会将count值减1,直到对应的count值完全为0时才会将该键释放。
同时这个机制也是为什么Redisson使用hash结构不用string结构实现分布式锁面试题的答案之一
2、可重试机制
在调用Redis的tryLock方法时,可以向其中传入ttl(重试时间参数)。如果不传该参数,默认为-1,即请求一次,如果获取锁失败就直接返回。这样在获取锁失败后,会再次进行获取锁的操作,但该操作并不是立即进行。该函数应用了publish-subscriber模式,当前拿到该锁的进程在执行完毕后,会发布一个标志信息到Redis中,而其他进程获取锁时,会判断从Redis中获取的标志信息,如果是执行完毕才会进行再次申请,这样能够减少对CPU的占用。
3、超时释放(看门狗)机制
在tryLock时,可以传入期望的释放时间(一般不传)。如果不传入,Redis中有类似的watchDog(看门狗)机制,它会设置一个初始的释放时间一般为30秒,每到1/3个释放时间即10秒时,便进行释放时间的更新,以此来保证键值的永远存在。而在释放该锁时,会通知该Redis对象从而解除看门狗,从而释放该锁。
4、主从一致性
在Redis中,如果主节点接收到信息向子节点传递信息时,服务器发生了宕机,会导致主从不一致的问题。可以采用锁连环的操作,将多个锁同时加入Redis中,并在更新数据的时候同时对他们进行更新。
附上图片更好理解:
以上便是关于分布式锁的全部内容了,如果想更深刻地了解分布式锁并应用于开发中,还是推荐小伙伴们实际动手实现以下超卖、缓存预热、幂等一致性等问题,这样能理解得更加深刻噢
看到这里了劳烦给我点个赞吧,谢谢你啦!!!