配色: 字号:
分布式锁的几种典型实现
2022-07-28 | 阅:  转:  |  分享 
  
分布式锁的几种典型实现编者荐语:本文主要介绍基于数据库、Redis和Zookeeper三种分布式锁的实现方法。详细介绍了在现实场景中如何选择
和使用合适的分布式锁。以下文章来源于58技术?,作者林洪优58技术58官方技术号,58技术创新、分享与交流平台。基于数据库实现分布
式锁1.?基于数据库表实现CREATE?TABLE?`t_ms_lock`?(????`id`?int(11)?NOT?NULL?
AUTO_INCREMENT?COMMENT?''主键'',????`name`?varchar(64)?NOT?NULL?DEFAU
LT?''''?COMMENT?''锁定的方法名'',????`desc`?varchar(1024)?NOT?NULL?DEFAULT?
''描述'',????`update_time`?timestamp?NOT?NULL?DEFAULT?CURRENT_TIMESTA
MP?ON?UPDATE?CURRENT_TIMESTAMP?COMMENT?''保存数据时间'',????PRIMARY?KEY?(
`id`),????UNIQUE?KEY?`uidx_name`?(`name?`)?USING?BTREE??)?ENGINE=
InnoDB?DEFAULT?CHARSET=utf8?COMMENT=''锁定中的方法'';当我们想要锁住某个方法时,执行以下SQL
:insert?into?t_ms_lock(name,desc)?values?(''name'',''desc'');因为我们对nam
e做了唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获
得了该方法的锁,可以执行方法体内容。当方法执行完毕之后,想要释放锁的话,需要执行以下Sql:delete?from?t_ms_lo
ck?where?name?=''name'';上面这种简单的实现有以下几个问题:这把锁强依赖数据库的可用性,数据库是一个单点,一旦数
据库挂掉,会导致业务系统不可用;这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁;这把锁
只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触
发获得锁操作;这把锁是不可重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。当然,我们也可以有其他方式
解决上面的问题:数据库是单点?搞两个数据库,数据之前双向同步。一旦挂掉快速切换到备库上;没有失效时间?只要做一个定时任务,每隔一定
时间把数据库中的超时数据清理一遍;非阻塞的?搞一个while循环,直到insert成功再返回成功;非重入的?在数据库表中加个字段,
记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话
,直接把锁分配给他就可以了。2.?基于数据库排他锁除了可以通过增删操作数据表中的记录以外,其实还可以借助数据中自带的锁来实现分布式
的锁。基于MySql的InnoDB引擎,可以使用以下方法来实现加锁操作:public?boolean?lock(){??????c
onnection.setAutoCommit(false)??????while(true){?result?=?select?
?from?t_ms_lock?where?name=xxx?for?update;??????????if(result==n
ull){??????????????return?true;??????????}??????}??????return?fal
se;??}在查询语句后面增加forupdate,数据库会在查询过程中给数据库表增加排他锁。当某条记录被加上排他锁之后,其他线程无
法再在该行记录上增加排他锁。(这里再多提一句,InnoDB引擎在加锁的时候,只有通过索引进行检索的时候才会使用行级锁,否则会使用表
级锁。这里我们希望使用行级锁,就要给method_name添加索引,值得注意的是,这个索引一定要创建成唯一索引,否则会出现多个重载
方法之间无法同时被访问的问题。重载方法的话建议把参数类型也加上)我们可以认为获得排它锁的线程即可获得分布式锁,当获取到锁之后,可以
执行方法的业务逻辑,执行完方法之后,再通过以下方法解锁:public?void?unlock(){????????connecti
on.commit();????}这里还可能存在另外一个问题,虽然我们对name?使用了唯一索引,并且显示使用for?update
来使用行级锁。但是,MySql会对查询进行优化,即便在条件中使用了索引字段,但是否使用索引来检索数据是由MySQL?通过判断不同执
行计划的代价来决定的,如果?MySQL?认为全表扫效率更高,比如对一些很小的表,它就不会使用索引,这种情况下?InnoDB?将使用
表锁,而不是行锁。如果发生这种情况就悲剧了。。。还有一个问题,就是我们要使用排他锁来进行分布式锁的lock,那么一个排他锁长时间不
提交,就会占用数据库连接。一旦类似的连接变得多了,就可能把数据库连接池撑爆。3.?基于数据库的乐观锁大多数是基于数据版本(vers
ion)的记录机制实现的。何谓数据版本号?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表添加一个“
version”字段来实现读取出数据时,将此版本号一同读出,之后更新时,对此版本号加1。在更新过程中,会对版本号进行比较,如果是一
致的,没有发生改变,则会成功执行本次操作;如果版本号不一致,则会更新失败。基于Redis实现分布式锁1.?Reids分布式锁简单实
现分布式锁使用Redis做分布式锁的思路大概是这样的:在redis中设置一个值表示加了锁,然后释放锁的时候就把这个key删除。具体
代码是这样的://?获取锁??//?NX是指如果key不存在就成功,key存在返回false,PX可以指定过期时间???????S
ET?d_lock?unique_value?NX?PX?30000//?释放锁:通过执行一段lua脚本?//?释放锁涉及到两条指
令,这两条指令不是原子性的??//?需要用到redis的lua脚本支持特性,redis执行lua脚本是原子性的???if?redi
s.call("get",KEYS[1])?==?ARGV[1]?then????return?redis.call("del",
KEYS[1])????else????return?0end这种方式有两大要点:1)一定要用SET?key?value?NX?P
Xmilliseconds?命令如果不用,先设置了值,再设置过期时间,这个不是原子性操作,有可能在设置过期时间之前宕机,会造成死锁
(key永久存在)。?2)value要具有唯一性这个是为了在解锁的时候,需要验证value是和加锁的一致才删除key。这是避免了一
种情况:假设A获取了锁,过期时间30s,此时35s之后,锁已经自动释放了,A去释放锁,但是此时可能B获取了锁。A客户端就不能删除B
的锁了。除了要考虑客户端要怎么实现分布式锁之外,还需要考虑redis的部署问题。?redis?有3种部署方式:单机模式master
-slave?+?sentinel选举模式redis?cluster模式使用redis做分布式锁的缺点在于:如果采用单机部署模式,
会存在单点问题,只要redis故障了。加锁就不行了。采用master-slave模式,加锁的时候只对一个节点加锁,即便通过sent
inel做了高可用,但是如果master节点故障了,发生主从切换,此时就会有可能出现锁丢失的问题。基于以上的考虑,其实redis的
作者也考虑到这个问题,他提出了一个RedLock的算法,这个算法的意思大概是这样的:假设redis的部署模式是redis?clus
ter,总共有5个master节点,通过以下步骤获取一把锁:1.??????获取当前时间戳,单位是毫秒;2.??????轮流尝试在
每个master节点上创建锁,过期时间设置较短,一般就几十毫秒;3.??????尝试在大多数节点上建立一个锁,比如5个节点就要求是
3个节点(n?/?2+1);4.??????客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了;5.?????
?要是锁建立失败了,那么就依次删除这个锁;6.??????只要别人建立了一把分布式锁,你就得不断轮询去尝试获取锁。但是这样的这种算
法还是颇具争议的,可能还会存在不少的问题,无法保证加锁的过程一定正确。2.?RedissionJavaer都知道Jedis,Jed
is是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支持。Redission也是Redis的客户端,相
比于Jedis功能简单。Jedis简单使用阻塞的I/O和redis交互,Redission通过Netty支持非阻塞I/O。Redi
ssion封装了锁的实现,其继承了java.util.concurrent.locks.Lock的接口,让我们像操作我们的本地Lo
ck一样去操作Redission的Lock,下面介绍一下其如何实现分布式锁。如果自己写代码来通过redis设置一个值,是通过下面这
个命令设置的。SET?d_lock?unique_value?NX?PX?30000这里设置的超时时间是30s,假如我超过30s都
还没有完成业务逻辑的情况下,key会过期,其他线程有可能会获取到锁。这样一来的话,第一个线程还没执行完业务逻辑,第二个线程进来了也
会出现线程安全问题。所以我们还需要额外的去维护这个过期时间,太麻烦了~我们来看看redisson是怎么实现的:?Config?co
nfig?=?new?Config();???????config.useClusterServers()????????.add
NodeAddress("redis://192.168.1.101:7001")????????.addNodeAddress(
"redis://192.168.1.101:7002")????????.addNodeAddress("redis://192
.168.1.101:7003")????????.addNodeAddress("redis://192.168.1.102:7
001")????????.addNodeAddress("redis://192.168.1.102:7002")???????
?.addNodeAddress("redis://192.168.1.102:7003");????????RedissonCl
ient?redisson?=?Redisson.create(config);????????RLock?lock?=?redi
sson.getLock("d_lock");????????lock.lock();????????lock.unlock();
我们只需要通过它的api中的lock和unlock即可完成分布式锁,他帮我们考虑了很多细节:redisson所有指令都通过lua脚
本执行,redis支持lua脚本原子性执行redisson设置一个key的默认过期时间为30s,如果某个客户端持有一个锁超过了30
s怎么办?redisson中有一个watchdog的概念,翻译过来就是看门狗,它会在你获取锁之后,每隔10秒帮你把key的超时时间
设为30s这样的话,就算一直持有锁也不会出现key过期了,其他线程获取到锁的问题了。redisson的“看门狗”逻辑保证了没有死锁
发生(如果机器宕机了,看门狗也就没了。此时就不会延长key的过期时间,到了30s之后就会自动过期了,其他线程可以获取到锁)3.?R
edis小结优点:?对于Redis实现简单,性能对比ZK和Mysql较好。如果不需要特别复杂的要求,那么自己就可以利用setNx进
行实现,如果自己需要复杂的需求的话那么可以利用或者借鉴Redission。对于一些要求比较严格的场景来说的话可以使用RedLock
。缺点:?需要维护Redis集群,如果要实现RedLock那么需要维护更多的集群。基于Zookeeper实现分布式锁1.?Zook
eeper分布式锁介绍常见的分布式锁实现方案里面,除了使用redis来实现之外,使用zookeeper也可以实现分布式锁。在介绍z
ookeeper实现分布式锁的机制之前,先粗略介绍一下zk是什么东西:zk是一种提供配置管理、分布式协同以及命名的中心化服务。zk
的模型是这样的:zk包含一系列的节点,叫做znode,就好像文件系统一样每个znode表示一个目录,然后znode有一些特性:1)
有序节点:假如当前有一个父节点为/lock,我们可以在父节点下面创建子节点;zookeeper提供了一个可选的有序特性,例如我们可
以创建子节点“/lock/node-”并且指明有序,那么zookeeper在生成子节点时会根据当前的子节点数量自动添加整数序号也就
是说,如果是第一个创建的子节点,那么生成的子节点为/lock/node-0000000000,下一个节点则为/lock/node-
0000000001,依次类推。2)临时节点:客户端可以建立一个临时节点,在会话结束或者会话超时后,zookeeper会自动删除该
节点。3)事件监听:在读取数据时,我们可以同时对节点设置事件监听,当节点数据或结构变化时,zookeeper会通知客户端。当前zo
okeeper有如下四种事件:节点创建节点删除节点数据修改子节点变更基于以上的一些zk的特性,我们很容易得出使用zk实现分布式锁的
落地方案:1.?使用zk的临时节点和有序节点,每个线程获取锁就是在zk创建一个临时有序的节点,比如在/lock/目录下。2.?创建
节点成功后,获取/lock目录下的所有临时节点,再判断当前线程创建的节点是否是所有的节点的序号最小的节点3.?如果当前线程创建的节
点是所有节点序号最小的节点,则认为获取锁成功4.?如果当前线程创建的节点不是所有节点序号最小的节点,则对节点序号的前一个节点添加一
个事件监听。比如当前线程获取到的节点序号为/lock/003,然后所有的节点列表为:[/lock/001,/lock/002,/l
ock/003],则对/lock/002这个节点添加一个事件监听器。如果锁释放了,会唤醒下一个序号的节点,然后重新执行第3步,判断
是否自己的节点序号是最小。比如/lock/001释放了,/lock/002监听到时间,此时节点集合为[/lock/002,/loc
k/003],则/lock/002为最小序号节点,获取到锁。整个过程如下:2.?Curator?介绍Curator是一个zooke
eper的开源客户端,也提供了分布式锁的实现。他的使用方式也比较简单:InterProcessMutex?ipm?=?new?In
terProcessMutex(client,"/d_lock");????????????????????????ipm.acq
uire();????????????????????????ipm.release();其实现分布式锁的核心源码如下:?whil
e?(?(client.getState()?==?CuratorFrameworkState.STARTED)?&&?!have
TheLock?)?{????????????//?获取当前所有节点排序后的集合??????????????List>?children?=?getSortedChildren();????????????//?获取当前节点的名称????????
???????String?sequenceNodeName?=?ourPath.substring(basePath.lengt
h()?+?1);?????????????//?判断当前节点是否是最小的节点??????????????PredicateRes
ults???predicateResults?=?driver.getsTheLock(client,?children,?se
quenceNodeName,?maxLeases);????????????if?(?predicateResults.gets
TheLock()?)?{????????????????//?获取到锁????????????????????haveTheLo
ck?=?true;????????????}?else?{????????????????//?没获取到锁,对当前节点的上一个节
点注册一个监听器???????????????????……?????????????}????????}3.?ZK与Redis的优
缺点比较学完了几种分布式锁的实现方案之后,本节需要讨论的是redis和zk的实现方案中各自的优缺点。对于redis的分布式锁而言,
它有以下缺点:它获取锁的方式简单粗暴,获取不到锁直接不断尝试获取锁,比较消耗性能。另外来说的话,redis的设计定位决定了它的数据
并不是强一致性的,在某些极端情况下,可能会出现问题。锁的模型不够健壮,即便使用redlock算法来实现,在某些复杂场景下,也无法保
证其实现100%没有问题。但是另一方面使用redis实现分布式锁在很多企业中非常常见,而且大部分情况下都不会遇到所谓的“极端复杂场
景”,所以使用redis作为分布式锁也不失为一种好的方案,最重要的一点是redis的性能很高,可以支撑高并发的获取、释放锁操作。zookeeper天生设计定位就是分布式协调,强一致性。锁的模型健壮、简单易用、适合做分布式锁。如果获取不到锁,只需要添加一个监听器就可以了,不用一直轮询,性能消耗较小。但是如果有较多的客户端频繁的申请加锁、释放锁,对于zk集群的压力会比较大。总结我们介绍了几种分布式锁的实现方式,并进行了一些优缺点比较,哪种方式都无法做到完美。就像CAP一样,在复杂性、可靠性、性能等方面无法同时满足,所以需要根据不同的应用场景选择最适合的方式。从理解的难易程度角度(从低到高)数据库?>?缓存?>?Zookeeper从实现的复杂性角度(从低到高)Zookeeper?>=?缓存?>?数据库从性能角度(从高到低)缓存?>?Zookeeper?>=?数据库从可靠性角度(从高到低)Zookeeper?>?缓存?>?数据库作者简介:林洪优???58集团资深工程师猜你喜欢1、Zeus:?Uber?开发的分布式的高扩展?Shuffle?服务组件2、Apache?Spark?中支持的七种?Join?类型简介3、实时离线一体化系统在?360?的应用4、低代码在爱奇艺鹊桥数据同步平台的实践
献花(0)
+1
(本文系昵称1008795...首藏)