视频链接:https://www.bilibili.com/video/BV1vz411i782
https://www.bilibili.com/video/BV1gJ411d7Ze?p=1
面试时问分布式锁的原理、优缺点
使用锁的场景
同事满足以下三个条件,一旦三个中缺一个,都不需要上锁。
1.共享资源
2.资源共享时是互斥的
3.多任务环境
类似下面的上厕所场景。只有一个坑位(共享资源),但是有多个人想要同时使用这个坑(多任务环境);一人使用时,其他人无法使用(资源共享时是互斥的)。
基于MySQL的分布式锁
这个场景就符合上述三个条件,需要加锁。
三个线程(Thread)都在一个JVM里,JDK已经提供了许多锁。
现在变成了三个独立的JVM来买票,用JDK提供的锁,不能保证这三个独立的JVM排队,这时候需要一把外部的锁,如果这把外部的锁存在于MySQL中,就是基于MySQL的分布式锁。所谓的MySQL锁,其实就是一个字段。JVM向MYSQL中插入字段,谁插入成功,谁就抢到了锁。然后去买票,买完之后,就释放锁。没抢到锁的JVM,就每隔一段时间就去数据库里看看,这个字段(锁)还在不在。
这种实现有两个问题:
1.浪费资源。没抢到锁的JVM要时不时地去查看数据库的字段。
2.基于MySQL的分布式锁,存在死锁问题。例如,JVM1抢到了锁,然后去买票,但是在买票的过程中,JVM1宕了,就一直不会释放锁,锁一直存在,那剩下的两个JVM就永远得不到锁。
解决死锁问题的方法
新起一个监视进程,监视MySQL里面的锁,如果锁超过了已经设置的超时时间,那进行就把锁给删掉。
如果JVM1挂了,监视进行也挂了,依然会产生死锁,但这个概率很低了。可以设置两个监视进程,如下图。
但这样也带来了新的问题,1.两个监视进程的通信延迟很低;2.保证两个监视进程中的数据得一致。除此之外,超时时间的设置也是一个问题。
如上图,超时时间是1S,在JVM1买票中,发生了GC(费时间),结果监视进程判断超时,删掉了锁。GC之后,JVM1继续买票,但这时候JVM1的锁已经没了,JVM2抢到了锁,开始执行,JVM1和JVM2同时买票,可能会产生乱入锁问题,导致一票多卖。
如果,超时时间设置为100Min,在JVM1买票中,JVM1挂了,那需要很长时间锁才会被监视进程干掉。
基于Redis的分布式锁
需要设置一个有效期,但这个有效期的设置也是很难拿捏的。
基于ZooKeeper的分布式锁(重点,用的最多)
ZooKeeper基本概念
一致性:client1在ZK1创建了一个目录,那么ZK2和ZK3也会有和ZK1相同的目录,这个是ZooKeeper自己实现的。
持久目录:client1创建了一个目录,当client1和zk1的连接断开后,这个目录依然一直存在。
临时目录:client1创建了一个目录,当client1和zk1的连接断开后,这个目录消失了。客户端和ZooKeeper是通过心跳机制保持连接的,客户端每隔一段时间(心跳时间,可以设置)就会给zk发送一个心跳表明自己还活着。如果超过一段时间后,Zk没有收到心跳,那客户端就死掉了,此时会删除临时目录。
有序:全局有序唯一的序号
1 | ls dir # 查看目录 |
ZooKeeper支持事件回调机制:client1在zk1上注册了一个事件,当zk1发生删除事件时,回调client1上的回调函数。
基于ZooKeeper的分布式锁
思路:类似银行办理业务。
银行只有一个窗口,还有一台叫号机,办理业务的人先去叫号机要一个号,拿到最小号的人先去窗口办业务,办理完之后,银行里的喇叭,会喊持有下一个号的人来办理业务。
注:事件是在创建临时节点的时候,就注册好的。
基于ZooKeeper的分布式锁好处:
1.节省了资源。JVM1买票时,2和3可以做其他的事情,不需要轮训
2.通过有顺序临时节点和时间通知机制巧妙地解决了死锁问题。JVM1挂了,那门Zk上的目录就没了,回调机制使得回调JVM2上的买票函数。