Java分布式锁实现教程详解
时间:2025-08-30 08:23:53 275浏览 收藏
还在为Java分布式锁的实现而困惑吗?本文为你提供一份详尽的代码教程,深入探讨如何利用Redis和ZooKeeper构建高效可靠的分布式锁。文章首先概述了实现分布式锁的关键:通过共享存储服务协调多节点资源访问。针对高性能场景,我们提供了基于Redis的解决方案,结合SETNX、EXPIRE命令以及Lua脚本,确保锁的唯一性和原子性。对于追求高可靠性的场景,则详细介绍了基于ZooKeeper的方案,通过创建临时顺序节点和监听机制,实现自动释放锁。此外,本文还剖析了选择方案时需权衡的关键因素,并针对死锁、误删、提前释放等常见问题,提供了实用的解决方案。最后,我们还扩展介绍了数据库行锁、etcd、Consul等其他实现方案,助你根据实际业务需求,做出最优选择。
实现分布式锁的核心在于利用共享存储服务协调多个节点对资源的访问,1. 基于Redis的方案使用SETNX和EXPIRE命令结合UUID和Lua脚本确保唯一性和原子性,适合高性能场景;2. 基于ZooKeeper的方案通过创建临时顺序节点并监听前驱节点实现,利用Watcher机制减少轮询,支持自动释放锁,适合高可靠场景;3. 选择方案时需权衡性能、可靠性、复杂度、锁类型及是否需要自动释放等因素,Redis适合高性能低延迟需求,ZooKeeper适合强一致性要求;4. 避免常见问题如死锁需设置过期时间或使用临时节点,防止误删需用Lua脚本保证删除原子性,避免提前释放可启用锁续约,防脑裂需合理配置集群参数,支持重入可记录线程ID与重入次数;5. 其他方案包括数据库行锁、etcd、Consul以及基于Paxos/Raft的一致性系统,分别适用于不同性能、可靠性与复杂度要求的场景,最终选择应根据实际业务需求综合评估确定。

