一、核心原理:Redis 分布式锁的设计基石
1.1 分布式锁的核心要求
一款可靠的分布式锁需满足以下 4 点核心要求,否则易引发死锁、锁误删、数据不一致等问题:
-
互斥性:同一时间只有一个线程能持有锁,杜绝并发竞争;
-
安全性:仅持有锁的线程能释放锁,防止误删其他线程的锁;
-
防死锁:锁需设置过期时间,避免线程持有锁后宕机导致锁永久占用;
-
高可用:加锁、解锁操作高效,适配高并发场景,不成为性能瓶颈。
1.2 UUID+Lua 方案的核心逻辑
本方案通过"Redis 原子加锁 + UUID 唯一标识 + Lua 原子解锁"三者结合,满足上述要求:
-
UUID 唯一标识:作为锁的 Value 值,绑定加锁线程,确保"锁归属唯一"。UUID 全局唯一,可避免分布式场景下(多服务、多线程)锁归属误判,替代线程 ID(进程内唯一,跨进程易重复);
-
Redis 原子加锁 :使用
setIfAbsent(SETNX)操作,原子性完成"锁不存在则设置 + 过期时间",避免加锁与设置过期时间分离导致的死锁; -
Lua 原子解锁:通过 Lua 脚本原子执行"判断锁归属 + 删除锁",避免"判断"与"删除"两步操作分离导致的锁误删。
二、完整实现:通用 Redis 分布式锁工具类
java
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* 通用 Redis 分布式等待锁工具类(UUID+Lua 方案)
* 核心能力:超时等待加锁、原子解锁,适配高并发场景
*/
@Component
public class GenericRedisWaitLock {
private final StringRedisTemplate stringRedisTemplate;
// Lua 原子解锁脚本:验证锁归属(UUID)后删除,避免误删
private static final String UNLOCK_LUA_SCRIPT = """
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
""";
// 重试间隔(50ms):平衡重试效率与 CPU 占用
private static final long DEFAULT_RETRY_INTERVAL = 50;
// 构造器注入 Redis 模板(Spring 自动装配)
public GenericRedisWaitLock(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 尝试获取分布式锁(支持自定义等待时间单位,过期时间固定为秒)
* @param lockKey 锁键(如:lock:device:1001)
* @param waitTime 最大等待时间(超时后放弃)
* @param waitTimeUnit 等待时间单位(秒/毫秒等)
* @param expireTimeSec 锁过期时间(秒,必须>0,防止死锁)
* @return 锁标识(UUID):成功返回标识,失败返回 null
*/
public String tryLock(String lockKey,
long waitTime,
TimeUnit waitTimeUnit,
long expireTimeSec) {
// 入参校验:避免非法参数导致异常
Assert.hasText(lockKey, "lockKey 不能为空");
Assert.isTrue(waitTime >= 0, "waitTime 不能为负数");
Assert.isTrue(expireTimeSec > 0, "expireTimeSec 必须大于 0");
Assert.notNull(waitTimeUnit, "waitTimeUnit 不能为空");
// 生成 UUID 作为锁标识,绑定当前线程
String lockValue = UUID.randomUUID().toString();
// 转换等待时间为毫秒,计算截止时间
long waitTimeMs = waitTimeUnit.toMillis(waitTime);
long deadline = System.currentTimeMillis() + waitTimeMs;
// 循环尝试加锁,直到超时
while (System.currentTimeMillis() < deadline) {
// 原子加锁:不存在则设置值 + 过期时间
Boolean lockSuccess = stringRedisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, expireTimeSec, TimeUnit.SECONDS);
// 加锁成功,返回 UUID 标识(解锁时需传入)
if (Boolean.TRUE.equals(lockSuccess)) {
return lockValue;
}
// 加锁失败,短暂休眠后重试(避免 CPU 空转)
try {
Thread.sleep(DEFAULT_RETRY_INTERVAL);
} catch (InterruptedException e) {
// 捕获中断异常,恢复线程状态并退出
Thread.currentThread().interrupt();
return null;
}
}
// 等待超时,返回 null 表示加锁失败
return null;
}
/**
* 重载方法:等待时间与过期时间均为秒(极简调用)
* @param lockKey 锁键
* @param waitTimeSec 最大等待时间(秒)
* @param expireTimeSec 锁过期时间(秒)
* @return 锁标识(UUID)/null
*/
public String tryLock(String lockKey, long waitTimeSec, long expireTimeSec) {
return tryLock(lockKey, waitTimeSec, TimeUnit.SECONDS, expireTimeSec);
}
/**
* 原子释放分布式锁
* @param lockKey 锁键(与加锁时一致)
* @param lockValue 锁标识(加锁时返回的 UUID)
* @return true=解锁成功;false=锁不存在/非当前线程持有
*/
public boolean unlock(String lockKey, String lockValue) {
// 空值快速失败:避免空指针与无效解锁
if (lockKey == null || lockValue == null) {
return false;
}
// 初始化 Lua 脚本
DefaultRedisScript<Long> unlockScript = new DefaultRedisScript<>();
unlockScript.setScriptText(UNLOCK_LUA_SCRIPT);
unlockScript.setResultType(Long.class);
// 执行 Lua 脚本:原子判断并删除锁
Long executeResult = stringRedisTemplate.execute(
unlockScript,
Collections.singletonList(lockKey), // KEYS[1] = 锁键
lockValue // ARGV[1] = UUID 标识
);
// 结果为 1 表示解锁成功,0 表示锁归属不匹配/已过期
return executeResult != null && executeResult == 1;
}
}
三、实战使用:高并发场景示例
以"设备序号递增"和"FDT 巡检记录创建"两个典型高并发场景为例,演示工具类的使用方式,重点体现"加锁-执行业务-解锁"的完整流程。
3.1 场景一:设备序号递增(避免重复/跳号)
java
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
@Service
public class DeviceSeqService {
private final GenericRedisWaitLock redisWaitLock;
private final StringRedisTemplate stringRedisTemplate;
// 设备序号缓存键
private static final String DEVICE_SEQ_KEY = "device:seq:current";
// 设备序号锁键
private static final String DEVICE_SEQ_LOCK_KEY = "lock:device:seq";
// 加锁配置:最多等 3 秒,锁过期 10 秒
private static final long WAIT_TIME_SEC = 3;
private static final long EXPIRE_TIME_SEC = 10;
public DeviceSeqService(GenericRedisWaitLock redisWaitLock, StringRedisTemplate stringRedisTemplate) {
this.redisWaitLock = redisWaitLock;
this.stringRedisTemplate = stringRedisTemplate;
}
// 生成下一个设备序号(高并发安全)
public Long generateNextSeq() {
String lockValue = null;
try {
// 1. 获取分布式锁
lockValue = redisWaitLock.tryLock(DEVICE_SEQ_LOCK_KEY, WAIT_TIME_SEC, EXPIRE_TIME_SEC);
if (lockValue == null) {
throw new RuntimeException("获取锁超时,序号生成失败");
}
// 2. 一查二判三更新(临界区业务)
String currentSeqStr = stringRedisTemplate.opsForValue().get(DEVICE_SEQ_KEY);
Long currentSeq = currentSeqStr == null ? 0 : Long.parseLong(currentSeqStr);
if (currentSeq < 0) {
throw new RuntimeException("设备序号异常");
}
Long newSeq = currentSeq + 1;
stringRedisTemplate.opsForValue().set(DEVICE_SEQ_KEY, newSeq.toString());
return newSeq;
} finally {
// 3. 最终释放锁(必须在 finally 中,确保锁释放)
if (lockValue != null) {
redisWaitLock.unlock(DEVICE_SEQ_LOCK_KEY, lockValue);
}
}
}
}
3.2 场景二:FDT 巡检记录创建(避免重复创建)
java
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
@Service
public class FdtInspectService {
private final GenericRedisWaitLock redisWaitLock;
private final StringRedisTemplate stringRedisTemplate;
// 巡检任务缓存键前缀(设备 ID 为后缀)
private static final String INSPECT_TASK_PREFIX = "fdt:inspect:task:";
// 巡检锁键前缀(设备 ID 为后缀,细粒度锁)
private static final String INSPECT_LOCK_PREFIX = "lock:fdt:inspect:";
// 加锁配置
private static final long WAIT_TIME_SEC = 3;
private static final long EXPIRE_TIME_SEC = 10;
public FdtInspectService(GenericRedisWaitLock redisWaitLock, StringRedisTemplate stringRedisTemplate) {
this.redisWaitLock = redisWaitLock;
this.stringRedisTemplate = stringRedisTemplate;
}
// 创建设备巡检记录(高并发安全)
public boolean createInspectTask(String deviceId) {
String lockKey = INSPECT_LOCK_PREFIX + deviceId;
String taskKey = INSPECT_TASK_PREFIX + deviceId;
String lockValue = null;
try {
// 1. 获取设备维度的细粒度锁(不影响其他设备并发)
lockValue = redisWaitLock.tryLock(lockKey, WAIT_TIME_SEC, EXPIRE_TIME_SEC);
if (lockValue == null) {
return false; // 加锁失败,说明已有采集器处理该设备
}
// 2. 一查二判三更新(临界区业务)
String taskStatus = stringRedisTemplate.opsForValue().get(taskKey);
if ("RUNNING".equals(taskStatus)) {
return false; // 已存在巡检任务,不重复创建
}
// 创建设备任务(设置为运行中状态,有效期 30 分钟)
stringRedisTemplate.opsForValue().set(taskKey, "RUNNING", 30, java.util.concurrent.TimeUnit.MINUTES);
// 模拟巡检记录入库逻辑
saveInspectTaskToDb(deviceId);
return true;
} finally {
// 3. 释放锁
if (lockValue != null) {
redisWaitLock.unlock(lockKey, lockValue);
}
}
}
// 模拟巡检记录入库
private void saveInspectTaskToDb(String deviceId) {
// 实际业务中替换为数据库写入逻辑
}
}
四、关键注意事项与最佳实践
4.1 锁的粒度设计
优先使用细粒度锁(如场景二中按设备 ID 加锁),而非全局锁。细粒度锁可减少锁竞争,提升系统并发能力;全局锁会导致所有线程排队,成为性能瓶颈。
4.2 过期时间设置
锁过期时间(expireTimeSec)需满足:过期时间 > 业务最大耗时,建议设置为业务最大耗时的 1.5-2 倍。例如业务最多执行 5 秒,过期时间可设为 10 秒,避免锁提前过期导致并发问题。
4.3 解锁操作规范
-
解锁必须传入加锁时返回的 UUID 标识,否则 Lua 脚本会拒绝解锁,防止误删;
-
解锁操作必须放在
finally块中,确保无论业务执行成功还是异常,锁都能最终释放; -
解锁失败仅需日志记录,无需抛异常(锁已自动过期,不影响数据一致性)。
4.4 加锁失败处理
加锁失败(返回 null)时,可根据业务场景选择处理方式:
-
抛异常:适用于必须获取锁才能执行的业务(如序号生成);
-
返回失败:适用于非核心业务(如巡检记录创建);
-
有限重试:通过循环重试获取锁(需控制重试次数,避免无限循环)。
4.5 避免常见坑
-
不直接使用
delete解锁:直接删除锁会导致误删其他线程的锁,必须用 Lua 脚本原子解锁; -
不忽略中断异常:捕获
InterruptedException后需恢复线程中断状态,避免线程状态错乱; -
不使用线程 ID 作为锁标识:线程 ID 跨进程易重复,无法满足分布式场景需求。
五、总结
Redis 分布式锁(UUID+Lua)方案,通过 UUID 保证锁归属唯一,通过 Lua 脚本保证解锁原子性,结合 Redis 高性能原子操作,可完美解决分布式高并发场景下的数据一致性问题。本文提供的工具类轻量化、无业务耦合,可直接复用;实战示例覆盖了常见高并发场景,给出了细粒度锁、过期时间配置等最佳实践。
在实际开发中,需根据业务特点调整加锁等待时间、过期时间,合理设计锁粒度,避开常见坑点,才能充分发挥该方案的优势,构建高可用、高并发的分布式系统。