赞
踩
在分布式系统中,分布式锁用来解决分布式系统中多线程、多进程在不同机器上共享资源访问的问题。本文简要介绍分布式锁的四种实现机制,包括数据库、Redis缓存、Zookeeper和Etcd,以加深了解。
在单体应用中,通过锁机制实现多线程对共享资源的访问的,在分布式系统中,由于多线程、多进程是分布在不同的机器上,单机部署的并发锁控制机制已经不能满足分布式要求。分布式锁就是解决分布式系统中共享资源访问的问题,与单体应用不同的是,资源控制的最小粒度也从线程升级到了进程。
为了满足分布式系统中资源的并发访问控制,分布式锁在设计上应满足以下原则:
常见的分布式锁实现方法有几种:基于数据库通过唯一索引实现、基于缓存Redis实现、基于一致性算法Zookeeper或Etcd实现
下面将分别介绍以上几种实现方法。
基于数据库实现分布式锁的原理是使用表的唯一索引:在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引,想要执行某个方法,就是要这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁。
CREATE TABLE `distributed_lock` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`unique_method` varchar(255) NOT NULL COMMENT '锁定的方法名',
`holder_id` varchar(255) NOT NULL COMMENT '锁持有者id',
`create_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `unique_method_index` (`unique_method`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
其中id字段为自增id,unique_method字段就是防重的唯一方法名,也就是加锁的对象。在表中创建了唯一索引,保证unique_method的唯一性。
1)加锁即插入一条记录
insert into distributed_lock(unique_method, holder_id) values (‘unique_method’, ‘holder_id’);
如果当前sql执行成功代表加锁成功,如果抛出唯一索引异常(DuplicatedKeyException)则代表加锁失败,当前锁已经被其他竞争者获取。
2)解锁很简单,直接删除此条记录即可
delete from methodLock where unique_method=‘unique_method’ and holder_id=‘holder_id’;
3)数据库实现简单,操作简单,用操作数据库的方式即可实现锁,但是存在以下问题:
Redis加锁的基本实现:使用setnx、expire、delete以及LUA脚本实现
1) 使用setnx设置超时时间,锁定资源,客户端在此超时时间内完成对共享资源的访问
set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
2) 使用随机字符串做value值,预防以下情况:客户端1获取锁成功,在某个操作阻塞很久后超时,自动释放锁,客户端2拿到此资源的锁,客户端1从阻塞中恢复过来,释放客户端2的锁,所以这个值需要是随机的
3) 释放锁的操作需要使用LUA脚本,包括get、判读和del,保证操作的原子性
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
注意到这里解锁其实是分为2个步骤,涉及到解锁操作的一个原子性操作问题。这也是为什么解锁的时候用Lua脚本来实现,因为Lua脚本可以保证操作的原子性。
4) 节点宕机,在failover过程中丧失了锁的安全性。在主从架构下,客户端A从master获取到锁,在master将锁同步到slave之前主节点宕机了,slave节点升级为主节点,客户端B获取了客户端A相同的资源,但是已经A已经获取了另外一个锁,锁安全失效。
Redlock算法是在单节点Redis的基础上引入的高可用模式,基于Redlock分布式锁的实现原理如下,假设有N个Redis master节点(N为奇数),同时需要在N台服务器上面运行这些Redis实例,这样保证不会同时宕掉。为了获取到锁,客户端应该执行以下操作:
Zookeeper节点可以看成是树形结构,每个目录都被定义为一个目录节点znode,znode一共有四种类型:
Zookeeper分布式锁是基于临时顺序节点实现的。
排他锁实现机制是线程在zookeeper上创建临时有序节点,使用watch监控资源节点等待获得锁。具体流程如下:
在上述流程中,客户端通过调用create方法创建表示锁的临时节点/lock,创建成功的客户端获得了锁,同时让没有获得锁的节点在该节点上注册Watcher监听,以便实时监听到lock节点的变更情况。当当前获得锁的客户端正常执行完业务逻辑,客户端会主动删除创建的临时节点。同时,如果获得锁的客户端发生宕机或异常,那么zookeeper上这个节点就会被删除也会释放锁资源。
排他锁有一个缺点就是,如果并发量大,那么同一时刻会有很多连接对同一节点进行监听,但检测到删除事件后,zk需要通知所有的连接,所有连接收到监听后,会同一时间在发生高并发竞争,给性能带来严重损耗。多数场景下考虑使用共享锁实现:
同时为了避免锁竞争,会使用公平锁机制,将没有获得锁的线程放入队列进行排队,等锁资源释放以后,按照先进先出的算法取出一个线程尝试获取锁。
Zookeeper实现分布式锁有以下特征:
Etcd分布式锁实现流程如下所示,分为6个阶段:
1)准备阶段
客户端连接Etcd,以/lock/mylock为前缀创建全局唯一的key,假设第一个客户端对应的key为“/lock/mylock/UUIDA”,第二个客户端对应的key为“/lock/mylock/UUIDB”,第三个客户端对应的key为“/lock/mylock/UUIDC”。客户端分别为自己的key创建租约lease,租约的长度根据业务耗时确定。
2)创建定时任务作为租约的“心跳”
当客户端持有锁期间,其它客户端只能等到,为了避免等待期间租约失效,客户端需要创建一个定时任务作为心跳以保证租约的有效性。此外,如果持有锁期间客户端奔溃,心跳停止,key值也会因为租约到期而被删除,从而释放锁资源,避免死锁。
7) 客户端将自己全局唯一的key写入Etcd
客户端进行Put操作,将步骤1中创建的key值绑定租约写入Etcd,根据ETCD的revision机制,ETCD中会根据事务的操作顺序记录revision值。同时,客户端需要记录Etcd返回的revision值,用于接下来判断是否获得锁。在图中,Etcd中插入三条key-value记录,Revision分别为1/2/3,其中客户端A返回的Revision值为1。
4)客户端判断是否获得锁
客户端以前缀/lock/mylock读取key-value列表,判断自己的Revision是否为当前key-value列表中最小的,如果是则认为获得锁;否则的话,会监听key-value中前一个Revision比自己小的key的DELETE事件,一旦监听到删除事件或者因为租约到期的删除事件,则客户端获得锁资源。在图中,客户端A执行完事务,释放锁资源执行DELETE操作,客户端B即获得锁资源。
5)执行业务
客户端在获得锁资源后,执行业务逻辑。
6)获得锁
完成业务流程后对应的key释放锁。
以上介绍了分布式锁实现的几种机制,总结如下:
总结上表,对比分布式锁四种实现机制的特点:
参考资料:
转载请注明原文地址:https://blog.csdn.net/solihawk/article/details/125756140
文章会同步在公众号“牧羊人的方向”更新,感兴趣的可以关注公众号,谢谢!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。