用redis来实现锁的方式
基于 Redis 实现分布式锁,在 Java 面试中通常被归纳为三种方式:手写基础版、Lua 脚本增强版,以及使用 Redisson 框架。
这几种方式本质上是递进的,核心都是在解决手写实现中存在的原子性、锁续期和可重入等痛点。
🔧 方式一:基于 SETNX + EXPIRE(基础版)
这是最原始的写法,利用 Redis 的 SETNX(不存在才设置)命令实现互斥。
- 核心思路:用
SETNX抢占锁,成功后用EXPIRE设置超时,防止宕机死锁。 - 关键问题:
SETNX和EXPIRE是两条命令,不是原子操作。如果设置锁成功后,设置过期时间前服务宕机,锁将永远无法释放,导致死锁。
⚙️ 方式二:基于 SET NX EX + Lua 脚本(原子增强版)
为了解决方式一的原子性问题,Redis 2.6.12 之后支持了带 NX 和 EX 参数的 SET 命令,并配合 Lua 脚本解决误删问题。
- 加锁改进:使用
SET lockKey uniqueValue EX 30 NX,一条命令完成“不存在则设置”和“设置过期时间”,保证原子性。 - 解锁改进:释放锁时不能直接
DEL。因为可能存在线程 A 业务超时导致锁过期,线程 B 拿到锁后,线程 A 业务执行完误删了 B 的锁。解决方法是把uniqueValue(如 UUID+线程ID)作为锁的值,释放前通过 Lua 脚本判断值是否匹配,匹配才删除,保证“判断+删除”的原子性。
🚀 方式三:基于 Redisson 框架(企业级工业版)
手写方案仍然难以解决业务超时导致锁提前释放以及可重入等问题。在实际生产环境或面试中,Redisson 是更高级的答案。
- 看门狗(Watch Dog)机制:这是 Redisson 解决锁续期的核心。当获取锁且未指定超时时间时,Redisson 会启动一个后台线程,默认每隔 10秒(约锁超时时间的 1/3)检查一次业务是否执行完毕。若未完毕,则自动延长锁的过期时间,有效防止业务未完成时锁自动失效。
- 可重入性:利用 Redis 的 Hash 结构存储线程 ID 和重入次数。同一线程再次获取锁时,重入次数加 1,而不是被阻塞。
- 重试机制:底层利用信号量(Semaphore)和发布订阅(PubSub)功能,在获取锁失败时等待其他线程释放锁并唤醒自己,实现等待重试。
📊 三种方式对比总结
| 特性 | 基础版 (SETNX + EXPIRE) | 原子增强版 (SET NX EX + Lua) | Redisson 框架 |
|---|---|---|---|
| 加锁原子性 | ❌ 非原子 | ✅ 原子(单命令) | ✅ 原子(Lua 封装) |
| 防误删 | ❌ 无法防止 | ✅ 通过 Lua 校验 uniqueValue | ✅ 内部封装 |
| 锁续期(看门狗) | ❌ 无 | ❌ 无 | ✅ 自动续期 |
| 可重入 | ❌ 不支持 | ❌ 不支持 | ✅ 支持 |
| 获取锁失败重试 | ❌ 直接失败 | ❌ 直接失败 | ✅ 支持(带等待) |
| 适用场景 | 仅学习原理 | 简单临时场景 | 生产环境首选 |
在面试时,建议这样回答:“主要分三种,最基础的是用 SETNX 和 EXPIRE 组合,但有原子性问题。改进版是用 SET NX EX 加 Lua 脚本保证原子释放,但依然无法解决业务超时和可重入。所以生产上通常直接用 Redisson,它的看门狗机制能自动续期,还支持可重入和重试,是目前的工业标准。”
Redis 分布式锁"误删锁"问题 Demo
我用生活化的比喻 + 完整可运行代码,帮你彻底搞懂这个问题。
一、先讲个故事(理解问题本质)
假设你去公共厕所,门上有一把锁:
- 锁 = Redis 里的 lockKey
- 钥匙 = uniqueValue(唯一标识)
- 30秒自动开锁 = 锁的过期时间
事故现场:
1. 张三(A) 进厕所,上锁,钥匙上写"张三的钥匙" —— 但张三拉肚子,30秒没出来
2. 30秒到,锁自动开了(张三还在里面!)
3. 李四(B) 进来,上锁,钥匙写"李四的钥匙"
4. 张三终于出来了,看都没看,直接把门上的锁拆了 —— 拆的是李四的锁!
5. 王五(C) 推门进来 —— 厕所里李四还在,乱套了
核心问题:张三拆锁时,没确认这把锁是不是自己的。
解决方案:拆锁前先看钥匙上写的是不是自己的名字,确认是自己的才拆。而且"看+拆"必须一次做完(不能看的时候是张三的,手伸过去的瞬间变成李四的)。
这就是 uniqueValue + Lua 脚本 的作用。
二、错误示范(会误删别人的锁)
public class WrongLock {
// ❌ 错误:直接删除,不管锁是不是自己的
public static void unlock(StringRedisTemplate redis, String lockKey) {
redis.delete(lockKey); // 危险!!!
}
}
触发场景:
| 时间 | 线程 A | 线程 B | Redis 里的锁 |
|---|---|---|---|
| T0 | 加锁成功(超时5秒) | 锁=A | |
| T1 | 业务卡住(超过5秒) | 锁=A | |
| T6 | 还在执行... | 锁过期,自动删除 | |
| T6 | 加锁成功 | 锁=B | |
| T7 | 执行完毕,调用 delete |
锁被A删了!(B的锁) | |
| T8 | 还在执行 | 锁=空 | |
| T8 | 线程C可以加锁成功,B和C同时执行 💥 |
三、正确实现(uniqueValue + Lua)
1. 加锁:SET NX EX + 唯一值
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.UUID;
@Component
public class RedisLock {
private final StringRedisTemplate redis;
public RedisLock(StringRedisTemplate redis) {
this.redis = redis;
}
/**
* 尝试加锁
* @param lockKey 锁的key,如 "lock:order:123"
* @param expireSec 过期时间(秒)
* @return 成功返回 uniqueValue,失败返回 null
*/
public String tryLock(String lockKey, long expireSec) {
// 生成唯一标识:UUID + 线程ID,保证每个线程的值不同
String uniqueValue = UUID.randomUUID() + ":" + Thread.currentThread().getId();
// 一条命令完成"不存在才设置"+"设置过期",原子操作
Boolean success = redis.opsForValue()
.setIfAbsent(lockKey, uniqueValue, expireSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success) ? uniqueValue : null;
}
/**
* 解锁:先判断是不是自己的锁,是才删除
* 用 Lua 保证"判断+删除"的原子性
*/
public boolean unlock(String lockKey, String uniqueValue) {
// Lua 脚本:值匹配才删除,返回1;不匹配返回0
String luaScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
Long result = redis.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(lockKey),
uniqueValue // 传入自己加锁时的唯一值
);
return result != null && result == 1L;
}
}
⚠️ 注意:需要
import java.util.concurrent.TimeUnit;
2. 为什么必须用 Lua?
因为 "GET 判断" 和 "DEL 删除" 是两步操作,中间可能被打断:
线程A: GET lockKey → 值是我的 (判断通过) ✅
↓ 【此刻锁刚好过期,B加锁成功,锁变成B的】
线程A: DEL lockKey → 删掉的是B的锁!❌
Lua 脚本在 Redis 中是单线程原子执行的,"判断"和"删除"之间不会被其他命令插入,从根本上杜绝了这个问题。
3. 业务代码使用示例
@Service
public class OrderService {
@Autowired
private RedisLock redisLock;
public void createOrder(Long orderId) {
String lockKey = "lock:order:" + orderId;
String uniqueValue = redisLock.tryLock(lockKey, 30);
if (uniqueValue == null) {
throw new RuntimeException("操作太频繁,请稍后再试");
}
try {
// ===== 真正的业务逻辑 =====
doBusiness(orderId);
} finally {
// 无论如何都要尝试释放锁,但只会释放自己的
redisLock.unlock(lockKey, uniqueValue);
}
}
private void doBusiness(Long orderId) {
System.out.println("处理订单:" + orderId);
}
}
四、完整可运行的测试 Demo
用 main 方法模拟"业务超时导致锁过期"的场景:
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
public class LockDemo {
static StringRedisTemplate redis;
public static void main(String[] args) throws Exception {
// 1. 初始化 Redis 连接(本地 Redis 默认端口 6379)
LettuceConnectionFactory factory = new LettuceConnectionFactory("localhost", 6379);
factory.afterPropertiesSet();
redis = new StringRedisTemplate(factory);
redis.afterPropertiesSet();
String lockKey = "lock:order:1001";
// ================= 场景1:正常释放 =================
System.out.println("===== 场景1:线程A正常执行并释放 =====");
String valueA = tryLock(lockKey, 30);
System.out.println("A 加锁: " + valueA);
boolean r1 = unlock(lockKey, valueA);
System.out.println("A 解锁结果(应为true): " + r1 + "\n");
// ================= 场景2:误删演示 =================
System.out.println("===== 场景2:模拟业务超时,锁自动过期后被B拿到 =====");
String valueA2 = tryLock(lockKey, 2); // A 只锁2秒
System.out.println("A 加锁(2秒过期): " + valueA2);
System.out.println("A 业务执行中,休眠3秒导致锁过期...");
Thread.sleep(3000); // 模拟A业务超时,锁自动过期
String valueB = tryLock(lockKey, 30); // B 此时能拿到锁
System.out.println("B 加锁成功: " + valueB);
// A 执行完,尝试解锁 —— 关键点来了
boolean r2 = unlock(lockKey, valueA2);
System.out.println("A 解锁结果(应为false,因为锁是B的): " + r2);
// 验证 B 的锁还在
String currentValue = redis.opsForValue().get(lockKey);
System.out.println("当前锁的值: " + currentValue);
System.out.println("B的锁是否安然无恙: " + valueB.equals(currentValue) + "\n");
// 清理
unlock(lockKey, valueB);
factory.destroy();
}
/** 加锁 */
static String tryLock(String key, long expireSec) {
String value = UUID.randomUUID() + ":" + Thread.currentThread().getId();
Boolean ok = redis.opsForValue().setIfAbsent(key, value, expireSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(ok) ? value : null;
}
/** 解锁:Lua 保证原子性 */
static boolean unlock(String key, String value) {
String lua =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else return 0 end";
Long result = redis.execute(
new DefaultRedisScript<>(lua, Long.class),
Collections.singletonList(key), value);
return result != null && result == 1L;
}
}
运行结果:
===== 场景1:线程A正常执行并释放 =====
A 加锁: 8a3f...:1
A 解锁结果(应为true): true
===== 场景2:模拟业务超时,锁自动过期后被B拿到 =====
A 加锁(2秒过期): 5b2c...:1
A 业务执行中,休眠3秒导致锁过期...
B 加锁成功: 9d1e...:1
A 解锁结果(应为false,因为锁是B的): false ← 关键!A没删掉B的锁
当前锁的值: 9d1e...:1
B的锁是否安然无恙: true
看结果就懂了:A 虽然执行完了想去删锁,但因为锁已经是 B 的了,Lua 判断值不匹配,拒绝删除,B 的锁安全了。✅
五、Maven 依赖(Spring Boot 项目)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
application.yml:
spring:
redis:
host: localhost
port: 6379
六、一图总结
加锁:SET lockKey uniqueValue NX EX 30
└─ uniqueValue = UUID + 线程ID,标识"这是谁的锁"
解锁:Lua 脚本
├─ 判断:GET lockKey == 我的 uniqueValue ?
│ ├─ 是 → DEL lockKey(只删自己的)✅
│ └─ 否 → 什么都不做(别人的锁不能碰)✅
└─ 整个过程原子执行,中间不会被插入
七、温馨提示(面试加分点)
这套手写方案虽然解决了误删,但仍有 2 个没解决的问题,正是 Redisson 存在的意义:
- 业务超时问题:A 业务执行超过 30 秒,锁还是会过期 → 需要看门狗自动续期
- 可重入问题:同一线程再次加锁会失败 → 需要 Hash 结构记录重入次数
所以面试时可以这样收尾:
"手写版用 uniqueValue + Lua 解决了误删,但业务超时和可重入还得自己处理,生产上直接上 Redisson 更省心。"
这样回答,既有原理深度,又有工程视野。

浙公网安备 33010602011771号