0101 分布式锁实现方式

本文介绍了分布式系统中锁的必要性,并详细解析了Redis的RedLock、Zookeeper和MySQL三种分布式锁的实现机制。RedLock通过在多个Redis实例上获取锁来保证安全性,Zookeeper利用临时顺序节点实现锁,而MySQL则通过表记录或排它锁来实现分布式锁。每种方案都有其特点和适用场景。
摘要由CSDN通过智能技术生成
究事物之际,通技艺之变,成一家之言

​ 在单机服务中,锁存在于单机的JVM中,对单机中应用的线程进行加锁和释放锁的操作。然而在分布式系统中,应用是部署在多台机器上的,因此便存在互相独立的多个JVM,传统的锁方式便失效,因为传统的锁只能对单个JVM中的线程作用。于是,分布式锁便应运而生。

​ 分布式锁目前的方案有很多种,包括但不限于:Redis分布式锁(RedLock),Zookeeper分布式锁、数据库(实现的分布式锁(MySQL)。下面分别介绍三种锁的实现方法及细节。

RedLock

RedLock是Redis官方提出的基于Redis实现分布式锁的初级方案(官方redlock),也是目前较为完备的方案之一。分布式的三个基本要求:

  1. 安全特性:互斥。在任何给定时刻,只有一个客户端可以持有锁。
  2. 活力属性A:无死锁。即使锁定资源的客户端崩溃或分区,也始终可以获得锁定。
  3. 活动性B:容错能力。只要大多数Redis节点都处于运行状态,客户端就可以获取和释放锁。

1、单个实例锁的实现

​ Redis命令实现锁命令:

    SET resource_name my_random_value NX PX 30000

该命令仅在密钥不存在(NX选项)且到期时间为30000毫秒(PX选项)时设置密钥。密钥设置为“我的随机值”。该值在所有客户端和所有锁定请求中必须唯一。随机值是为了以安全的方式释放锁,脚本会告诉Redis:仅当密钥存在且存储在密钥上的值恰好是我期望的值时,才删除该密钥。如Lua脚本实现:

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

为了避免删除另一个客户端创建的锁,这一点很重要。例如,一个客户端可能获取了该锁,在某些操作中被阻塞的时间超过了该锁的有效时间(密钥将过期的时间),然后又删除了某个其他客户端已经获取的锁。仅使用DEL是不安全的,因为一个客户端可能会删除另一个客户端的锁。使用上述脚本时,每个锁都由一个随机字符串“签名”,因此仅当该锁仍是客户端尝试将其删除时设置的锁时,该锁才会被删除。

**随机字符串如何产生?**一个简单的解决方案是结合使用unix时间和微秒级分辨率,并将其与客户端ID串联在一起,它不那么安全,但在大多数环境中可能可以完成任务。

我们用作生存的关键时间的时间称为“锁定有效时间”。它既是自动释放时间,又是客户端执行另一操作之前客户端可以再次获取锁而技术上不违反互斥保证的时间,该时间仅限于给定的时间范围从获得锁的那一刻起的时间。

因此,现在我们有了获取和释放锁的好方法。该系统基于由一个始终可用的单个实例组成的非分布式系统的推理是安全的。让我们将概念扩展到没有此类保证的分布式系统。

2、Redlock分布式算法步骤

为了获取锁,客户端执行以下操作:

  1. 获取当前时间(毫秒)。
  2. 它尝试在所有N个实例中依次使用所有实例中相同的键名和随机值来获取锁定。在第2步中,在每个实例中设置锁时,客户端使用的超时时间小于总锁自动释放时间,以便获取该超时时间。例如,如果自动释放时间为10秒,则超时时间可能在5到50毫秒之间。这样可以防止客户端长时间与处于故障状态的Redis节点进行通信:如果某个实例不可用,我们应该尝试与下一个实例尽快进行通信。
  3. 客户端通过从当前时间中减去在步骤1中获得的时间戳,来计算获取锁所花费的时间。当且仅当客户端能够在大多数实例(过半)中获取锁时并且获取锁所花费的总时间小于锁有效时间,则认为已获取锁
  4. 如果获取了锁,则将其有效时间视为初始有效时间减去经过的时间,如步骤3中所计算。
  5. 如果客户端由于某种原因(无法锁定N / 2 + 1个实例或有效时间为负数)而未能获得该锁,它将尝试解锁所有实例(即使它认为不是该实例)能够锁定)。

