目录
1、什么是分布式锁
2、引入setnx
3、引入过期时间
4、引入检验id
5、引入lua脚本
6、引入看门狗
7、redlock算法
我们在前面学习中,都有了解关于线程安全的问题,那引发这个问题的关键就是,多个线程去修改了同一个公共资源引发的“一票多卖”的问题,例如Java中就可以使用synchronized来保证线程安全。但这种方法只是在同一进程下管用,当我们引入了分布式系统后,是多个进程在工作,那多个进程在多个主机下,想要同时修改数据库中的某些公共资源而引发的问题,我们该如何解决呢?
这里就要使用到分布式锁了。其本质上就是使用一个公共的服务器来记录加锁状态。这个公共服务器可以是Redis,也可以是其他组件如MySQL/ZooKeeper,也可以是我们自己写的一个服务器~
上面这些听着云里雾里的,我们结合下图可以有一个更加清晰的思路:
不添加分布式锁:
上图中,可以观察到,三个服务器,可能会出现同时查询票数的情况,此时查出来的票数都是1,都开始进行买票操作,此时就可能会出现,票已经为0了,还在售票的情况~
添加分布式锁:
说明:
为什么引入setnx:
上面提到添加分布式锁,在里面的一个实现思路是,在Redis中添加一个键值对,如果键值对存在,则加锁成功,否则加锁失败。那么结合我们前面学习过的Redis命令中,有一个命令和这里就非常的符合:setnx--->这里的就是设置key,不存在设置成功,否则设置失败。后续操作完成,解锁则是直接将这个键值对即可~
总的来说就是三步:
上面的这个三个步骤中,也会引起一个问题:如果说加锁成功后,还没来得及解锁,服务就崩溃了,服务器直接给挂了,服务就卡这了,这时该怎么办?【这里要注意的一个点,挂掉的是业务服务器不是Redis服务器,相当于是服务器1把001车次锁住了,服务器1又挂掉了,但是Redis这边已经显示车次001还在加锁状态,所以其他服务器都没法来售卖001车次的票了...】
下面就是引入过期时间,来解决上述问题:
引入过期时间,当时间到了,key就自动被删除了。例如:设置key的过期时间为1000ms,这就意味着,即使服务器1挂掉了,1000ms到了后,这个key就被删了,其他服务器可以正常售卖车次001的车票了~
使用命令set ex nx来完成这样的设置,最好不要使用命令setnx + 命令expire ,因为Redis中多个命令之间无法保证原子性,可能会出现一个命令成功,一个命令失败了。
上述操作也可能存在问题,Redis中的key被误删了~ 为什么会出现这种情况呢? 因为对于Redis中写入的加锁键值对,其他节点是可以对其进行删除的。例如服务器1设置了key,服务器2却把这个key去给删掉了。为什么会出现服务器2去平白无故在加锁服务器上删数据呢?最常见的就是可能出bug了,导致其误删了。
下面就是引入了检验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脚本就是为了解决该问题:
我们为什么不使用事务来保证他的原子性呢?首先就是Redis的事务是可以解决上述这样的问题的,但是Redis官方有说明,更好地方案就是使用lua脚本来解决原子性问题~
使用lua脚本的原因:
刚才引入过期时间,其实还会导致一个问题,如果设置的时间不合适,可能会导致操作还没有执行完毕,时间过期了,锁释放了;或者是时间太长了就会导致锁释放不及时这样的问题。为了解决这样的问题,我们就要考虑时间续约的问题。
在Redis中,使用的“动态续约”,也就是说会有专门的一个线程来负责续约的事情,这个线程就叫做看门狗~
实现的大致场景:
我们这里使用分布式锁,是引入了一个新的Redis服务器,既然是一个服务器,我们就需要考虑到这个服务器如果挂了的情况,如果这个Redis服务器挂了,后续的业务操作没法进行加锁了,就可能会造成一些很严重的问题。
错误的解决方案:引入从节点,使用主从复制的方式,再加上哨兵节点,主节点挂了,从节点就可以自动补上了。为什么说是错误的解决方案?因为主从节点的数据同步需要时间,如果主节点过来的加锁操作,从节点还没收到数据同步,主节点解挂了~
正确的解决方案:使用redlock算法。
redlock算法简单介绍:
引入多组Redis节点。其中每一组Redis节点都包含一个主节点和若干个从节点 并且组和组之间存储数据都是一致的,相互之间是“备份”关系【和集群不同,并不是一个主节点上只存储数据的一部分】。加锁的时候,按照一定的顺序,写多个Master节点。在写锁的时候需要设定操作的“超时时间”,例如50ms,如果setnx操作超过了50ms还没成功,就视为是加锁失败了~
结合下图来看:
上图的从节点,我就不画了~
说明:
好了,本篇文章就到这里啦,上面我们只是简单了解了一下 互斥锁~