分布式锁的实现方式

什么是分布式锁

分布式锁是指分布式应用各节点对共享资源的排他式访问而设定的锁。分布式CAP理论:任何一个分布式系统都不无法同时满足一致性、可用性和分区容错性,最多执行同时满足两项。在互联网领域绝大数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证最终一致性,只要这个最终时间是在用户可以接受的范围内容即可。

基于MySQL数据库表实现

通过MySQL实现分布式锁,最简单的方式可能就是直接创建一种锁表,然后通过操作该表中的数据来实现了。当锁住某个方法或者资源时,就在该表中增加一条记录,如果想要释放锁的时候就删除这条记录。

问题

  • 这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库出故障,会导致业务系统不可用

解决方案:可以建立两个数据库,数据之间双向同步,一旦出故障快速切换到备用库上

  • 这把锁没有失效时间,一旦解锁操作失败,锁记录就会一直在数据库中,导致其他线程无法再获得锁

解决方案:可以做一个定时任务,每隔一段时间把数据库中的超时数据清理一遍即可

  • 这把锁只能是非阻塞的,因为数据的插入操作一旦失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想获得锁就要再次触发获得锁操作

解决方案:加一个while循环,直到insert语句成功再返回

  • 这把锁是非重入的,同一个线程在没有释放锁之前无法再再次获得该锁,因为数据已经存在了

解决方案:在数据库表中加个字段,记录当前获取锁机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给它就可以了

基于数据库排他锁实现

在查询语言后面增加“FOR UPDATE”,数据库会在查询过程中给数据库表增加排他锁。当某条记录被加上排他锁之后,其他线程无法再在该记录上增加排他锁。获得排他锁的线程即可获得分布式锁,当获取锁之后,可以执行方法的业务逻辑,执行完方法后,通过Commit()提交事务操作来释放锁。此方法可以有效得解决上面提到的无法释放锁和阻塞锁的问题。FOR UPDATE 语句会在执行成功后立即返回,在执行失败时一直处于阻塞状态,知道成功。针对锁定之后服务宕机,无法释放问题,可以使用这种方式服务宕机之后数据库会自己把锁释放掉。但是还是无法直接 解决数据库单点和可重入问题。

用zookeeper实现分布式锁

zookeeper是Apache软件基金会的一个软件项目,它为大型分布式计算提供开源的分布式配置服务、同步服务和命令注册。zookeeper的架构通过冗余服务实现高可用性。zookeeper是一种用于协调的服务分布式应用程序。zookeeper是一个分布式应用提供一致性服务的开源组件,它内部一个分层的文件系统目录树结构,它规定同一个目录下只能有一个唯一文件名。基于zookeeper实现分布式锁步骤如下:

  1. 创建一个目录mylock
  2. 线程A想获取锁就在mylock目录下创建临时顺序节点
  3. 获取mylock目录下所有子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序序号最小,获得锁
  4. 线程B获取所有节点,判断自己不是最小节点,设置监听比自己小的节点
  5. 线程A处理完后,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获取锁

Apache的开源库Curator,它是一个zookeeper客户端,Curator提供的Inter-ProcessMutex是分布式锁的实现,其中,acquire()方法用于获取锁,release()方法用于释放锁。

基于zookeeper的锁与基于Redis的锁不同之处在于Lock成功之前会一直阻塞,这与单机场景中的mutex.Lock很相似。其原理也是基于临时Sequence节点和watch API,例如这里使用的是“/lock”节点。Lock会在该节点下的节点列表插入自己的值,只要节点下的子节点发生变化,就会通知所有监听该节点的程序。这时程序会检查当前节点下最小子节点的id是否与自己一致。如果一致,说明加锁成功。

这种分布式的阻塞锁比较适合分布式任务调度场景,但不适合高频次,持锁时间短的抢锁场景。基于强一致协议的锁适用于粗粒度的加锁操作。这里的粗粒度是指锁占用时间较长。

zookeeper的优点是具备高可用,可重入,阻塞锁特性,可解决失效死锁问题。zookeeper缺点:因为需要频繁地创建和删除节点,性能上不如Redis方式。

