分布式锁学习
转自: https://www.cnblogs.com/austinspark-jessylu/p/8043726.html
1.分布式锁
分布式的CAP理论告诉我们“任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。”所以,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。
针对分布式锁的实现,目前比较常用的有以下几种方案:
- 基于数据库实现分布式锁
- 基于缓存实现分布式锁
- 基于Zookeeper实现分布式锁
要求:
- 在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
- 可重入锁(避免死锁);
- 具备锁失效机制,防止死锁;
- 具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败;
- 高可用的获取锁与释放锁,高性能的获取锁与释放锁。
2.基于数据库实现
2.1 借助唯一性约束
利用数据库表键的唯一性做分布式锁,当我们要锁住某个方法或资源时,我们就在该表中增加一条记录,想要释放锁的时候就删除这条记录。
针对要锁住的方法名,添加唯一性约束,如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。正确执行insert操作,即affect rows=1的认为获取到了锁,可以执行后续的操作。释放锁使用delete删除该行即可。
存在的问题:
- 【可用性】锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用;【准备两个数据库,双向同步,一个出现故障马上切换】
- 【释放锁】锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁;【只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍】
- 【非阻塞】锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。【做一个while循环,获取锁成功之后再返回】
- 【非重入】锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。【在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。】
2.2 借助排他锁
基于MySql的InnoDB引擎,可以使用以下方法来实现加锁操作:
在查询语句后面增加for update
,数据库会在查询过程中给数据库表增加排他锁。可以认为获得排它锁的线程即可获得分布式锁,当获取到锁之后,通过以下方法解锁:
通过提交事务来解锁。存在的问题:
- 【可用性】数据库单点故障仍然无法解决;
- 【释放锁】排他锁,服务宕机之后数据库会自己把锁释放掉;
- 【非阻塞】
for update
语句会在执行成功后立即返回,在执行失败时一直处于阻塞状态,直到成功; - 【非重入】仍是非重入的。
- DB引擎不一定会对method_name使用索引,不一定使用行级排他锁。MySql会对查询进行优化,即便在条件中使用了索引字段,但是否使用索引来检索数据是由 MySQL 通过判断不同执行计划的代价来决定的,如果 MySQL 认为全表扫效率更高,比如对一些很小的表,它就不会使用索引,这种情况下 InnoDB 将使用表锁,而不是行锁。
- 要使用排他锁来进行分布式锁的lock,那么一个排他锁长时间不提交,就会占用数据库连接。一旦类似的连接变得多了,就可能把数据库连接池撑爆。
3.基于缓存实现
很多缓存是可以集群部署的,可以解决单点可用性问题。
命令:
SETNX key val //当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0 expire key timeout //为key设置一个超时时间,单位为秒,超过这个时间锁会自动释放,避免死锁 delete key //删除key
步骤:
- 获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断;
- 获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁;
- 释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。
存在的问题:
- 如果在第一步 setnx 执行成功后,在 expire() 命令执行成功前,发生了宕机的现象,那么就依然会出现死锁的问题;
- 缓存失效时间的设置,太短,方法没等执行完,锁就自动释放了,那么就会产生并发问题,太长则有其他锁等待的问题。
https://zhuanlan.zhihu.com/p/42056183介绍了优化,通过setnx()、get()和getset()做分布式锁,来解决死锁的问题,没仔细看。
//其他还有基于redlock、zookeeper实现分布式锁,都还不太了解。
4.redLock
单个节点可能会出现的问题:
如果进程A在主节点上加锁成功,然后这个主节点宕机了,则从节点将会晋升为主节点。若此时进程B在新的主节点上加锁成果,之后原主节点重启,成为了从节点,系统中将同时出现两把锁,这是违背锁的唯一性原则的。
https://juejin.cn/post/6844903688088059912,https://juejin.cn/post/7120420868513071141
需要实现多个Redis集群,然后进行红锁的加锁,解锁。具体的步骤如下:
- 这些节点相互独立,不存在主从复制或者集群协调机制;
- 加锁:以相同的KEY向N个实例加锁,只要超过一半节点成功,则认定加锁成功;
- 解锁:向所有的实例发送DEL命令,进行解锁;
RedLock基本原理是利用多个Redis集群,用多数的集群加锁成功,减少Redis某个集群出故障,造成分布式锁出现问题的概率。
分布式中会出现的问题是,两个线程都获取到了锁,比如时钟跳跃,导致锁过期时间就不是我们预期的了,也会出现client1和client2获取到同一把锁,那么也会出现不安全;以及长时间的网络IO,导致调用时间由可能比我们锁的过期时间都还长,那么也会出现不安全的问题。