在单体应用程序中,多线程访问共享资源会产生并发问题。其中一个解决方案就是加锁,一次只让一个线程访问这个资源,其锁的本质就是使用一个第三方信号量标记这个资源是否正在被线程访问。synchronized就是在充当锁对象的对象头中标记锁状态,是有锁还是无锁,这个对象会关联一个monitor管程对象(监视器),这个对象就可以让JVM执行 monitorenter或monitorexit 两个指令对锁状态进行改变;ReentrantLock是通过AQS抽象队列同步器中的state同步状态来标记是否有线程持有锁。拿到锁的线程才能对共享资源操作,以此保证并发安全性。
但在分布式当中,节点都能访问共享资源存,单机锁只能保证一个节点中只有一个线程对其访问,但多个节点就会出现并发安全问题。
分布式锁就可以参考单机锁,也使用一个第三方信号量来标记这个资源状态,但是这个第三方得让这些节点都能访问到,就能使用 redis、zookeeper、mysql中存这个标记
有分布式锁还需要jvm锁吗?
分布式锁可以保证所有进程只有一个线程对该数据访问,不需要jvm锁也可以保证数据安全;但是会增加网络的IO,所以会在每个进程内使用jvm锁达到类似过滤效果,提高性能
类cas自旋式
mysql redis
一个线程访问到数据然后加上了锁,其他访问该数据的线程不断以询问的方式自旋获取锁;自旋询问时不是依靠cpu了而是网络IO,性能大大下降
event事件通知锁状态
zookeeper
一个线程访问到数据然后加上了锁,其他线程看见数据被锁就类似阻塞,等到这条数据释放锁,通知一个正在等待的线程,告知它可以访问
setnx
也就是set if not exists
:如果该key不存在才插入,会返回一个boolean值表示是否插入。
这个就可以模拟加锁操作,我要给一个资源加锁,就往redis插入一个key,key就是这个资源的标识,如果插入成功,就完成加锁,没插入进去,就代表这个资源已经被锁定,然后会自旋重新插入也就是加锁,但这个资源依靠的就不是cpu了,而是网络; 释放锁就直接将这个key删除。
如果代码出现异常了,最后的解锁动作就不会执行,所以释放锁的操作要写在 finally 中
服务宕机保证释放锁:一个节点中线程完成加锁操作,然后这个节点挂了,这个资源就会被一致锁定,于是就要给key设置过期时间(setnx ex
),超时就自动释放锁
业务代码执行慢导致锁提前释放:锁提前释放,别的线程就可以加锁访问,出现并发安全问题;并且会释放其他线程加的锁:A线程的自动释放,B线程发现没锁就加锁,A线程完成任务就把B线程加的锁释放了,又会有其他线程进来。 (UUID+判断在释放=只释放自己加的锁)
解决:使用watch dog看门狗,新建一个线程定期去检查这个业务代码是否执行完毕,如果发现下次检查时这个锁就到期了,就立即对锁续命,延长锁的时间
Redis宕机锁丢失:为了保证Redis宕机锁不会全部丢失,使用Redis主从集群保证可用性,但是Redis是AP架构,它的主从同步与主机执行命令是异步执行的,所以主机和从机间会有数据差异,导致部分锁丢失
解决:red lock
红锁算法,设置多个Redis主节点,这些节点是完全独立的,一个节点要在多半Redis节点中加锁成功,才认为他真正获取到锁。如果某个节点不可用,就立即去操作其他节点。 但是这样效率会大大降低
<dependency>
<groupId>org.redissongroupId>
<artifactId>redissonartifactId>
<version>3.12.0version>
dependency>
Redission是Redis官方推荐的客户端,提供了一个RLock的锁,RLock继承自juc的Lock接口,提供了中断,超时,尝试获取锁等操作,支持可重入,互斥等特性
RLock底层使用Redis的Hash作为存储结构,其中Hash的key用于存储锁的名字,Hash的filed用于存储客户端id,filed对应的value是线程重入次数。
底层的加锁和释放锁都是使用lua脚本进行操作的,保证隔离性。
Zookeeper的存储像一棵树,一个节点成为 Znode,Znode分四种类型:
顺序:实现公平锁,ZK可以通知编号最小的节点来获取锁,避免了通过网络自旋;
临时:可以保证客户端挂掉后,该客户端加的锁就会被释放,防止一直占用锁的情况;
可靠性高:对ZK的一个写操作,会首先同步到半数以上的节点,才会返回写入成功
因此如果某个节点存在网络问题,与ZK集群断开连接,Session超时同样会导致锁被错误释放,因此ZK也无法保证完全一致性;具有较好的稳定性;
加锁过程
对一个资源创建一个持久节点(父节点),A客户端要来拿锁,就在这个节点下创建一个临时顺序节点,同时判断是不是最靠前的一个节点,如果是就加锁成功;B客户端来加锁创建节点发现不是最靠前的节点,就向前边那个节点注册一个wather,用于监听A节点是否存在,等到A节点把锁释放,B节点就感知到然后获取锁。
在mysql里创建一个表,设置一个主键或者唯一索引,锁住key(商品ID),同一个key在mysql表里只能插入一次;加锁时就向表中插入一条数据,该数据的Id就是一把锁,等到删除这条数据释放了锁,其他线程才能加锁;这样对锁的竞争就交给了数据库;