基于Redis的SETNX实现

Redis官方提供了一个名为RedLock的分布式锁算法来实现分布式锁。Redlock算法是Antirez(Redis作者)在单Redis节点基础上引入的高可用模式。在Redis的分布式环境中,假设有N个完全互相独立的Redis节点,有N个Redis实例上使用与在Redis单实例下相同方法获取锁和释放锁。现在假设有N个Redis主节点(大于3的奇数个),这样基本保证他们不会同时都宕掉。在获取锁和释放锁的过程中,客户端会执行以下操作:

  • 获取当前Unix时间,以ms为单位

  • 依次尝试从N个实例中,使用相同的key和具有唯一性的value获取锁。当向Redis请求获取锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间,这样可以避免客户端一直等待

  • 客户端使用当前时间减去开始获取锁时间就得到获取锁使用时间。而且仅当从半数以上的Redis节点取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功

  • 如果取到了锁,则key的实际有效时间等于有效时间减去获取锁所使用的时间

  • 如果因为某些原因,获取锁失败(没有在半数以上的实例取到锁或者取锁时间已经超过了有效时间),则客户端应该在所有的Redis实例上进行解锁,无论Redis实例是否加锁成功。因为可以服务端响应消息丢失了但是实际成功了,毕竟多释放一次也不会有问题。

    上面5个步骤是RedLock算法的主要过程,这种分布式锁有三个重要的考量点:

    1. 互斥,只能有一个客户端获取锁
    2. 不能死锁
    3. 容错,只要大部分Redis节点创建了这把锁就可以

    实现分布式锁的另一种方式就是通过Redis等缓存系统实现。使用Redis实现分布式锁,根本原理是使用SETNX指令:

    SETNX key value

    如果key不存在,则设置key的值为value,如果key已经存在,则不执行赋值操作,并使用不同的返回值标识。

    • 使用 SETNX + DELETE命令实现,通过SETNX设置一个随机值,然后删除这个随机值。
    SETNX lock_key random_value
    // 逻辑处理
    DELETE lock_key
    // 问题:服务获取锁后,因为某种原因出现故障,则锁一直无法自动释放,从而导致死锁
    
    • 使用 SETNX + SETEX命令实现,通过SETNX设置一个随机值,然后通过SETEX设置超时时间,最后删除随机值
    SETNX lock_key random_value
    SETEX lock_key 5 random_value // 5s超时
    // 逻辑处理
    DELETE lock_key
    // 问题:在 SETNX之后,SETEX之前服务出故障,会陷入死锁
    
    • 使用 SET···NX+PX命令实现,将加锁、设置超时两个步骤合并为一个原子操作
    SET lock_key random_value NX PX 5000 // 5s超时
    // 逻辑处理
    DELETE lock_key
    // 问题:如果锁被错误地释放(如超时),或被错误地抢占,或因Redis问题等导致锁丢失,则无法很快地感知到
    
    • 使用 SET Key RandomValue NX PX 命令实现,此方案比上一个方案增加了对value的检查,只解除自己加的锁,类似于CAS(Compare And Swap,比较并交换),是一种原子操作。可用于在多线程编程中实现不被打断的数据交换操作,从而避免多线程同时改写某一数据时由于执行顺序不确定行以及中断的不可预知性产生的数据不一致问题。该操作通过将内存中的值与指定数据进行比较,当数值一样时将内存中的数据替换为新的值。此处不过是先比较后删除,此方案Redis原生命令不支持,为保证原子性,需要使用Lua脚本实现,伪代码如下:
    SET lock_key random_value NX PX 5000 // 5s超时
    // 逻辑处理
    eval "ifredis.call('get',KEYS[1])==ARGV[1]then return redis.call('del',KEYS[1]) 
    else return 0 end 1" lock_key random_value
    // 此方案更加严谨,即使因为某些异常导致锁被错误地抢占,也能部分保证锁的正确释放。并且在释放锁时能检测到锁是否被错误抢占,错误释放,从而进行特殊处理。
    

    注意事项:

    • 超时时间
    1. 不能太短,否则在任务执行完成前就自动释放锁了,导致资源暴露在锁保护之外
    2. 不能太长,否则会导致以意外死锁后时间的等待,除非人为介入处理
    3. 建议根据任务内容,合理衡量超时时间,将超时时间设置为任务内容的几倍即可。如果实在无法确定而又要求比较严格,可以采用SETEX/Expire定期更新超时时间实现
    • 重试:等待次数需要参考任务执行时间

    • 与Redis事务比较,SETNX使用更为灵活方便。Multi/Exec事务的实现形式更为复杂,且部分Redis集群方案不支持Multi/Exec事务

    使用etcd实现分布式锁

    etcd是使用go语言开发的一个开源的,高可用的分布式key-value存储系统,可以用于配置共享和服务的注册和实现。etcd使用Raft算法保持了数据的 强一致性,每次操作存储到集群中的值必然是全局一致性,很容易实现分布式锁。锁服务有“保持独占” “控制时序” 两种使用方式。

    “保持独占”即所有获取锁的用户最终只有一个可以得到。etcd为此提供了一套实现分布式锁原子操作CAS(Compare AND Swap,比较并交换)的API。通过设置prevExist值,可以保证在多个节点同时去创建某个目录时,只有一个成功,而成功创建的用户就可以认为是获得了锁。

    “控制时序” 是指所有想要获得锁的用户都会被安排执行,但是获得锁的顺序也是全局唯一的,同时决定了执行顺序。etcd为此也提供了一套自动创建有序键的API接口,对一个目录建值时指定为POST动作,这样etcd会自动在目录下生成一个当前最大的值为键,存储这个新的值(客户端编号)。同时还可以使用API接口按顺序列出所有当前目录下的键值。此时这些键的值就是客户端的时序,也可以是代表客户端的编号。

    etcd分布式锁实现原理总结如下:

    • 利用租约在etcd集群中创建一个key,这个key有两种形态,存在和不存在,而这两种形态就是互斥量
    • 如果key不存在,则线程创建key,成功则获取到锁,该key为存在状态
    • 如果key已经存在,则线程就不能创建key,获取锁失败

    分布式锁的选择

    实现方式功能要求实现难度学习程度运维成本
    MySQL借助表锁/行锁实现满足基本要求不难熟悉一般 小量可以使用;大量影响现有业务。主多从架构,不方便扩容
    通过zookeeper的方式实现满足要求要求熟悉zookeeper API需要学习较高,需要队机器,有跨机房请求
    Redis使用SET NX PX满足基本要求不难熟悉一般,扩容方便,方便使用现有服务
    通过etcd实现满足要求较易熟悉较高,不能增加节点来提高其性能

    对锁数据的可靠性要求极高的话,那只能使用etcd或者zookeeper这种通过一致性协议保证数据可靠性的方案,但吞吐量低和较高的延迟。

