分布式锁的 Java 实现与性能对比:从实战落地到选型指南(一) - 指南
核心价值:覆盖电商秒杀、分布式任务调度、库存扣减 3 大高频场景,拆解 Redis(原生 / Redisson)、ZooKeeper、数据库 3 类 Java 实现方案,提供 “问题场景→原理图解→实战代码→故障案例→性能测试” 闭环,附 30 + 段可复用代码、6 张架构 / 时序图,帮你避开 90% 分布式锁落地坑。
一、为什么需要分布式锁?—— 从业务故障切入
在单机系统中,synchronized
或ReentrantLock
可解决多线程资源竞争,但分布式系统(微服务集群)中,多节点竞争同一资源(如库存、任务)时,本地锁完全失效,直接引发业务故障:
1.1 典型故障案例:电商秒杀超卖
场景:某电商秒杀活动,商品库存 100 件,部署 3 个订单服务节点,用本地锁控制库存扣减。
问题:活动开始后,3 个节点同时读取库存 = 100,各自扣减后最终库存 =-20,出现超卖。
根因:本地锁仅能控制单节点内的线程竞争,无法跨节点同步资源状态(3 个节点的本地锁互不感知)。
1.2 分布式锁的 3 大核心需求
互斥性:同一时间仅允许 1 个节点的 1 个线程获取锁;
高可用:锁服务无单点故障(避免锁不可用导致业务中断);
安全性:锁需自动释放(避免死锁,如节点宕机后锁无法释放);
可选特性:重入性(同一线程可重复获取锁)、公平性(按请求顺序获取锁)、高性能(支持高并发)。
二、方案 1:基于 Redis 的分布式锁(高并发首选)
Redis 因高性能、部署简单,成为高并发场景(如秒杀)的分布式锁首选,常见实现分 “原生 Redis 命令” 和 “Redisson 封装” 两种。
2.1 问题场景:秒杀库存扣减(10 万 QPS)
需实现 “库存 100 件,多节点并发扣减,无超卖、无漏减”,要求锁响应时间 < 1ms,支持高并发。
2.2 实现 1:原生 Redis 命令(SET NX EX)
2.2.1 原理:利用 Redis 原子命令
Redis 的SET key value NX EX timeout
命令是原子操作,满足锁的核心需求:
NX
(Not Exist):仅当 key 不存在时才设置(保证互斥性);EX
(Expire):设置过期时间(避免死锁,保证安全性);锁 key:如
lock:seckill:1001
(1001 为商品 ID,区分不同资源);锁 value:随机 UUID(避免误释放其他线程的锁)。
2.2.2 实战代码(Java + Jedis)
import redis.clients.jedis.Jedis;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
public class RedisNativeLock
{
// Redis连接信息
private static final String REDIS\_HOST = "192.168.1.100";
private static final int REDIS\_PORT = 6379;
private static final String REDIS\_PASSWORD = "Redis@123456";
// 锁过期时间(5秒,避免死锁)
private static final int LOCK\_EXPIRE = 5;
// 重试间隔(100毫秒,避免频繁重试)
private static final int RETRY\_INTERVAL = 100;
// 生成随机UUID(避免误释放锁)
private String generateLockValue() {
return UUID.randomUUID().toString();
}
// 获取锁(带重试)
public String acquireLock(String lockKey, int maxRetryTimes) {
Jedis jedis = null;
try {
// 1. 建立Redis连接
jedis = new Jedis(REDIS\_HOST, REDIS\_PORT);
jedis.auth(REDIS\_PASSWORD);
// 2. 生成锁value
String lockValue = generateLockValue();
int retryCount = 0;
// 3. 循环重试获取锁
while (retryCount < maxRetryTimes) {
// 核心:执行SET NX EX命令(原子操作)
String result = jedis.set(lockKey, lockValue, "NX", "EX", LOCK\_EXPIRE);
if ("OK".equals(result)) {
System.out.println("获取锁成功,lockKey=" + lockKey + ", lockValue=" + lockValue);
return lockValue;
// 返回value,用于释放锁
}
// 重试间隔
TimeUnit.MILLISECONDS.sleep(RETRY\_INTERVAL);
retryCount++;
System.out.println("获取锁失败,重试次数=" + retryCount);
}
// 重试次数耗尽,获取锁失败
System.out.println("获取锁失败,已达最大重试次数=" + maxRetryTimes);
return null;
} catch (Exception e) {
System.err.println("获取锁异常:" + e.getMessage());
return null;
} finally {
// 关闭Redis连接
if (jedis != null) {
jedis.close();
}
}
}
// 释放锁(需验证value,避免误释放)
public boolean releaseLock(String lockKey, String lockValue) {
Jedis jedis = null;
try {
jedis = new Jedis(REDIS\_HOST, REDIS\_PORT);
jedis.auth(REDIS\_PASSWORD);
// 1. 先查询锁的value(确保是当前线程的锁)
String currentValue = jedis.get(lockKey);
if (lockValue.equals(currentValue)) {
// 2. 释放锁(DEL命令)
jedis.del(lockKey);
System.out.println("释放锁成功,lockKey=" + lockKey + ", lockValue=" + lockValue);
return true;
}
// 锁已被其他线程占用或已过期
System.out.println("释放锁失败:锁value不匹配或已过期");
return false;
} catch (Exception e) {
System.err.println("释放锁异常:" + e.getMessage());
return false;
} finally {
if (jedis != null) {
jedis.close();
}
}
}
// 实战:秒杀库存扣减(用Redis锁控制)
public void seckillStock(String lockKey, int productId) {
// 1. 获取锁(最多重试3次)
String lockValue = acquireLock(lockKey, 3);
if (lockValue == null) {
throw new RuntimeException("秒杀失败:获取锁超时");
}
// 2. 执行业务(扣减库存,此处用Redis模拟库存)
Jedis jedis = null;
try {
jedis = new Jedis(REDIS\_HOST, REDIS\_PORT);
jedis.auth(REDIS\_PASSWORD);
// 查询当前库存
String stockKey = "stock:product:" + productId;
int stock = Integer.parseInt(jedis.get(stockKey));
if (stock <= 0) {
throw new RuntimeException("秒杀失败:库存不足");
}
// 扣减库存(原子操作,避免超卖)
jedis.decr(stockKey);
System.out.println("秒杀成功,剩余库存=" + (stock - 1));
} catch (Exception e) {
throw new RuntimeException("秒杀业务异常:" + e.getMessage());
} finally {
// 3. 释放锁(必须在finally中,确保锁释放)
releaseLock(lockKey, lockValue);
}
}
// 测试:3个线程模拟3个服务节点秒杀
public static void main(String\[] args) {
RedisNativeLock lock = new RedisNativeLock();
String lockKey = "lock:seckill:1001";
// 商品1001的秒杀锁
int productId = 1001;
// 初始化库存(100件)
Jedis jedis = new Jedis(REDIS\_HOST, REDIS\_PORT);
jedis.auth(REDIS\_PASSWORD);
jedis.set("stock:product:" + productId, "100");
jedis.close();
// 启动3个线程(模拟3个服务节点)
for (int i = 0; i <
3; i++) {
new Thread(() ->
{
for (int j = 0; j <
40; j++) {
// 每个线程尝试秒杀40次
try {
lock.seckillStock(lockKey, productId);
} catch (Exception e) {
// 忽略失败(如库存不足、获取锁超时)
}
}
}).start();
}
}
}
2.2.3 原生实现的痛点与解决方案
痛点 | 问题描述 | 解决方案 |
---|---|---|
锁过期导致业务未完成 | 锁过期时间 5 秒,但业务执行需 10 秒,锁被释放后其他线程抢占 | 实现 “看门狗” 机制(定时续期锁过期时间) |
非重入性 | 同一线程无法重复获取同一把锁(如递归调用) | 用 Redis Hash 存储 “线程标识 + 重入次数” |
释放锁非原子操作 | 查询 value 和 DEL 命令分两步,可能误释放其他线程的锁 | 用 Lua 脚本实现原子释放(if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end ) |
2.3 实现 2:Redisson 封装(企业级首选)
Redisson 是 Redis 官方推荐的 Java 客户端,已封装分布式锁的 “看门狗”“重入性”“原子释放” 等特性,无需手动实现。
2.3.1 核心特性
重入锁:支持同一线程重复获取锁(
RLock.lock()
);看门狗续期:默认 30 秒过期,业务未完成时自动续期(每 10 秒续期一次);
公平锁:按请求顺序获取锁(
Redisson.createFairLock()
);读写锁:支持并发读、独占写(
RReadWriteLock
),适合读多写少场景。
2.3.2 实战代码(Spring Boot + Redisson)
- 依赖配置(pom.xml)
<!-- Redisson依赖 --><dependency><groupId>org.redisson\</groupId><artifactId>redisson-spring-boot-starter\</artifactId><version>3.23.3\</version></dependency>
- Redisson 配置(application.yml)
spring:
redis:
host: 192.168.1.100
port: 6379
password: Redis@123456
timeout: 2000ms
redisson:
lock:
watch-dog-timeout: 30000 # 看门狗超时时间(30秒,默认)
threads: 4 # 业务线程池大小
netty-threads: 4 # Netty线程池大小
- Redisson 锁实现(秒杀业务)
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class RedissonSeckillService
{
@Autowired
private RedissonClient redissonClient;
@Autowired
private StringRedisTemplate stringRedisTemplate;
// 秒杀库存扣减(Redisson锁实现)
public void seckill(String productId) {
// 1. 定义锁key(按商品ID区分)
String lockKey = "lock:seckill:" + productId;
// 2. 获取重入锁
RLock lock = redissonClient.getLock(lockKey);
try {
// 3. 获取锁(最多等待3秒,锁自动过期30秒,看门狗自动续期)
boolean isLocked = lock.tryLock(3, 30, TimeUnit.SECONDS);
if (!isLocked) {
throw new RuntimeException("秒杀失败:获取锁超时");
}
// 4. 执行业务(扣减库存)
String stockKey = "stock:product:" + productId;
Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get(stockKey));
if (stock == null || stock <= 0) {
throw new RuntimeException("秒杀失败:库存不足");
}
// 原子扣减库存
stringRedisTemplate.opsForValue().decrement(stockKey);
System.out.println("秒杀成功,商品ID=" + productId + ",剩余库存=" + (stock - 1));
} catch (InterruptedException e) {
throw new RuntimeException("获取锁被中断:" + e.getMessage());
} finally {
// 5. 释放锁(仅当前线程持有锁时才释放)
if (lock.isHeldByCurrentThread()) {
lock.unlock();
System.out.println("释放锁成功,lockKey=" + lockKey);
}
}
}
// 测试:高并发秒杀(100个线程)
public static void main(String\[] args) {
// 模拟Spring容器初始化(实际项目中由Spring管理)
RedissonSeckillService service = new RedissonSeckillService();
// 初始化库存(100件)
service.stringRedisTemplate.opsForValue().set("stock:product:1001", "100");
// 启动100个线程模拟高并发
for (int i = 0; i <
100; i++) {
new Thread(() ->
{
try {
service.seckill("1001");
} catch (Exception e) {
// 忽略失败
}
}).start();
}
}
}
2.4 故障案例:Redis 锁超时导致超卖
2.4.1 问题背景
某电商用原生 Redis 锁实现秒杀,锁过期时间设为 5 秒。大促期间,因 Redis 响应延迟,业务执行耗时增至 8 秒,锁自动过期后被其他线程抢占,导致同一库存被 2 个线程扣减,出现超卖。
2.4.2 根因分析
原生 Redis 锁未实现 “看门狗” 续期,锁过期时间固定;
业务执行时间受 Redis 性能影响,超过锁过期时间后,锁被释放;
未添加 “库存预扣减 + 最终校验” 机制,无法发现超卖。
2.4.3 解决方案
改用 Redisson 锁,依赖 “看门狗” 自动续期(业务未完成时,每 10 秒续期一次);
库存扣减前添加 “版本号校验”(用 Redis 的
watch
命令,避免并发修改);最终库存落地数据库,定时校验 Redis 与数据库库存一致性,修复超卖数据。
2.5 Redis 锁避坑总结
✅ 适用场景:高并发(10 万 + QPS)、秒杀、库存扣减等性能优先场景;
❌ 不适用场景:对一致性要求极高(如金融支付)、Redis 集群不稳定场景;
⚠️ 必避坑点:
锁 key 需按资源粒度设计(如
lock:seckill:商品ID
,避免锁粒度太大导致并发低);原生实现必须用 Lua 脚本原子释放锁,避免误释放;
高并发场景优先用 Redisson,避免重复造轮子;
Redis 集群需开启持久化(AOF+RDB),避免宕机后锁数据丢失。
三、方案 2:基于 ZooKeeper 的分布式锁(高可用首选)
ZooKeeper 因 “临时顺序节点” 特性,天然支持分布式锁的高可用与公平性,适合对一致性要求高的场景(如分布式任务调度)。
3.1 问题场景:分布式任务调度(避免重复执行)
某系统需定时执行 “订单对账” 任务,部署 3 个任务节点,要求同一时间仅 1 个节点执行任务,避免重复对账。
3.2 原理:临时顺序节点的 “Watcher” 机制
ZooKeeper 的分布式锁基于 “临时顺序节点 + Watcher 监听” 实现,核心逻辑:
所有节点在
/lock/task
路径下创建 “临时顺序节点”(如/lock/task/lock-0000000001
);每个节点判断自己创建的节点是否为 “最小序号节点”:
是:获取锁成功,执行业务;
否:监听前一个节点(如节点 2 监听节点 1),前一个节点删除时触发监听,重新判断;
- 节点宕机后,临时节点自动删除,锁释放(保证安全性)。
3.2.1 架构图:ZooKeeper 分布式锁节点结构
3.3 实战代码(Java + Curator)
Curator 是 ZooKeeper 官方推荐的 Java 客户端,已封装分布式锁实现(InterProcessMutex
),无需手动处理节点监听。
3.3.1 1. 依赖配置(pom.xml)
<!-- Curator依赖 --><dependency><groupId>org.apache.curator\</groupId><artifactId>curator-recipes\</artifactId><version>5.5.0\</version></dependency><dependency><groupId>org.apache.zookeeper\</groupId><artifactId>zookeeper\</artifactId><version>3.8.2\</version><!-- 排除冲突依赖 --><exclusions><exclusion><groupId>org.slf4j\</groupId><artifactId>slf4j-log4j12\</artifactId></exclusion></exclusions></dependency>
3.3.2 2. Curator 锁实现(分布式任务调度)
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 ZkLockTaskService
{
// ZooKeeper集群地址
private static final String ZK\_CONNECT\_STR = "192.168.1.100:2181,192.168.1.101:2181,192.168.1.102:2181";
// 锁节点路径(任务对账锁)
private static final String LOCK\_PATH = "/lock/task/reconciliation";
// 会话超时时间
private static final int SESSION\_TIMEOUT = 5000;
// 连接超时时间
private static final int CONNECTION\_TIMEOUT = 3000;
// 初始化Curator客户端
private CuratorFramework createCuratorClient() {
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString(ZK\_CONNECT\_STR)
.sessionTimeoutMs(SESSION\_TIMEOUT)
.connectionTimeoutMs(CONNECTION\_TIMEOUT)
.retryPolicy(new ExponentialBackoffRetry(1000, 3)) // 重试策略:1秒重试,最多3次
.build();
client.start();
// 启动客户端
return client;
}
// 执行对账任务(ZooKeeper锁控制)
public void executeReconciliationTask() {
CuratorFramework client = null;
InterProcessMutex lock = null;
try {
// 1. 创建Curator客户端
client = createCuratorClient();
// 2. 创建分布式锁(重入锁)
lock = new InterProcessMutex(client, LOCK\_PATH);
// 3. 获取锁(最多等待5秒,获取成功后无过期时间,节点宕机自动释放)
boolean isLocked = lock.acquire(5, TimeUnit.SECONDS);
if (!isLocked) {
throw new RuntimeException("获取对账任务锁超时,放弃执行");
}
// 4. 执行业务(对账逻辑)
System.out.println("获取锁成功,开始执行对账任务...");
// 模拟对账耗时(3秒)
TimeUnit.SECONDS.sleep(3);
System.out.println("对账任务执行完成,释放锁");
} catch (Exception e) {
throw new RuntimeException("对账任务执行异常:" + e.getMessage());
} finally {
// 5. 释放锁(必须在finally中)
if (lock != null && lock.isAcquiredInThisProcess()) {
try {
lock.release();
System.out.println("锁释放成功");
} catch (Exception e) {
System.err.println("释放锁异常:" + e.getMessage());
}
}
// 6. 关闭Curator客户端
if (client != null) {
client.close();
}
}
}
// 测试:3个节点模拟分布式任务
public static void main(String\[] args) {
ZkLockTaskService service = new ZkLockTaskService();
// 启动3个线程(模拟3个任务节点)
for (int i = 0; i <
3; i++) {
int nodeId = i + 1;
new Thread(() ->
{
System.out.println("节点" + nodeId + "尝试执行对账任务");
try {
service.executeReconciliationTask();
} catch (Exception e) {
System.out.println("节点" + nodeId + "执行失败:" + e.getMessage());
}
}).start();
}
}
}
3.4 故障案例:ZooKeeper 羊群效应导致性能下降
3.4.1 问题背景
某系统用 ZooKeeper 锁实现分布式任务调度,部署 10 个任务节点。当持有锁的节点释放锁时,所有等待节点同时监听 “最小节点”,导致 ZooKeeper 服务器收到大量监听通知,响应延迟从 100ms 增至 500ms。
3.4.2 根因分析
错误实现:所有等待节点监听 “最小节点”,而非 “前一个节点”,导致 “羊群效应”(一个节点释放锁,所有节点被通知);
ZooKeeper 的 Watcher 机制是 “一次性” 的,每次通知后需重新注册监听,增加服务器压力;
未限制等待节点数量,导致大量节点同时竞争锁。
3.4.3 解决方案
改用 Curator 的
InterProcessMutex
,其内部已实现 “监听前一个节点”,避免羊群效应;任务节点添加 “执行间隔”(如同一任务仅允许 1 个节点执行,其他节点间隔 10 秒后重试);
优化 ZooKeeper 集群(增加服务器节点、调整会话超时时间),提升并发处理能力。
3.5 ZooKeeper 锁避坑总结
✅ 适用场景:高可用、公平性要求高、一致性优先的场景(分布式任务调度、配置同步);
❌ 不适用场景:超高并发(1 万 + QPS)、对性能敏感的场景;
⚠️ 必避坑点:
必须用 Curator 客户端,避免手动实现监听导致羊群效应;
锁节点路径需唯一(如按任务类型区分),避免不同任务竞争同一把锁;
ZooKeeper 集群需部署 3 个以上节点,确保高可用;
避免频繁创建 / 删除节点(ZooKeeper 性能瓶颈在节点操作)。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/933885.shtml
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!