目录
1、什么是分布式锁
2、引入setnx
3、引入过期时间
4、引入检验id
5、引入lua脚本
6、引入看门狗
7、redlock算法
1、什么是分布式锁
我们在前面学习中,都有了解关于线程安全的问题,那引发这个问题的关键就是,多个线程去修改了同一个公共资源引发的“一票多卖”的问题,例如Java中就可以使用synchronized来保证线程安全。但这种方法只是在同一进程下管用,当我们引入了分布式系统后,是多个进程在工作,那多个进程在多个主机下,想要同时修改数据库中的某些公共资源而引发的问题,我们该如何解决呢?
这里就要使用到分布式锁了。其本质上就是使用一个公共的服务器来记录加锁状态。这个公共服务器可以是Redis,也可以是其他组件如MySQL/ZooKeeper,也可以是我们自己写的一个服务器~
上面这些听着云里雾里的,我们结合下图可以有一个更加清晰的思路:
不添加分布式锁:
上图中,可以观察到,三个服务器,可能会出现同时查询票数的情况,此时查出来的票数都是1,都开始进行买票操作,此时就可能会出现,票已经为0了,还在售票的情况~
添加分布式锁:
说明:
- 如果服务器1尝试进行买票,就需要先访问Redis,在Redis上设置一个键值对,比如key就是车次,value设置为某个值(1)。
- 如果这个操作设置成功,就视为当前没有节点对该key001车次枷锁,就可以进行数据库的读写操作了,操作完后,再把这个Redis上刚才的这个键值对删除即可。
- 如果在服务器1操作数据库的过程中,买票服务器2也想买票,也会尝试去给Redis上写一个键值对,key同样是车次,但此时key已经存在了,则认为已经有其他服务器正在持有锁,服务器2要么选择等待,要么选择暂时放弃~
2、引入setnx
为什么引入setnx:
上面提到添加分布式锁,在里面的一个实现思路是,在Redis中添加一个键值对,如果键值对存在,则加锁成功,否则加锁失败。那么结合我们前面学习过的Redis命令中,有一个命令和这里就非常的符合:setnx--->这里的就是设置key,不存在设置成功,否则设置失败。后续操作完成,解锁则是直接将这个键值对即可~
总的来说就是三步:
- 加锁,尝试设置key-value
- 执行业务操作
- 解锁,删除key-value
上面的这个三个步骤中,也会引起一个问题:如果说加锁成功后,还没来得及解锁,服务就崩溃了,服务器直接给挂了,服务就卡这了,这时该怎么办?【这里要注意的一个点,挂掉的是业务服务器不是Redis服务器,相当于是服务器1把001车次锁住了,服务器1又挂掉了,但是Redis这边已经显示车次001还在加锁状态,所以其他服务器都没法来售卖001车次的票了...】
下面就是引入过期时间,来解决上述问题:
3、引入过期时间
引入过期时间,当时间到了,key就自动被删除了。例如:设置key的过期时间为1000ms,这就意味着,即使服务器1挂掉了,1000ms到了后,这个key就被删了,其他服务器可以正常售卖车次001的车票了~
使用命令set ex nx来完成这样的设置,最好不要使用命令setnx + 命令expire ,因为Redis中多个命令之间无法保证原子性,可能会出现一个命令成功,一个命令失败了。
上述操作也可能存在问题,Redis中的key被误删了~ 为什么会出现这种情况呢? 因为对于Redis中写入的加锁键值对,其他节点是可以对其进行删除的。例如服务器1设置了key,服务器2却把这个key去给删掉了。为什么会出现服务器2去平白无故在加锁服务器上删数据呢?最常见的就是可能出bug了,导致其误删了。
下面就是引入了检验id,来解决上述问题:
4、引入检验id
引入检验id,id添加在哪里呢?举例方法不唯一:例如我们可以设置value中,上述的value我们设为了1,现在可以修改一下,value存为服务器编号,例key:车次001,value:"服务器 1"
这样设置后,再删除key时,需要先校验当前删除的key的服务器是否是当初加锁的服务器,如果是,才能执行删除操作~
上述操作,又会出现一个问题,删除时,要先查询get,确认后再删除del,这是两个命令操作,无法保证其原子性。这两个操作无法保证原子性,为什么会带来问题呢?我们这两个操作都是在Redis加锁服务器中执行的,那一个服务器中,一个进程里,我们就需要考虑到线程安全问题。结合下图来看:
上图中,体现的意思就是,服务器1向Redis服务器发送了两个请求,都是删除key,因为服务器id是正确的,所以两个get获取并对比后,都表示都可以进行删除操作,线程A删除后,线程B再删除一次到也没问题,就担心另一个服务器刚加过来一个key,转手就被刚才第二个del给删除掉了~
因此,为了保证他的原子性,下面引入lua脚本就是为了解决该问题:
5、引入lua脚本
我们为什么不使用事务来保证他的原子性呢?首先就是Redis的事务是可以解决上述这样的问题的,但是Redis官方有说明,更好地方案就是使用lua脚本来解决原子性问题~
使用lua脚本的原因:
- lua脚本非常的轻量(实现一个lua解释器消耗的体积非常小)
- Redis执行lua脚本的过程是原子的,相当于执行一条命令一样(lua脚本中编写一些逻辑,把这个脚本上传到Redis服务器上,然后就可以让客户端来控制Redis执行上述脚本了)
- Redis官方说明,lua就是属于事务的替代方案~
6、引入看门狗
刚才引入过期时间,其实还会导致一个问题,如果设置的时间不合适,可能会导致操作还没有执行完毕,时间过期了,锁释放了;或者是时间太长了就会导致锁释放不及时这样的问题。为了解决这样的问题,我们就要考虑时间续约的问题。
在Redis中,使用的“动态续约”,也就是说会有专门的一个线程来负责续约的事情,这个线程就叫做看门狗~
实现的大致场景:
- 初始情况下,设置一个过期时间(例:1s),还剩一点时间(例:300ms)的时候,如果当前任务还没有执行完,就把过期时间再续上(1s),等到时间快到了,任务没完,又可以继续续约了~
- 如果说服务器中途崩溃了,自然就没有人续约了,此时锁就能在较短时间内被自然释放~
7、redlock算法
我们这里使用分布式锁,是引入了一个新的Redis服务器,既然是一个服务器,我们就需要考虑到这个服务器如果挂了的情况,如果这个Redis服务器挂了,后续的业务操作没法进行加锁了,就可能会造成一些很严重的问题。
错误的解决方案:引入从节点,使用主从复制的方式,再加上哨兵节点,主节点挂了,从节点就可以自动补上了。为什么说是错误的解决方案?因为主从节点的数据同步需要时间,如果主节点过来的加锁操作,从节点还没收到数据同步,主节点解挂了~
正确的解决方案:使用redlock算法。
redlock算法简单介绍:
引入多组Redis节点。其中每一组Redis节点都包含一个主节点和若干个从节点 并且组和组之间存储数据都是一致的,相互之间是“备份”关系【和集群不同,并不是一个主节点上只存储数据的一部分】。加锁的时候,按照一定的顺序,写多个Master节点。在写锁的时候需要设定操作的“超时时间”,例如50ms,如果setnx操作超过了50ms还没成功,就视为是加锁失败了~
结合下图来看:
上图的从节点,我就不画了~
说明:
- 如果给某个节点加锁失败,就立即尝试下一个节点
- 当加锁成功的节点数超过总数的一半,才视为加锁成功--》这样的话,即使有的节点挂了,也不会影响锁的正确性
- 释放锁的时候, 也需要把所有节点都进⾏解锁操作. (即使是之前超时的节点, 也要尝试解锁, 尽量保 证逻辑严密).
- Redlock 算法的核⼼就是, 加锁操作不能只写给⼀个 Redis 节点, ⽽要写个多个!! 分布式系统 中任何⼀个节点都是不可靠的. 最终的加锁成功结论是 "少数服从多数的"
好了,本篇文章就到这里啦,上面我们只是简单了解了一下 互斥锁~