分布式锁

1.分布式锁的介绍

1.1 分布式锁

分布式锁是指分布式环境下,系统集群部署,实现多进程分布式互斥的一种锁。
为了保证多个进程能看到锁,锁被存在公共存储(比如 Redis、Memcache、数据库等三方存储中),以实现多个进程并发访问同一个临界资源,同一时刻只有一个进程可访问共享资源,确保数据的一致性。

1.2 为什么需要分布式锁

在单机应用程序开发环境中,当线程需要并发同步时,对于多线程间涉及到的的代码同步问题,我们一般采用synchronized/Lock的方式来进行解决,同时,我们将基于锁来检查多个线程间对资源的安全访问。例如,在文件读写系统中,用户想要进行一个写操作,那么写进程就需要检查系统是否存在一个写线程锁。
如果存在一个写线程锁,那么我们就需要等待直到锁释放后,才能去争夺资源,才有可能获取到属于该线程的锁并进行写操作,这样,通过锁就可以避免多个线程同时写操作造成的数据冲突。而对于读进程,往往是可以多个读进程一起执行的,但过大的访问量,也会给系统带来很大的压力。此时,单台服务器已经无法满足我们的需求。但当我们的应用处于分布式环境下工作时,基本锁已经不再适用,这时候,我们就需要一种更加高级的锁机制来处理这个进程级别的代码同步和并发问题。
其实,操作系统中提供了许多内置的函数,以帮助程序员来实现并发控制。但是,对于运行分布在多台机器上的多线程的程序来说,我们无法再通过操作系统的内置函数来实现对资源的控制。如果借助关系数据库事务来实现锁,这种方法会有许多不足,例如性能差、稳定性不足。所以,我们必须引入分布式锁来约束并发操作。

1.3 分布式锁的基本要求

分布式锁,是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,分布式锁对系统资源和数据起到了一个很好的协调作用。为了保证分布式锁可用,我们在确保锁实现的同时,需要至少满足以下几个条件:
(1)互斥性:在任何时候,锁只能由一个客户端获取,而不能由两个或多个客户端同时获取。
(2)高可用的获取锁与释放锁;
(3)高性能的获取锁与释放锁;
(4)具备可重入特性;
(5)具备锁失效机制:由于某些原因(如宕机等),获取锁的客户端未能及时释放锁,导致其它客户端无法进行获取,从而导致死锁,导致进程无法进行下去。因此,分布式锁应具备锁失效机制。
(6)具备非阻塞锁特性:当客户端没有获取到锁时,应直接将信息返回,通知获取锁失败。

2.分布式锁的实现

分布式锁主要用于解决分布式系统中数据不一致的问题,分布式锁一般有3种实现方式:
(1)基于数据库的分布式锁;
(2)基于 Redis的分布式锁;
(3)基于ZooKeeper的分布式锁。

2.1 基于数据库的分布式锁

2.1.1选用数据库实现分布式锁的原因

优点:简单,易实现;

2.1.2 基于数据库实现分布式锁的缺点

没有锁失效机制、可用性、性能较差;
·当并发量较大时,对数据库压力较大;

2.1.3分布式锁的实现

(1)基于表记录
最简单的方法是直接创建一张锁表,通过操作该表中的数据来实现分布式锁,此时,我们对method_name做一些唯一性约束。以保证,数据库同时接收到多个请求,保证只有一个操作可以成功获取锁,假设某个线程操作成功,那么我们可以认为该线程获得了该方法的锁,也就说明可以开始执行方法体内容。当我们想要获取某个资源时,我们就要向表中添加一条数据,执行完成相应方法体后删除对应的行数据即表示释放锁。

(2) 悲观锁
基于select … where … for update 排他锁来实现的,我们可以认为,获得排它锁意味着获得了分布式锁,接着进一步,我们就可以执行方法的业务逻辑,并通过connection.commit()操作来提交事务,从而释放锁。但是,数据库单点问题和可重入问题,在悲观锁中还是无法得到直接解决。

(3) 乐观锁
乐观锁是基于CAS思想(Compare and swap(比较与交换),用来解决多线程并发情况下使用锁造成性能开销的一种机制)来实现的,但不具有互斥性,并不会进行加锁,而是假设可以没有冲突的去完成某项操作,操作过程中认为不存在并发冲突,我们是通过增加递增的version字段对乐观锁进行实现。
若数据库中的version字段值和更新时携带的version字段值不同,则表示更新失败(即返回0)。由于写一条sql都需要判断,增加了数据库操作的次数,在高并发的一下,对数据库连接的开销是巨大的。

2.2 基于Redis的分布式锁

2.2.1选用Redis实现分布式锁的原因