该算法基于这样的假设:尽管各进程之间没有同步时钟,但每个进程中的本地时间仍以近似相同的速率流动,并且与锁的自动释放时间相比,误差较小。

3、重试失败

当客户端无法获取锁时,它应该在随机延迟后重试,以尝试使试图同时获取同一资源的多个客户端不同步(这可能会导致脑裂的情况,其中没人胜)。同样,客户端在大多数Redis实例中尝试获取锁的速度越快,出现脑裂情况(以及重试的需求)的窗口就越小,因此理想情况下,客户端应尝试将SET命令发送到N个实例同时使用多路复用。

值得强调的是,对于未能获取大多数锁的客户端,尽快释放(部分)获取的锁有多么重要,这样就不必等待密钥期满才能再次获取锁(但是,如果发生了网络分区,并且客户端不再能够与Redis实例进行通信,则在等待密钥到期时要付出可用性代价)。

4、释放锁

释放锁很简单,只需在所有实例中释放锁,无论客户端是否认为它能够成功锁定给定的实例。

5、安全性讨论

该算法安全吗?我们可以尝试了解在不同情况下会发生什么。

首先,让我们假设客户端能够在大多数实例中获取锁。所有实例都将包含一个具有相同生存时间的密钥。但是,密钥是在不同的时间设置的,因此密钥也会在不同的时间失效。但是,如果第一个密钥在时间T1(在与第一台服务器联系之前进行采样的时间)设置为最差,而最后一个密钥在时间T2(从最后一台服务器获得答复的时间)设置为最坏的话,集合中第一个过期的密钥至少存在一次MIN_VALIDITY=TTL-(T2-T1)-CLOCK_DRIFT。所有其他键都将在以后失效,因此我们确保至少在这次同时设置这些键。

在设置大多数键的过程中,另一个客户端将无法获取锁,因为如果已经存在N / 2 + 1个键,则N / 2 + 1 SET NX操作将无法成功。因此,如果获取了锁,则不可能同时重新获取它(违反互斥属性)。

但是,我们还要确保尝试同时获取锁的多个客户端不能同时成功。

如果客户端使用接近或大于锁定最大有效时间(基本上是用于SET的TTL)的时间锁定了大多数实例,则它将认为锁定无效并将实例解锁,因此我们只需要考虑客户端能够在比有效时间短的时间内锁定大多数实例的情况。在这种情况下,对于上面已经说明的参数,MIN_VALIDITY没有客户端应该能够重新获得该锁。因此,只有当大多数锁定时间大于TTL时间时,多个客户端才可以同时锁定N / 2 + 1个实例(“时间”为步骤2的结尾),从而使锁定无效。

您是否能够提供正式的安全证明,指向相似的现有算法或发现错误?这将不胜感激。

6、性能,崩溃恢复和fsync

使用Redis作为锁定服务器的许多用户在获取和释放锁的延迟以及每秒可能执行的获取/释放操作数方面都需要高性能。为了满足此需求,与N个Redis服务器进行通信以减少延迟的策略肯定是多路复用(或简易版的多路复用,即将套接字置于非阻塞模式,发送所有命令,并读取所有命令)之后,假设客户端和每个实例之间的RTT相似)。

但是,如果我们要针对崩溃恢复系统模型,还需要考虑持久性

从理论上讲,如果要在遇到任何类型的实例重新启动时都保证锁定安全性,则需要在持久性设置中始终启用fsync = always。反过来,这将完全破坏性能,使其达到传统上以安全方式实现分布式锁的CP系统的水平。

但是,事情总比乍看之下要好。基本上,只要实例在崩溃后重新启动时就一直保持算法安全,它不再参与任何当前活动的锁,因此实例重新启动时的一组当前活动的锁全部是通过锁定实例而不是实例来获得的。正在重新加入系统。

为了保证这一点,我们只需要使一个实例在崩溃后至少不可用,而不是我们使用的最大TTL(即实例崩溃时存在的所有与锁有关的所有键)所需的时间。无效并自动释放。