实现分布式锁,核心在于利用分布式系统中的共享存储服务,例如Redis、ZooKeeper或者数据库,来协调多个节点对共享资源的访问。选择哪种方案取决于你的具体需求,比如性能、可靠性和复杂度。
解决方案:
以下分别介绍基于Redis和ZooKeeper实现分布式锁的Java代码示例:
1. 基于Redis的分布式锁
Redis的SETNX(Set if Not Exists)命令和EXPIRE命令组合,可以实现一个简单的分布式锁。为了避免死锁,需要设置锁的过期时间。
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import java.util.UUID;
public class RedisDistributedLock {
private static final String LOCK_PREFIX = "lock:";
private static final String SUCCESS_RESULT = "OK";
private static final Long RELEASE_SUCCESS = 1L;
private JedisPool jedisPool;
public RedisDistributedLock(String host, int port) {
JedisPoolConfig config = new JedisPoolConfig();
// 可以根据需要调整连接池配置
jedisPool = new JedisPool(config, host, port);
}
/**
* 尝试获取锁
* @param lockName 锁的名称
* @param expireTime 锁的过期时间,单位:秒
* @return 获取锁成功返回锁的值,失败返回null
*/
public String tryLock(String lockName, int expireTime) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
String lockKey = LOCK_PREFIX + lockName;
String lockValue = UUID.randomUUID().toString(); // 锁的值,用于释放锁时验证
String result = jedis.set(lockKey, lockValue, "NX", "EX", expireTime);
if (SUCCESS_RESULT.equals(result)) {
return lockValue;
}
return null;
} finally {
if (jedis != null) {
jedis.close();
}
}
}
/**
* 释放锁
* @param lockName 锁的名称
* @param lockValue 锁的值
* @return 释放锁是否成功
*/
public boolean releaseLock(String lockName, String lockValue) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
String lockKey = LOCK_PREFIX + lockName;
// 使用Lua脚本保证原子性
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, 1, lockKey, lockValue);
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
} finally {
if (jedis != null) {
jedis.close();
}
}
}
public void close() {
if (jedisPool != null) {
jedisPool.close();
}
}
public static void main(String[] args) throws InterruptedException {
RedisDistributedLock lock = new RedisDistributedLock("localhost", 6379);
String lockName = "myLock";
String lockValue = lock.tryLock(lockName, 10); // 尝试获取锁,过期时间10秒
if (lockValue != null) {
try {
System.out.println("获取锁成功,lockValue: " + lockValue);
// 模拟业务逻辑
Thread.sleep(5000);
} finally {
boolean released = lock.releaseLock(lockName, lockValue);
System.out.println("释放锁结果: " + released);
lock.close();
}
} else {
System.out.println("获取锁失败");
lock.close();
}
}
}关键点:
- 锁的唯一标识: 使用UUID作为锁的值,防止误删其他客户端的锁。
- Lua脚本: 使用Lua脚本保证删除锁的原子性,避免并发问题。
- 连接池: 使用JedisPool管理Redis连接,提高性能。
2. 基于ZooKeeper的分布式锁
ZooKeeper通过创建临时顺序节点来实现分布式锁。当一个客户端创建了一个锁节点后,如果该节点是序号最小的节点,则认为该客户端获取到了锁。释放锁时,删除该节点即可。
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class ZookeeperDistributedLock {
private static final String ZK_ADDRESS = "localhost:2181";
private static final String LOCK_ROOT_PATH = "/locks";
private static final String LOCK_NODE_PREFIX = "lock-";
private ZooKeeper zkClient;
private String lockPath;
private String currentLockNode;
private CountDownLatch latch = new CountDownLatch(1);
public ZookeeperDistributedLock(String lockName) throws IOException, InterruptedException, KeeperException {
zkClient = new ZooKeeper(ZK_ADDRESS, 5000, new Watcher() {
@Override
public void process(WatchedEvent event) {
if (event.getState() == Event.KeeperState.SyncConnected) {
latch.countDown();
}
}
});
latch.await();
// 检查根节点是否存在,不存在则创建
Stat stat = zkClient.exists(LOCK_ROOT_PATH, false);
if (stat == null) {
zkClient.create(LOCK_ROOT_PATH, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
this.lockPath = LOCK_ROOT_PATH + "/" + lockName;
Stat lockStat = zkClient.exists(this.lockPath, false);
if(lockStat == null){
zkClient.create(this.lockPath, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
}
/**
* 尝试获取锁
* @param timeout 超时时间,单位:毫秒
* @return 获取锁是否成功
*/
public boolean tryLock(long timeout) throws InterruptedException, KeeperException {
try {
// 创建临时顺序节点
currentLockNode = zkClient.create(lockPath + "/" + LOCK_NODE_PREFIX, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
// 获取所有子节点
List<String> children = zkClient.getChildren(lockPath, false);
Collections.sort(children);
// 如果当前节点是最小的节点,则获取锁成功
if (currentLockNode.equals(lockPath + "/" + children.get(0))) {
return true;
}
// 监听前一个节点
String previousNode = null;
for (int i = 0; i < children.size(); i++) {
if (currentLockNode.equals(lockPath + "/" + children.get(i))) {
previousNode = lockPath + "/" + children.get(i - 1);
break;
}
}
final CountDownLatch waitLatch = new CountDownLatch(1);
Watcher watcher = new Watcher() {
@Override
public void process(WatchedEvent event) {
if (event.getType() == Event.EventType.NodeDeleted) {
waitLatch.countDown();
}
}
};
zkClient.exists(previousNode, watcher);
return waitLatch.await(timeout, TimeUnit.MILLISECONDS); // 等待前一个节点被删除
} catch (KeeperException e) {
// 处理连接断开的情况,重新尝试获取锁
if (e.code() == KeeperException.Code.CONNECTIONLOSS) {
return tryLock(timeout);
} else {
throw e;
}
}
}
/**
* 释放锁
*/
public void unlock() throws InterruptedException, KeeperException {
if (currentLockNode != null) {
zkClient.delete(currentLockNode, -1);
currentLockNode = null;
}
}
public void close() throws InterruptedException {
if (zkClient != null) {
zkClient.close();
}
}
public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
ZookeeperDistributedLock lock = new ZookeeperDistributedLock("myLock");
try {
if (lock.tryLock(5000)) { // 尝试获取锁,超时时间5秒
System.out.println("获取锁成功");
Thread.sleep(5000); // 模拟业务逻辑
} else {
System.out.println("获取锁失败");
}
} finally {
lock.unlock();
lock.close();
}
}
}关键点:
- 临时顺序节点: 使用临时顺序节点保证锁的自动释放,避免死锁。
- Watcher机制: 使用Watcher监听前一个节点的删除事件,减少轮询。
- 连接丢失处理: 处理连接丢失的情况,重新尝试获取锁。
选择Redis还是ZooKeeper取决于你的应用场景。Redis适合对性能要求高的场景,而ZooKeeper适合对可靠性要求高的场景。另外,还可以考虑使用Redisson等封装好的客户端,它们提供了更高级的分布式锁功能。
如何选择合适的分布式锁实现方案?
选择合适的分布式锁实现方案,需要综合考虑以下几个方面:
- 性能: Redis通常具有更高的性能,因为它是基于内存的,读写速度快。ZooKeeper的性能相对较低,因为它需要进行磁盘写入和节点同步。如果你的应用对性能要求很高,Redis可能更适合。
- 可靠性: ZooKeeper具有更高的可靠性,因为它是一个专门为分布式协调设计的服务,具有容错和数据一致性保证。Redis虽然也可以通过主从复制和Sentinel实现高可用,但在某些情况下可能存在数据丢失的风险。如果你的应用对可靠性要求很高,ZooKeeper可能更适合。
- 复杂度: Redis的实现相对简单,只需要使用
SETNX和EXPIRE命令即可。ZooKeeper的实现相对复杂,需要创建临时顺序节点、监听节点删除事件等。如果你的应用对开发效率有要求,Redis可能更适合。 - 锁的类型: Redis只支持排他锁,即同一时刻只能有一个客户端持有锁。ZooKeeper可以支持多种类型的锁,例如排他锁、共享锁等。如果你的应用需要支持多种类型的锁,ZooKeeper可能更适合。
- 是否需要自动释放锁: ZooKeeper的临时节点可以自动释放锁,即使客户端发生故障。Redis需要设置锁的过期时间来避免死锁,但如果客户端在过期时间之前完成操作,可能会导致锁被提前释放。如果你的应用需要自动释放锁的功能,ZooKeeper可能更适合。
总的来说,Redis适合对性能要求高、可靠性要求相对较低的场景,而ZooKeeper适合对可靠性要求高、性能要求相对较低的场景。在实际应用中,可以根据具体的需求进行选择。
如何避免分布式锁的常见问题?
在使用分布式锁时,需要注意以下几个常见问题:
- 死锁: 如果客户端在获取锁之后发生故障,没有释放锁,可能会导致死锁。为了避免死锁,需要设置锁的过期时间,或者使用ZooKeeper的临时节点。
- 锁被提前释放: 如果客户端在锁的过期时间之前完成操作,可能会导致锁被提前释放,其他客户端可以获取到锁,从而导致并发问题。为了避免锁被提前释放,可以使用Lua脚本来保证删除锁的原子性,或者使用Redisson等封装好的客户端。
- 锁的续约: 如果客户端的操作时间超过了锁的过期时间,锁可能会被自动释放,导致并发问题。为了避免锁被自动释放,可以使用锁的续约机制,即在锁即将过期时,客户端自动延长锁的过期时间。Redisson等客户端提供了锁的续约功能。
- 脑裂: 在Redis集群中,如果发生脑裂,即集群分裂成多个独立的子集群,可能会导致多个客户端同时获取到锁,从而导致并发问题。为了避免脑裂,需要合理配置Redis集群的参数,例如设置
min-replicas-to-write和min-replicas-max-lag参数。 - 重入性: 某些场景下,同一个线程可能需要多次获取同一个锁。如果锁不支持重入,可能会导致死锁。为了支持重入,可以在锁中记录持有锁的线程ID和重入次数。Redisson等客户端提供了可重入锁。
除了Redis和ZooKeeper,还有哪些实现分布式锁的方案?
除了Redis和ZooKeeper,还有以下几种实现分布式锁的方案:
- 数据库: 可以使用数据库的行锁或者乐观锁来实现分布式锁。例如,可以使用
SELECT ... FOR UPDATE语句来获取行锁,或者使用版本号来实现乐观锁。数据库实现分布式锁的优点是简单易懂,缺点是性能较低,因为需要进行磁盘IO。 - etcd: etcd是一个分布式键值存储系统,可以用于实现分布式锁。etcd的性能比ZooKeeper高,但可靠性相对较低。
- Consul: Consul是一个服务发现和配置管理系统,也可以用于实现分布式锁。Consul的优点是简单易用,缺点是性能较低。
- 基于Paxos/Raft算法的分布式一致性系统: 可以使用基于Paxos/Raft算法的分布式一致性系统,例如TiDB、CockroachDB等,来实现分布式锁。这种方案的优点是可靠性高,缺点是复杂度高。
选择哪种方案取决于你的具体需求。如果对性能要求很高,可以选择Redis或者etcd。如果对可靠性要求很高,可以选择ZooKeeper或者基于Paxos/Raft算法的分布式一致性系统。如果对复杂度有要求,可以选择数据库或者Consul。
理论要掌握,实操不能落!以上关于《Java分布式锁实现教程详解》的详细介绍,大家都掌握了吧!如果想要继续提升自己的能力,那么就来关注golang学习网公众号吧!
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
500 收藏
-
465 收藏
-
104 收藏
-
403 收藏
-
323 收藏
-
424 收藏
-
127 收藏
-
290 收藏
-
321 收藏
-
213 收藏
-
355 收藏
-
219 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习