(1)Redis是建于内存高性能数据库;
(2)Redis命令对此支持较好,实现起来较为方便;

2.2.2基于Redis实现分布式锁的缺点

(1)Redis实现分布式锁,需要自己不断地去尝试获取锁,比较消耗性能(需要轮询,占用CPU资源);
(2)如果在集群中,出现master宕机的情况。此时,锁key还没有同步到slave节点上,这时候会出现机器B从新的master上获取到了一个重复的锁的现象;
(3)假设拥有Redis锁的客户端因为某些原因挂了,如果想要释放锁,那么只能等到设定的超时时间到了。

2.2.3 基于Redis的分布式锁的实现

(1)一般来讲,我们会使用setnx加锁,并设置一个唯一的分布式锁key,并对key设置对应的客户端唯一的标识。为了避免死锁,我们可以通过expire命令为锁设置一个超时时间,超过时间则自动释放锁。我们可以设置一个随机生成的UUID(随机字符串)来表示锁的value值,来判断在某一时刻是否释放该锁;
(2)我们在获取锁的同时还应设置一个获取的超时时间,若是在时间内一直未获取到锁,则表示放弃获取该锁;
(3)当释放锁时,我们需要通过UUID来判断锁的确定性,若是该锁,则执行delete命令进行锁释放。

2.3 基于Zookeeper的分布式锁

2.3.1选用Zookeeper实现分布式锁的原因

(1)Zookeeper具备高可用、可重入、阻塞锁等特性,有较好的的性能和可靠性;
(2)Zookeeper可解决失效死锁的问题;
(3)当获取不到锁时,我们可以通过注册个监听器,来自动尝试获取锁,因此,性能开销较小。
(4)当获取锁的客户端由于某些原因挂了,因为在Zookeeper中,创建的是临时znode,只要客户端挂了,临时节点znode就会消失,此时锁就会自动被释放;

2.3.2 Zookeeper实现分布式锁的缺点

(1)需要频繁的创建和删除节点,获取锁和释放锁都需要在Leader上执行,然后同步到Follower中,因此,在性能上不如Redis实现的分布式锁。
(2)易出现“羊群效应”;

2.3.3基于Zookeeper的分布式锁的实现

基于Zookeeper的分布式锁的实现,主要有以下两种方法。

(1)可以借助开源库进行实现,例如,Apache的开源库Curator,是一个Zookeeper客户端。它提供的InterProcessMutex是可重入互斥锁,跨JVM工作,可以帮助我们来实现分布式锁。通过acquire方法来获取锁,release方法来释放锁。

(2)也可以通过自己编写,创建节点(临时节点)即加锁,删除节点即解锁,基于ZooKeeper实现分布式锁的步骤如下:
(1)创建一个目录mylock;
(2)某一时刻,线程thread_A想要申请获取锁,需要在mylock目录下创建临时顺序节点;
(3)获取mylock目录下所有的子节点后,然后寻找比自己小的兄弟节点,若找到并获取,若未找到,则说明不存在,表示当前线程顺序号最小,从而申请获得锁;
(4)线程thread_B想要获取锁,则它也要获取所有节点,判断自己不是最小节点,若不是,则需要设置监听比自己次小的节点;
(5)当线程thread_A的业务逻辑处理完之后,会删除自己的节点,线程thread_B监听到了事件变更,会重新判断自己是不是最小的节点,如果是,则申请获得锁。

3.三种分布式锁实现方式的比较

实现方式实现思路优点缺点
基于数据库利用数据库自身提供的锁机制,要求数据库支持行级锁。实现简单,稳定可靠。性能差,无法适应高并发的场所;容易出现思索的情况。
基于Redis使用setnx和lua脚本机制实现,保证对存序列的原子性操作。性能好。实现相对较复杂,有出现思索的可能性。
基于Zookeeper基于zk的节点特性以及watch机制实现。性能较好,可靠性高,有较好的实现阻塞锁。实现相对复杂。

4.总结

对于分别基于数据库、Redis、Zookeeper实现分布式锁的三种方式,任一方法都无法做到完美,都有利有弊。就像CAP理论一样,在性能、可靠性、复杂性等方面无法同时满足,所以,我们在使用分布式锁的时候应该根据业务场景来进行选择。

从各个角度对比可以得到以下结论:
(1)从理解的难易程度角度(从低到高):数据库 > Redis > Zookeeper
(2)从实现的复杂性角度(从低到高):Zookeeper >= Redis > 数据库
(3)从性能角度(从高到低):Redis > Zookeeper >= 数据库
(4)从可靠性角度(从高到低):Zookeeper > Redis > 数据库

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

PerryLes

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值