分布式锁的原理和实现
前言:
对于锁,大家都不陌生,手机上可以加锁,想用时候解锁,不用的 时候上锁。在日常开发中,我们为了保证资源操作的最终一致性,同样需要用到锁来进行操作控制。本chat结合自己工作中的经验沉淀,来跟大家聊一下。
为什么会出现分布式锁
如下图所示,一个应用被部署到多个机器上做负载均衡。为了保证一个方法或属性在高并发情况下的同一时间只能被同一个线程执行,我们该如何解决这个问题呢?

在传统单体应用单机部署的情况下,可以使用并发处相关的功能(入java并发处理相关的API:ReentrantLock 或者syncchronized)进行互斥控制来解决。但是,随着业务发展,系统架构也会逐步优化升级,原本单体单机部署的系统被演化为分布式集群系统,由于分布式系统多线程,多进程并且分布在多个不同机器上,这将使原来单机部署情况下的并发控制锁策略无法满足,并不能提供分布式锁的能力。为了解决这个问题就需一个跨机器的互斥机制来控制共享资源的访问,这就是分布式锁的解决的难题。
分布式锁应用场景有哪些?
针对分布式锁的目的来反向推到其应用场景,主要包括两类:
1,处理效率提升:引用分布式锁,可以减少重复任务的执行,避免资源处理效率的浪费;
2,数据准确性保障:使用分布式锁可以放在数据资源的并发访问,避免数据不一致情况,甚至数据损失等 。
分布式锁的实现前提
分布式CAP理论:
任何一个分布式系统都无法同时满足一致性,可用性和分区容错性,最多只能同事满足两项。
通常情况下,大家都会牺牲强一致性来换取系统的高可用性,这样我们很多的场景,其实是只需要为了保证数据“最终一致性”。
需要注意的是,这个最终时间需要是用户可以接收的范围内。
另外,要实现分布式锁,需要具备一条条件,主要包括以下几项:
1,在分布式系统环境下,一个方法在同一个时间只能被一个机器的一个线程执行;
2,获取锁和释放锁的高可用及高性能;
3,具备非阻塞锁特性,获取不到锁将直接返回获取锁失败;
4,具备锁失效机制,防止死锁;
上述条件,主要突出锁本身的提效率和保障准确性的应用特性,同事避免其本身对资源访问造成影响;
实现方式有哪些呢?
关于分布式锁的实现,可以分别控制在不同的环节。

常见的主要分为以下这几种:
1,开源组件锁控制:ZooKeeper
ZooKeeper是一个分布式协调服务的开源框架。主要用来解决分布式集群中应用系统的一致性的问题。例如怎么避免同事操作同一数据造成脏读的问题.ZooKeeper本质上是一个分布式的小文件存储系统。提供基于类似文件系统的目录树方式的数据存储,并且可以对树中的节点进行有效管理。

那如何使用ZooKeeper实现分布式锁呢?
1,客户端连接ZooKeeper,并在/tmp下创建临时的且有序的子节点,第一个客户端对应的子节点为lock-0000,第二个为lock-0001,依次类推;
2,客户端获取/lock下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁,否则监听刚好在自己之前一位的子节点删除消息,获得子节点变更通知后重复此步骤直至获得锁;
例如:/tmp下的子节点列表为Lock-0000,lock-0001,lock-0002,序号为1的客户端监听序号为0的子节点删除消息,序号为2的监听序号为1的子节点删除消息。
3,执行业务代码流程,删除当前客户端对应的子节点,锁释放。
ZooKeeper分布式锁方式,性能相对Redis方式较差,主要原因是写操作,都需要在Leader上执行,然后同步到follower。
2,任务处理锁控制:Redis
Redis是完全免费开源的,遵守BSD协议,是一个高性能的key-value数据库。
主要的优势包括:
1,性能极高-Redis能读的速度是11w+次/S,写的速度是8w+次/s
2,丰富的数据类型-Redis主要支持String,Lists,Hashes,Sets,Ordered Sets数据类型
3,原子性-Redis的所有操作都是原子性的,同事Redis还支持对几个操作合并后的原子性执行。
4,丰富的特性-Redis还支持publish/subscribe ,通知,key过期等等特性。
Redis实现简单分布式锁的过程:
1,获取锁,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的uuid,通过此在释放锁的时候判断。
2,获取锁:设置一个获取的超时时间,若超过这个时间则放弃获取锁。
3,释放锁:通过uuid判断是不是该锁,若是该锁,则执行delete进行锁释放。

利用Redis实现分布式锁,实现可能存在的缺点:
在执行delete进行释放锁的时候,加入操作删除锁动作失败,那此key-value过期时间则不好控制,可能会一直存在,可能对后续数据验证造成影响。
数据写入锁控制:Mysql
数据库层面是最终数据写入的时候,对数据写入控制处理,算是分布式锁的最终末端环节。主要包括以下三种方式,下面介绍一下:
实现方式一:唯一索引
UNIQUE KEY `uidx_name`(`name`) USING BTREE;
上述case中,我们对name字段做了索引的唯一性约束,当存在多个新增数据请求同时提交到数据库的话,数据自身则会利用唯一索引,来保证数据的唯一性。
实现方式二:排它锁
执行以下SQL:
SELECT status FROM users WHERE id=3 FRO UPDATE;
假如,在另一个事务中,再次执行:
SELECT status FROM users WHERE id=3 FO UPDATE;
则第二个事务会一直等待上一个事务的提交,此时第二个查询处于阻塞的状态;
排它锁的应用:
在进行事务操作时,通过“FOR UPDATE” 语句,MySql会对查询结果集中每行数据都添加排它锁,其他县城对该记录的更新与删除操作都会阻塞。排它锁包含行锁,表锁。
实现方式三:乐观锁
实现逻辑:乐观锁每次执行数据修改操作时,都会带上一个数据版本号,一单版本号和数据的版本号一直就可以执行修改操作并对版本号执行+1操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现ABA的问题。
除了version外,还可以使用时间戳,因为时间戳天然具有顺序递增性。
比较麻烦的一点:就是在操作业务前,需要先查询出当前的version版本。
数据库分布式锁实现可能存在的缺点:
1,DB操作性能较差,并且有锁表的风险。
2,非阻塞操作失败后,需要轮询,占用CPU资源
3,长时间不commit或者长时间轮询,可能会占用较多链接资源。
总结
上面的几种分布式锁的实现,需要根据不同的应用场景选择最合适的实现方式。
在分布式环境中,对资源进行上锁有时候是很重要的,比如秒杀,抢购某一资源,这时候使用分布式锁就可以很好的控制资源。同事在具体应用过程中,还需要考虑很多的因素,比如超时时间的选取,获取锁时间的选取对并发量等等,上述各方式实现的分布式锁仅作为一种简单的实现的参考,主要了解其中的思想。

浙公网安备 33010602011771号