[系统设计] 分布式系统 (1) 分布式锁 [转载]
在日常开发工作中,我们为了保证资源操作的最终一致性,同样需要用到锁来进行操作控制。本人结合自己工作中的经验沉淀,来跟大家一起聊聊 分布式锁的那些事,分享一些实用内容给大家。
为什么会出现分布式锁?
如下图所示,一个应用被部署到多个机器上做负载均衡。为了保证一个方法或属性在高并发情况下的同一时间只能被同一个线程执行,我们该如何解决这个问题呢?
在传统单体应用单机部署的情况下,可以使用并发处理相关的功能(如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 {key} {value} {expirationMilliseconds}
命令在加锁的同时,添加一个超时时间(必须保证此操作的原子性)。超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。
(2)获取锁:设置一个获取的超时时间,若超过这个时间则放弃获取锁。
(3)释放锁:通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。
利用Redis实现分布式锁,实现可能存在的缺点:
在执行delete进行释放锁的时候,假如操作删除锁动作失败,那此 Key-Value 过期时间则不好控制,可能会一直存在,可能对后续数据验证造成影响。
3、数据写入锁控制: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;
则第二个事务会一直等待上一个事务的提交,此时第二个查询处于阻塞的状态。
SELECT status FROM users WHERE id = 3 FOR UPDATE;
则第二个事务会一直等待上一个事务的提交,此时第二个查询处于阻塞的状态。
排它锁的应用:
在进行事务操作时,通过 “FOR UPDATE” 语句,MySQL会对查询结果集中每行数据都添加排他锁,其他线程对该记录的更新与删除操作都会阻塞。排他锁包含行锁、表锁。
实现方式三:乐观锁
实现逻辑:乐观锁每次在执行数据修改操作时,都会带上一个数据版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1 操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现 ABA 问题。
除了 version 以外,还可以使用时间戳,因为时间戳天然具有顺序递增性。
比较麻烦的一点:就是在操作业务前,需要先查询出当前的 version 版本。
数据库分布式锁实现可能存在的缺点:
- DB操作性能较差,并且有锁表的风险;
- 非阻塞操作失败后,需要轮询,占用cpu资源;
- 长时间不commit或者长时间轮询,可能会占用较多连接资源
小结
上面的几种分布式锁的实现,需要根据不同的应用场景选择最适合的实现方式。
在分布式环境中,对资源进行上锁有时候是很重要的,比如秒杀、抢购某一资源,这时候使用分布式锁就可以很好地控制资源。同时,在具体应用过程中,还需要考虑很多的因素,比如超时时间的选取,获取锁时间的选取对并发量等等,上述各方式实现的分布式锁仅作为一种简单的实现的参考,主要了解其中的思想。
面对任何问题,希望大家可以多做些深入分析,了解本质问题之后再考虑解决办法进行解决,希望大家能够掌握问题分析以及解决的能力,去触类旁通,做到游刃有余。
X 参考文献
本文链接: https://www.cnblogs.com/johnnyzen
关于博文:评论和私信会在第一时间回复,或直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
日常交流:大数据与软件开发-QQ交流群: 774386015 【入群二维码】参见左下角。您的支持、鼓励是博主技术写作的重要动力!