写点什么

一文搞懂分布式锁的原理与实现

发布于: 2021 年 04 月 19 日

前言

对于锁,大家应该都不陌生,手机上可以加锁,想用时候解锁,不用的时候上锁,那自行车、房门同样可以加把锁,道理属于类似的情况。


在日常开发工作中,我们为了保证资源操作的最终一致性,同样需要用到锁来进行操作控制。本 Chat 结合自己工作中的经验沉淀,来跟大家一起聊聊 分布式锁的那些事,分享一些实用内容给大家。

为什么会出现分布式锁?

如下图所示,一个应用被部署到多个机器上做负载均衡。为了保证一个方法或属性在高并发情况下的同一时间只能被同一个线程执行,我们该如何解决这个问题呢?

在传统单体应用单机部署的情况下,可以使用并发处理相关的功能(如 Java 并发处理相关的 API:ReentrantLcok 或 synchronized)进行互斥控制来解决。但是,随着业务的发展,系统架构也会逐步优化升级,原本单体单机部署的系统被演化成分布式集群系统,由于分布式系统多线程、多进程并且分布在多个不同机器上,这将使原单机部署情况下的并发控制锁策略无法满足,并不能提供分布式锁的能力。为了解决这个问题就需要一种跨机器的互斥机制来控制共享资源的访问,这就是分布式锁解决的难题!

分布式锁应用场景有哪些?

针对分布式锁的目的来反向推导其应用场景,主要包括两类:

1、处理效率提升:应用分布式锁,可以减少重复任务的执行,避免资源处理效率的浪费;

2、数据准确性保障:使用分布式锁可以放在数据资源的并发访问,避免数据不一致情况,甚至数据损失等。

分布式锁的实现前提

分布式的 CAP 理论:

任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。

通常情况下,大家都会牺牲强一致性来换取系统的高可用性,这样我们很多的场景,其实是只需为了保证数据的“最终一致性”。需要注意的是,这个最终时间需要是用户可以接受的范围内的。

另外,要实现分布式锁,需要具备一些条件,主要包括以下几项:

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 数据库。主要的优势包括:

  • 性能极高 – Redis 能读的速度是 11w+次/s,写的速度是 8w+次/s

  • 丰富的数据类型 – Redis 主要支持 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型

  • 原子性 – Redis 的所有操作都是原子性的,同时 Redis 还支持对几个操作合并后的原子性执行

  • 丰富的特性 – 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 FOR UPDATE;
复制代码

假如,在另一个事务中再次执行:

SELECT status FROM users WHERE id = 3 FOR UPDATE;
复制代码

则第二个事务会一直等待上一个事务的提交,此时第二个查询处于阻塞的状态。

排它锁的应用:

在进行事务操作时,通过 “FOR UPDATE” 语句,MySQL 会对查询结果集中每行数据都添加排他锁,其他线程对该记录的更新与删除操作都会阻塞。排他锁包含行锁、表锁。

实现方式三:乐观锁

实现逻辑:乐观锁每次在执行数据修改操作时,都会带上一个数据版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1 操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现 ABA 问题。

除了 version 以外,还可以使用时间戳,因为时间戳天然具有顺序递增性。

比较麻烦的一点:就是在操作业务前,需要先查询出当前的 version 版本。

数据库分布式锁实现可能存在的缺点:

  • DB 操作性能较差,并且有锁表的风险;

  • 非阻塞操作失败后,需要轮询,占用 cpu 资源;

  • 长时间不 commit 或者长时间轮询,可能会占用较多连接资源

总结

上面的几种分布式锁的实现,需要根据不同的应用场景选择最适合的实现方式。

在分布式环境中,对资源进行上锁有时候是很重要的,比如秒杀、抢购某一资源,这时候使用分布式锁就可以很好地控制资源。同时,在具体应用过程中,还需要考虑很多的因素,比如超时时间的选取,获取锁时间的选取对并发量等等,上述各方式实现的分布式锁仅作为一种简单的实现的参考,主要了解其中的思想。

面对任何问题,希望大家可以多做些深入分析,了解本质问题之后再考虑解决办法进行解决,希望大家能够掌握问题分析以及解决的能力,去触类旁通,做到游刃有余。


·················· END ··················



作者:架构精进之路,十年研发风雨路,大厂架构师,CSDN 博客专家,专注架构技术沉淀学习及分享,职业与认知升级,坚持分享接地气儿的干货文章,期待与你一起成长

关注并私信我回复“01”,送你一份程序员成长进阶大礼包,欢迎勾搭。



Thanks for reading!

发布于: 2021 年 04 月 19 日阅读数: 232
用户头像

坚持分享接地气儿的架构技术文章! 2018.02.26 加入

同名微信公众号「架构精进之路」,专注软件架构研究,技术学习与职业成长!坚持原创总结、沉淀和分享,希望能带给大家一些引导和启发,感谢各位的支持(关注、点赞、分享)!

评论

发布
暂无评论
一文搞懂分布式锁的原理与实现