Java分布式锁

为什么要使用分布式锁

为了保证一个方法在高并发情况下的同一时间只能被同一个线程执行,在传统单体应用单机部署的情况下,可以使用Java并发处理相关的API(如ReentrantLcok或synchronized)进行互斥控制。但是,随着业务发展的需要,原单体单机部署的系统被演化成分布式系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题。

分布式锁的三种实现方式

在分析分布式锁的三种实现方式之前,先了解一下分布式锁应该具备哪些条件。

在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行; 
高可用的获取锁与释放锁; 
高性能的获取锁与释放锁; 
具备可重入特性; 
具备锁失效机制,防止死锁; 
具备阻塞锁特性,即没有获取到锁将继续等待获取锁; 
具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。 
基于数据库的实现方式

在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引,想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁。

这种实现方式很简单,但是对于分布式锁应该具备的条件来说,它有一些问题需要解决及优化。

因为是基于数据库实现的,数据库的可用性和性能将直接影响分布式锁的可用性及性能,所以,数据库需要双机部署、数据同步、主备切换; 
不具备可重入的特性,因为同一个线程在释放锁之前,行数据一直存在,无法再次成功插入数据,所以,需要在表中新增一列,用于记录当前获取到锁的机器和线程信息,在再次获取锁的时候,先查询表中机器和线程信息是否和当前机器和线程相同,若相同则直接获取锁; 
没有锁失效机制,因为有可能出现成功插入数据后,服务器宕机了,对应的数据没有被删除,当服务恢复后一直获取不到锁,所以,需要在表中新增一列,用于记录失效时间,并且需要有定时任务清除这些失效的数据; 
不具备阻塞锁特性,获取不到锁直接返回失败,所以需要优化获取逻辑,循环多次去获取。 
优点:借助数据库,方案简单。

缺点:在实际实施的过程中会遇到各种不同的问题,为了解决这些问题,实现方式将会越来越复杂;依赖数据库需要一定的资源开销,性能问题需要考虑。

图片描述

基于Redis的实现方式

在Redis2.6.12版本之前,使用setnx命令设置key-value、使用expire命令设置key的过期时间获取分布式锁,使用del命令释放分布式锁,但是这种实现有如下一些问题:

setnx命令设置完key-value后,还没来得及使用expire命令设置过期时间,当前线程挂掉了,会导致当前线程设置的key一直有效,后续线程无法正常通过setnx获取锁,造成死锁; 
在分布式环境下,线程A通过这种实现方式获取到了锁,但是在获取到锁之后,执行被阻塞了,导致该锁失效,此时线程B获取到该锁,之后线程A恢复执行,执行完成后释放该锁,直接使用del命令,将会把线程B的锁也释放掉,而此时线程B还没执行完,将会导致不可预知的问题; 
为了实现高可用,将会选择主从复制机制,但是主从复制机制是异步的,会出现数据不同步的问题,可能导致多个机器的多个线程获取到同一个锁。 
针对上面这些问题,有如下一些解决方案:

第一个问题是因为两个命令是分开执行并且不具备原子特性,如果能将这两个命令合二为一就可以解决问题了。在Redis2.6.12版本中实现了这个功能,Redis为set命令增加了一系列选项,可以通过SET resource_name my_random_value NX PX max-lock-time来获取分布式锁,这个命令仅在不存在key(resource_name)的时候才能被执行成功(NX选项),并且这个key有一个max-lock-time秒的自动失效时间(PX属性)。这个key的值是“my_random_value”,它是一个随机值,这个值在所有的机器中必须是唯一的,用于安全释放锁。 
为了解决第二个问题,用到了“my_random_value”,释放锁的时候,只有key存在并且存储的“my_random_value”值和指定的值一样才执行del命令,此过程可以通过以下Lua脚本实现: 
if redis.call(“get”,KEYS[1]) == ARGV[1] then return redis.call(“del”,KEYS[1]) else return 0 end 
第三个问题是因为采用了主从复制导致的,解决方案是不采用主从复制,使用RedLock算法,这里引用网上一段关于RedLock算法的描述。 
在Redis的分布式环境中,假设有5个Redis master,这些节点完全互相独立,不存在主从复制或者其他集群协调机制。为了取到锁,客户端应该执行以下操作:

获取当前Unix时间,以毫秒为单位; 
依次尝试从N个实例,使用相同的key和随机值获取锁。在步骤2,当向Redis设置锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试另外一个Redis实例; 
客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁使用的时间。当且仅当从大多数(这里是3个节点)的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功。 
如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果); 
如果因为某些原因,获取锁失败(没有在至少N/2+1个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功)。 
通过上面的解决方案可以实现一个高效、高可用的分布式锁,这里推荐一个成熟、开源的分布式锁实现,即Redisson。

优点:高性能,借助Redis实现比较方便。

缺点:线程获取锁后,如果处理时间过长会导致锁超时失效,所以,通过锁超时机制不是十分可靠。

基于ZooKeeper的实现方式

ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。基于ZooKeeper实现分布式锁的步骤如下:

创建一个目录mylock; 
线程A想获取锁就在mylock目录下创建临时顺序节点; 
获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁; 
线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点; 
线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。 
这里推荐一个apache的开源库Curator,它是一个ZooKeeper客户端,Curator提供的InterProcessMutex是分布式锁的实现,acquire方法用于获取锁,release方法用于释放锁。

优点:具备高可用、可重入、阻塞锁特性,可解决失效死锁问题。

缺点:因为需要频繁的创建和删除节点,性能上不如Redis方式。

总结

上面的三种实现方式,没有在所有场合都是完美的,所以,应根据不同的应用场景选择最适合的实现方式。


原文链接:http://geek.youkuaiyun.com/news/detail/244872

### Java分布式锁的实现方式 在Java中,分布式锁是一种用于解决分布式系统中并发控制问题的机制。它可以确保在多个节点上运行的应用程序能够安全地访问共享资源。以下是几种常见的Java分布式锁实现方式: #### 1. 基于Redis的分布式锁 Redis因其高性能和简单的数据结构而成为实现分布式锁的常用选择。可以通过以下两种方式实现基于Redis的分布式锁。 - **使用SETNX命令** SETNX(Set if Not Exists)是一个原子操作,只有当键不存在时才会设置值。可以利用这一特性来实现分布式锁[^1]。例如: ```java public boolean tryLock(String key, String value, int expireTime) { Jedis jedis = new Jedis("localhost", 6379); String result = jedis.set(key, value, "NX", "PX", expireTime); return "OK".equals(result); } ``` - **使用Redisson库** Redisson是一个用于实现分布式锁的高级库,它提供了更丰富的功能和更高的可靠性。以下是一个基于Redisson的分布式锁示例代码[^2]: ```java @Resource private RedissonClient redissonClient; public void updateUser(String userId) { String lockKey = "config" + userId; RLock lock = redissonClient.getLock(lockKey); try { lock.lock(10, TimeUnit.SECONDS); // 加锁,指定锁定时间 // 处理业务逻辑 } finally { lock.unlock(); // 确保释放锁 } } ``` #### 2. 基于ZooKeeper的分布式锁 ZooKeeper是一个分布式协调服务,支持分布式锁的实现。其临时顺序节点的特性非常适合用于实现分布式锁。以下是一个简单的实现示例: ```java InterProcessMutex lock = new InterProcessMutex(client, "/lockpath"); try { lock.acquire(); // 获取锁 // 执行业务逻辑 } finally { lock.release(); // 释放锁 } ``` 此方法通过创建一个唯一的临时顺序节点来实现锁的获取与释放[^3]。 #### 3. 基于数据库的分布式锁 虽然不推荐,但在某些场景下也可以通过数据库实现分布式锁。通常使用`UPDATE`语句结合唯一约束来实现。例如: ```sql UPDATE lock_table SET locked = 1 WHERE name = 'resource' AND locked = 0; ``` 如果更新成功,则表示锁已被获取[^4]。 #### 注意事项 - **锁的超时机制**:为了避免死锁,所有分布式锁实现都应包含超时机制。 - **锁的释放**:无论是否发生异常,锁都应在`finally`块中释放。 - **性能与可靠性**:选择实现方式时需权衡性能与可靠性,例如Redis适合高并发场景,而ZooKeeper更适合需要强一致性的场景。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值