### 回答1: Java分布式锁实现方式有多种,常见的包括: 1. 基于Redis的分布式锁:利用Redis单线程的特性,使用SETNX命令创建锁,利用EXPIRE设置锁的过期时间,同时使用DEL命令释放锁,确保锁的释放是原子的。 2. 基于Zookeeper的分布式锁:通过创建临时节点实现分布式锁,当某个服务占用了锁,其它服务将无法创建同名节点,从而保证同一时间只有一个服务占用该锁。 3. 基于数据库的分布式锁:使用数据库表中的一行记录来表示锁状态,使用事务确保锁的获取和释放是原子的。 4. 基于Redisson的分布式锁:Redisson是一个开源的Java分布式框架,提供了对分布式锁的支持,使用SETNX和EXPIRE命令实现锁的创建和过期,同时还提供了自旋锁、可重入锁等高级特性。 以上是Java分布式锁实现方式的几种常见方式,不同的实现方式有着各自的特点和适用场景,需要根据实际需求进行选择。 ### 回答2: Java分布式锁分布式系统中实现数据同步和控制的关键技术之一,它用于保证多个分布式进程并发访问共享资源时的数据一致性和安全性。分布式锁与普通的锁相比,需要解决跨进程、跨节点的同步和并发控制问题。 Java分布式锁实现方式有以下几种: 1. 基于Zookeeper实现分布式锁 Zookeeper是一个高性能的分布式协调服务,它可以被用来实现分布式锁。Zookeeper的实现原理是基于它的强一致性和顺序性,可以保证多个进程访问同一个分布式锁时的数据同步和控制。 通过创建一个Zookeeper的持久节点来实现分布式锁,使用create()方法来创建节点,如果创建成功则说明获取锁成功。当多个进程同时请求获取锁时,只有一个进程能够创建节点成功,其它进程只能等待。当持有分布式锁的进程退出时,Zookeeper会自动删除对应的节点,其它进程就可以继续请求获取锁。 2. 基于Redis实现分布式锁 Redis是高性能的内存数据库,可以使用它的setnx()命令来实现分布式锁。setnx()命令可以在指定的key不存在时设置key的值,并返回1;如果key已经存在,则返回0。通过这个原子性的操作来实现分布式锁。 当多个进程同时请求获取锁时,只有一个进程能够成功执行setnx()命令,其它进程只能等待。进程在持有锁期间,可以利用Redis的expire()命令来更新锁的过期时间。当持有分布式锁的进程退出时,可以通过delete()命令来删除锁。 3. 基于数据库实现分布式锁 数据库通过ACID特性来保证数据的一致性、并发性和可靠性,可以通过在数据库中创建一个唯一索引来实现分布式锁。当多个进程同时请求获取锁时,只有一个进程能够成功插入唯一索引,其它进程只能等待。当持有分布式锁的进程退出时,可以通过删除索引中对应的记录来释放锁。 不同的实现方式各有优劣。基于Zookeeper的实现方式可以保证分布式锁的一致性和可靠性,但是需要引入额外的依赖;基于Redis可以实现较高性能的分布式锁,但是在高并发条件下可能会存在死锁等问题;基于数据库的实现方式简单,但在高并发条件下也可能会有锁争抢等问题。 总之,在选择分布式锁实现方式时,需要根据业务场景和需求来综合考虑各种因素,选择最适合自己的方式。 ### 回答3: 分布式系统中的并发控制是解决分布式系统中竞争资源的重要问题之一,而分布式锁作为一种并发控制工具,在分布式系统中被广泛采用。Java作为一种常用的编程语言,在分布式锁实现方面也提供了多种解决方案。下面就分别介绍Java分布式锁实现方式。 1. 基于ZooKeeper的分布式锁 ZooKeeper是分布式系统中常用的协调工具,其提供了一套完整的API用于实现分布式锁实现分布式锁的过程中需要创建一个Znode,表示锁,同时用于控制数据的访问。在这个Znode上注册监听器用于接收释放锁的成功/失败事件,从而控制加锁/解锁的过程。 2. 基于Redis的分布式锁 Redis作为一种高性能的Key-Value数据库,其提供了完整的API用于实现分布式锁实现分布式锁的过程中需要在Redis中创建一个Key,利用Redis的SETNX命令进行加锁,同时设置过期时间保证锁的生命周期。在解锁时需要判断是否持有锁并删除对应的Key。 3. 基于数据库的分布式锁 数据库作为分布式系统中常用的数据存储方式,其提供了事务机制用于实现分布式锁。在实现分布式锁的过程中需要在数据库中创建一个表,利用数据库的事务机制实现加锁/解锁,同时需要设置过期时间保证锁的生命周期。 总之,以上三种方式都是常用的Java分布式锁实现方式。选择合适的方法需要综合考虑锁的使用场景、性能需求、可靠性要求等因素。同时,在实现分布式锁的过程中需要注意锁的加锁/解锁的正确性和过期时间的设置,保证分布式系统的并发控制的正确性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

终生成长者

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

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

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

打赏作者

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

抵扣说明:

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

余额充值