分布式锁及其三种实现方式

参考:什么是分布式锁?实现分布式锁的三种方式 - 刘清政 - 博客园

一、使用背景:

        随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的应用并不能提供分布式锁的能力。为了解决这个问题就需要一种跨机器的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!

二、分布式锁应该具备哪些条件

  1. 在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
  2. 高可用的获取锁与释放锁;
  3. 高性能的获取锁与释放锁;
  4. 具备可重入特性;
  5. 具备锁失效机制,防止死锁;
  6. 具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。

三、分布式锁的三种实现方式

        目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。分布式的CAP理论告诉我们“任何一个分布式系统都无法同时满足一致性(Consistency)可用性(Availability)分区容错性(Partition tolerance),最多只能同时满足两项。”所以,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。

  • 基于数据库实现分布式锁;
  • 基于缓存(Redis等)实现分布式锁;
  • 基于Zookeeper实现分布式锁;

1. 基于数据库的实现方式

        基于数据库的实现方式的核心思想是:在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引,想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁。

(1)创建一个表:

DROP TABLE IF EXISTS `method_lock`;
CREATE TABLE `method_lock` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `method_name` varchar(64) NOT NULL COMMENT '锁定的方法名',
  `desc` varchar(255) NOT NULL COMMENT '备注信息',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uidx_method_name` (`method_name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';

(2)想要执行某个方法,就使用这个方法名向表中插入数据:

INSERT INTO method_lock (method_name, desc) VALUES ('methodName', '测试的methodName');

        因为我们对method_name做了唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。

 (3)成功插入则获取锁,执行完成后删除对应的行数据释放锁:

DELETE FROM method_lock WHERE method_name = 'methodName';

注意:这只是使用基于数据库的一种方法,使用数据库实现分布式锁还有很多其他的玩法!
使用基于数据库的这种实现方式很简单,但是对于分布式锁应该具备的条件来说,它有一些问题需要解决及优化:

  1. 高可用、高性能并不好:因为是基于数据库实现的,数据库的可用性和性能将直接影响分布式锁的可用性及性能,所以,数据库需要双机部署、数据同步、主备切换;
  2. 不具备可重入的特性:因为同一个线程在释放锁之前,行数据一直存在,无法再次成功插入数据,所以,需要在表中新增一列,用于记录当前获取到锁的机器和线程信息,在再次获取锁的时候,先查询表中机器和线程信息是否和当前机器和线程相同,若相同则直接获取锁;
  3. 没有锁失效机制:因为有可能出现成功插入数据后,服务器宕机了,对应的数据没有被删除,当服务恢复后一直获取不到锁,所以,需要在表中新增一列,用于记录失效时间,并且需要有定时任务清除这些失效的数据;
  4. 不具备阻塞锁特性:获取不到锁直接返回失败,所以需要优化获取逻辑,循环多次去获取。
  5. 总之,在实施的过程中会遇到各种不同的问题,为了解决这些问题,实现方式将会越来越复杂;依赖数据库需要一定的资源开销,性能问题需要考虑。

2、基于Redis的实现方式 

(1)选用Redis实现分布式锁原因:

  • Redis的高性能;
  • Redis是单线程的原因,命令对此支持较好,实现较方便 

(2)使用到的Redis命令

  1. setnx key val:当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。
  2. expire key timeout:为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。
  3. delete key:删除key

(3)实现思想:

  1. 获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁。锁的value值为一个随机生成的UUID,可用于在释放锁时进行判断是否是先前获取到的那个锁(避免解锁错乱)。
  2. 获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
  3. 释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。 

1️⃣分布式锁服务中的一个问题:

        如果一个获取到锁的 client 因为某种原因导致没能及时释放锁,并且 Redis 因为超时释放了锁,另外一个 client 获取到了锁,此时情况如图:

2️⃣如何解决这个问题

        一种方案是引入锁续约机制,也就是获取锁之后,释放锁之前,会定时进行锁续约,比如以锁超时时间的 1/3 为间隔周期进行锁续约。

        使用分布式锁,安全性上和多线程(同一个进程内)加锁是没法比的,可能由于网络原因,分布式锁服务(因为超时或者认为 client 挂了)将加锁资源给删除了,如果 client 端继续操作共享资源,此时是有隐患的。因此,对于分布式锁,一个是要尽量提高分布式锁服务的可用性,另一个就是要部署同一内网,尽量降低网络问题发生几率。

3、基于ZooKeeper的实现方式

 ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。基于ZooKeeper实现分布式锁的步骤如

(1)创建一个目录mylock;
(2)线程A想获取锁就在mylock目录下创建临时顺序节点;
(3)获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;
(4)线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;
(5)线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。

这里推荐一个Apache的开源库Curator,它是一个ZooKeeper客户端,Curator提供的InterProcessMutex是分布式锁的实现,acquire方法用于获取锁,release方法用于释放锁。

优点:具备高可用可重入阻塞锁特性可解决失效死锁问题
缺点:因为需要频繁的创建和删除节点,性能上不如Redis方式

四、总结

        上面的三种实现方式,没有在所有场合都是完美的,所以,应根据不同的应用场景选择最适合的实现方式。
        在分布式环境中,对资源进行上锁有时候是很重要的,比如抢购某一资源,这时候使用分布式锁就可以很好地控制资源。
        当然,在具体使用中,还需要考虑很多因素,比如超时时间的选取获取锁时间的选取对并发量都有很大的影响,上述实现的分布式锁也只是一种简单的实现,主要是一种思想。         

1️⃣性能:Redis > Zookeeper > DB。

2️⃣避免死锁:DB 通过应用层设置定时任务来删除过期未释放的锁,Zookeeper 是通过临时节点来解决,而 Redis 通过设置超时时间来解决。

3️⃣可用性:DB 可通过数据库同步复制,vip 切换 master 来解决;Zookeeper 本身自己是通过 zab 协议集群部署来解决的;Redis 可通过集群或者 master-slave 方式来解决。注意,DB 和 Redis 的复制一般都是异步的,也就是说某些时刻分布式锁发生故障可能存在数据不一致问题,而 Zookeeper 本身通过 zab 协议保证集群内(至少 n/2+1 个)节点数据一致性。

4️⃣锁唤醒:DB 和 Redis 分布式锁一般不支持唤醒机制(也可以通过应用层自己做轮询检测锁是否空闲,空闲就唤醒内部加锁线程),Zookeeper 可通过本身的 watcher/notify 机制来做。

5️⃣使用分布式锁,安全性上和多线程(同一个进程内)加锁是没法比的,可能由于网络原因,分布式锁服务(因为超时或者认为 client 挂了)将加锁资源给删除了,如果 client 端继续操作共享资源,此时是有隐患的。因此,对于分布式锁,一个是要尽量提高分布式锁服务的可用性,另一个就是要部署同一内网,尽量降低网络问题发生几率。 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值