通过Redis实现分布式锁
# 通过Redis实现分布式锁
# 前言
在编程世界的初期,程序部署在一台服务器上,但是随着发展,我们的程序变得越发庞大且复杂,一台服务器的性能已经渐渐的无法承担我们的程序,于是我们开始将程序部署在不同的服务器上,也就是所谓的
分布式
,部署在多台服务器比起单独的一台有诸多优点:
- 提高可用性:当一台服务器出现故障时,其他服务器仍可以继续提供服务。
- 提高可扩展性:可以通过增加服务器数量来提高系统的处理能力和吞吐量。
- 提高系统的稳定性:当某台服务器出现问题时,其他服务器仍然可以提供服务,从而减少整体的影响
- 增强安全性:如果服务器受到攻击,不会影响其他服务器
俗话说得好,发展固然能带来便利,但也会带来新的问题。而分布式并发问题,就是其中的重中之重。
# 分布式锁跟普通锁的区别
传统的“锁”无法解决分布式中的并发问题,因为普通的锁是在同一个计算机内部使用的,它主要用于在同一进程中保护共享资源免受多个线程的竞争,它可以感知到当前计算机内部程序的运行情况,但是却无法感知其他计算机的运行情况。因此当多台计算机同时对同一个资源进行操作时,就无法保证程序的正确执行跟数据的一致性。
我们在处理分布式并发的问题时,不仅要处理并发的问题
,还需要解决多台计算机之间的通信、协调和同步问题
。
因此我们使用的分布式锁也必须具有解决上面2个问题的能力才行。其实当我们能够协调多个节点之间对共享资源的访问时(即:只有一台计算机可以访问特定资源,并且在该计算机访问完资源后,其他计算机才能访问)。分布式并发也就变成了普通的并发问题。
因此解决分布式并发的关键是,如何解决多台计算机之间的通信、协调和同步
。
# 通过Redis实现分布式锁
# 解决超卖问题
每次在进行购买操作之前添加一个操作:将商品的唯一id(或者其他属性)作为key存储到redis中,其值为该商品的数量。
执行完对应商品库存数量的删减操作后,再去redis中删除对应商品的key。
其中的关键点在于对redis的存值操作
,这里使用的是setnx,而不是普通的set命令。
setnx的特点是:当键不存在时,才能成功,若键存在,什么也不做,成功返回1,失败返回0 。 SETNX实际上就是SET IF NOT Exists的缩写。通过该命令我们实现了只允许一个服务器对共享资源的操作,只有当redis返回1时,购买操作才能继续,否则只能等待。
总结:通过redis我们建立了多个服务器之间的联系,而setnx命令则实现了对同一资源的独立处理。
# 大致流程
# 购买操作前,将订单中的每个商品的唯一id作为key 进行redis存储
# 每个key 对应的value为该商品的库存数量
#根据 redis返回进行判断 当前购买是否能进行
# 因为订单中可能存在已经卖光的订单
boolean isLock = true;
for (int i = 0; i < 商品种类.size; i++) {
Boolean ifExist = redis进行setnx操作
if(isLock){
break;
}
// 只要有一个商品不满足 则该订单则无法进行
isLock = isLock && ifAbsent;
}
if(isLock){
# 如果被锁 则等待 或者其他操作——将部分被锁的商品解锁
}else{
# 如果没有被锁 则再查询一次商品的库存,看是否能进行操作
# 库存满足 则继续进行订单的后续操作 库存减少等操作
}finally{
# 完成操作 释放对应商品的锁
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
在加锁成功后,还对库存进行一次查询,是因为在加锁的过程中,商品的数量还可能发生变化。
# 存在的问题
- 在成功加锁后,进行订单的后续操作时,当前线程出现异常,导致无法释放对应商品的锁,怎么办呢?
在通过setnx的方式对商品进行加锁时,设置过期时间。这样即使因为异常,无法主动释放锁,一到时间后也会自行释放锁。
- 在加上时间后,如果因为网络波动,可能导致某个订单执行时发生非主动释放锁的情况——订单还未执行完,还未主动释放锁,便因为时间到期,释放了锁。这样可能导致其他订单或者该订单出现超卖的情况
当前订单出现超卖的情况,因为当前订单锁被释放了,因此其他订单可以进行商品库存的扣除,从而导致到当前订单进行扣除时,出现超卖的情况
其他订单出现超卖的情况,因为当前订单A锁被释放了,其他订单B可以加锁了,在B订单进行时,A进行了锁释放,因为A-B商品相同,所以B的锁被释放了,因此B订单可能出现超卖的情况。
解决误删锁的情况:
在加锁的时候(往redis中设置key-value时),为每个商品设置唯⼀的value
,用于在后续主动释放锁时,检测释放的锁是否是当初加的锁。即:在删除redis中的key时,先获取当前商品在redis中对应的value,如果获取的值与当前value相同,则释放锁,否则不删除redis中的key。这样就能实现误删锁。
但这并没有解决其他线程重新给某个商品加锁的情况,依然有出现超卖的可能。
俗话说得好,发展的过程中会解决一些问题,但也会带来新的问题。