架构师必备:分布式锁方案选型(含代码实战)
1. 引言
在单体应用时代,Java 的 synchronized 和 ReentrantLock 足以应对大部分并发场景。但当系统拆分为微服务架构后,多个进程、多台机器需要协调访问共享资源时,分布式锁便成为架构师必须掌握的核心技术。
本文将从原理出发,对比主流分布式锁方案(Redis、ZooKeeper、etcd、数据库),并给出可直接运行的代码实战,帮助你在真实项目中做出合理选型。
2. 分布式锁的核心要求
一个合格的分布式锁应满足以下条件:
- 互斥性:同一时刻只有一个客户端持有锁。
- 防死锁:持有锁的客户端崩溃后,锁能自动释放。
- 可重入性(可选):同一客户端可重复获取同一把锁。
- 高性能:加锁/解锁延迟低,吞吐量高。
- 高可用:锁服务本身不成为单点瓶颈。
3. 方案一:基于 Redis 的分布式锁
3.1 基本原理
利用 Redis 的 SET NX EX 原子命令实现加锁,Lua 脚本 保证解锁的原子性。
3.2 代码实战:Redisson 实现
Redisson 是官方推荐的 Java 客户端,内置了可重入锁、公平锁、红锁等实现。
<!-- pom.xml 依赖 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.27.0</version>
</dependency>
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import java.util.concurrent.TimeUnit;
public class RedisDistributedLock {
private final RedissonClient redissonClient;
public RedisDistributedLock() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379");
this.redissonClient = Redisson.create(config);
}
/**
模拟扣减库存
*/
public void deductStock(String productId, int quantity) {
String lockKey = "lock:stock:" + productId;
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试加锁,最多等待 5 秒,锁有效期 30 秒
boolean locked = lock.tryLock(5, 30, TimeUnit.SECONDS);
if (!locked) {
System.out.println("获取锁失败,请稍后重试");
return;
}
// 业务逻辑:扣减库存
System.out.println("成功扣减库存 " + quantity + ",剩余库存:...");
Thread.sleep(100); // 模拟业务耗时
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 释放锁(Redisson 自动处理 Lua 脚本)
lock.unlock();
}
}
}
3.3 优缺点分析
- 优点:性能极高(纯内存操作),Redisson 封装完善,支持看门狗自动续期。
- 缺点:主从切换时可能出现锁丢失(Redis 异步复制导致),需要引入 RedLock 或使用 Redis Cluster 提升可靠性。
4. 方案二:基于 ZooKeeper 的分布式锁
4.1 基本原理
利用 ZooKeeper 的临时顺序节点 + Watcher 机制实现。客户端创建临时顺序节点,序号最小的节点获得锁,其他节点监听前一个节点的删除事件。
4.2 代码实战:Curator 实现
Apache Curator 是 ZooKeeper 的顶级 Java 客户端,内置了 InterProcessMutex 可重入锁。
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>5.6.0</version>
</dependency>
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;
import java.util.concurrent.TimeUnit;
public class ZkDistributedLock {
private final CuratorFramework client;
private static final String LOCK_PATH = "/locks/stock";
public ZkDistributedLock() {
this.client = CuratorFrameworkFactory.builder()
.connectString("127.0.0.1:2181")
.sessionTimeoutMs(60000)
.connectionTimeoutMs(15000)
.retryPolicy(new ExponentialBackoffRetry(1000, 3))
.build();
client.start();
}
public void deductStock(String productId, int quantity) throws Exception {
InterProcessMutex lock = new InterProcessMutex(client, LOCK_PATH + "/" + productId);
if (lock.acquire(5, TimeUnit.SECONDS)) {
try {
System.out.println("获取 ZK 锁成功,扣减库存 " + quantity);
Thread.sleep(100);
} finally {
lock.release();
}
} else {
System.out.println("获取 ZK 锁超时");
}
}
}
4.3 优缺点分析
- 优点:强一致性(ZAB 协议),临时节点自动释放,无锁丢失风险。
- 缺点:性能低于 Redis(磁盘写入 + 选举开销),频繁加解锁可能产生羊群效应。
5. 方案三:基于 etcd 的分布式锁
5.1 基本原理
etcd 使用 Raft 协议保证强一致性,通过 Lease(租约)+ Revision(版本号)实现分布式锁。客户端创建 key 时绑定租约,租约到期自动删除 key,防止死锁。
5.2 代码实战:Jetcd 实现
<dependency>
<groupId>io.etcd</groupId>
<artifactId>jetcd-core</artifactId>
<version>0.7.7</version>
</dependency>
import io.etcd.jetcd.*;
import io.etcd.jetcd.kv.TxnResponse;
import io.etcd.jetcd.op.Cmp;
import io.etcd.jetcd.op.CmpTarget;
import io.etcd.jetcd.op.Op;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CompletableFuture;
public class EtcdDistributedLock {
private final Client client;
private final KV kvClient;
private final Lease leaseClient;
public EtcdDistributedLock() {
this.client = Client.builder().endpoints("http://127.0.0.1:2379").build();
this.kvClient = client.getKVClient();
this.leaseClient = client.getLeaseClient();
}
public boolean tryLock(String lockKey, long ttlSeconds) throws Exception {
ByteSequence key = ByteSequence.from(lockKey, StandardCharsets.UTF_8);
ByteSequence value = ByteSequence.from("locked", StandardCharsets.UTF_8);
// 创建租约
long leaseId = leaseClient.grant(ttlSeconds).get().getID();
// 事务:如果 key 不存在则创建,否则失败
CompletableFuture&lt;TxnResponse&gt; txnFuture = kvClient.txn()
.If(new Cmp(key, Cmp.Op.EQUAL, CmpTarget.createRevision(0)))
.Then(Op.put(key, value, leaseId))
.Else()
.commit();
TxnResponse response = txnFuture.get();
return response.isSucceeded();
}
public void unlock(String lockKey) throws Exception {
ByteSequence key = ByteSequence.from(lockKey, StandardCharsets.UTF_8);
kvClient.delete(key).get();
}
}
5.3 优缺点分析
- 优点:强一致性,天然支持租约续期,gRPC 协议性能优秀,云原生生态首选。
- 缺点:社区生态不如 Redis 成熟,Java 客户端(Jetcd)文档较少。
6. 方案四:基于数据库的分布式锁
6.1 基本原理
利用数据库唯一索引或 SELECT ... FOR UPDATE 实现互斥。常见做法是创建一张锁表,通过插入/删除记录模拟加锁/解锁。
6.2 代码实战:MySQL 实现
CREATE TABLE `distributed_lock` (
`lock_key` VARCHAR(128) NOT NULL COMMENT '锁标识',
`owner_id` VARCHAR(64) NOT NULL COMMENT '持有者标识',
`expire_at` DATETIME NOT NULL COMMENT '过期时间',
`create_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`lock_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.time.LocalDateTime;
public class DbDistributedLock {
private final DataSource dataSource;
public DbDistributedLock(DataSource dataSource) {
this.dataSource = dataSource;
}
public boolean tryLock(String lockKey, String ownerId, int ttlSeconds) {
String sql = "INSERT INTO distributed_lock (lock_key, owner_id, expire_at) " +
"VALUES (?, ?, ?)";
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, lockKey);
ps.setString(2, ownerId);
ps.setObject(3, LocalDateTime.now().plusSeconds(ttlSeconds));
return ps.executeUpdate() > 0;
} catch (Exception e) {
// 唯一键冲突表示锁已被占用
return false;
}
}
public boolean unlock(String lockKey, String ownerId) {
String sql = "DELETE FROM distributed_lock WHERE lock_key = ? AND owner_id = ?";
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, lockKey);
ps.setString(2, ownerId);
return ps.executeUpdate() > 0;
} catch (Exception e) {
return false;
}
}
}
6.3 优缺点分析
- 优点:无需引入额外中间件,利用现有数据库即可实现。
- 缺点:性能最差(磁盘 IO + 行锁竞争),数据库单点风险,需自行处理锁过期清理。
7. 方案对比与选型建议
| 维度 | Redis | ZooKeeper | etcd | 数据库 |
|---|---|---|---|---|
| 一致性 | 最终一致性 | 强一致性 | 强一致性 | 强一致性 |
| 性能 | 极高 | 中等 | 高 | 低 |
| 自动续期 | 支持(看门狗) | 支持(临时节点) | 支持(Lease) | 需自行实现 |
| 锁丢失风险 | 主从切换时存在 | 无 | 无 | 无 |
| 运维复杂度 | 低 | 中 | 中 | 低 |
| 社区生态 | 非常成熟 | 成熟 | 快速发展 | 无专用生态 |
选型建议
- 高并发、允许短暂不一致:首选 Redis + Redisson,性能最优。
- 强一致性、金融级场景:选择 ZooKeeper 或 etcd,避免锁丢失。
- 云原生架构、Kubernetes 环境:etcd 是最佳选择,天然集成。
- 中小团队、已有 MySQL 集群:数据库锁可作为快速实现方案,但不要用于核心高并发链路。
8. 总结
分布式锁没有银弹,选型需要结合业务场景、一致性要求、团队技术栈和运维能力综合判断。本文给出了四种主流方案的完整代码示例,你可以直接复制到项目中验证。建议先通过压测对比各方案在真实业务下的表现,再做最终决策。
如果你正在设计新的微服务系统,推荐优先考虑 Redis + Redisson 或 etcd + Jetcd,两者在性能和可靠性之间取得了较好的平衡。
更多推荐
所有评论(0)