使用***延迟重新启动***,即使没有任何种类的Redis持久性,也基本上可以实现安全性,但是请注意,这可能会转化为可用性损失。例如,如果大多数实例崩溃,则系统将对TTL全局不可用(此处全局意味着在此期间根本没有资源可锁定)。

Zookeeper实现分布式锁

实现方法:

1、多个客户端争取一把锁,先去ZK上争先在锁节点下创建临时顺序节点,ZK的节点是有顺序创建的,谁第一个创建成功谁获得锁;

2、没有获得锁的节点,就对自己的上一个节点进行监听

3、自己的上一个节点释放了锁,ZK有通知机制,下一个节点收到通知后排到前面去。

4、如果获得锁的客户端宕机,ZK会感知到,然后自动删除其临时节点,相当于释放了锁。

下图是A先获得锁,B后获得锁的图解:
ZK分布式图示

ZK锁机制讨论

1)羊群效应:ZK会会短时间向客户端推送很多无用的消息,因为每个节点只关心序号比自己小(在自己前面)的节点是否存在,自己能否补上去获取锁,因此ZK会产生很多消耗性能的通知。改进方法

  • 客户端调用getChildren方法获取所有已经创建的子节点列表(不注册任何Watcher);
  • 如果获取不到读锁,那么调用exist来对比自己小的那个节点注册监听器:读请求向比自己小的最后一个写请求节点注册Watcher监听,写请求向比自己小的最后一个节点注册监听器;
  • 等待Watcher监听,重复第一步。如下图所示:
    羊群效应改进对比图示

2)目前使用的ZK分布式锁工具:Apache Curator,是一个Zookeeper的开源客户端,其提供以下锁:

  • Shared Reentrant Lock 可重入锁

  • Shared Lock 共享不可重入锁

  • Shared Reentrant Read Write Lock 可重入读写锁

  • Shared Semaphore 信号量

  • Multi Shared Lock 多锁

MySQL实现分布式锁

首先创建一个锁表

CREATE TABLE `methodLock` ( 
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',  
`method_name` varchar(64) NOT NULL DEFAULT '' COMMENT '锁定的方法名',
`desc` varchar(1024) NOT NULL DEFAULT '备注信息',  
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间,自动生成',  
PRIMARY KEY (`id`),  
UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';

其中,method_name为要锁住的方法名,有唯一性约束

1、实现方式一:基于表记录

实现原理:当多个客户端向表中insert时,只会有一个提交成功,剩下的都执行失败(收到ERROR 1062 XXX错误信息),没收到错误信息的就是获取锁成功。执行完后delete掉该条数据即完成释放锁的操作。

注意事项

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

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

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

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

  • 这把锁是非公平锁,所有等待锁的线程凭运气去争夺锁。

2、实现方式二:基于数据库自身的排它锁

同样使用上方的表结构,可以通过数据库的排他锁来实现分布式锁,在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁。当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁。

我们可以认为获得排它锁的线程即可获得分布式锁,当获取到锁之后,可以执行方法的业务逻辑,执行完方法之后,再通过connection.commit();操作来释放锁,代码如下:

public boolean lock(){
	connection.setAutoCommit(false)
	while(true){
		try{
            //核心操作
			result = select * from methodLock where method_name=xxx for update;
			if(result==null) return true;
		}catch(Exception e){
		}
	sleep(1000);
	}
	return false;
}
	public void unlock(){
	connection.commit();
}

3、乐观锁实现

一般是通过为数据库表添加一个 “version”字段来实现读取出数据时,将此版本号一同读出,之后更新时,对此版本号加1,在更新过程中,会对版本号进行比较,如果是一致的,没有发生改变,则会成功执行本次操作;如果版本号不一致,则会更新失败,实际就是个diff过程。

问题所在

(1). 这种操作方式,使原本一次的update操作,必须变为2次操作: select版本号一次;update一次。增加了数据库操作的次数。

(2). 如果业务场景中的一次业务流程中,多个资源都需要用保证数据一致性,那么如果全部使用基于数据库资源表的乐观锁,就要让每个资源都有一张资源表,这个在实际使用场景中肯定是无法满足的。而且这些都基于数据库操作,在高并发的要求下,对数据库连接的开销一定是无法忍受的。

(3). 乐观锁机制往往基于系统中的数据存储逻辑,因此可能会造成脏数据被更新到数据库中。

### 回答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
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值