"锁得住Redis的心,才锁得住并发的魂。"
在微服务、分布式、集群化的今天,如果你还在用 synchronized
对抗并发,那你就像拿着锁门的钥匙试图锁防盗门......根本锁不到点上!
一、为什么需要 Redis 分布式锁?
🔒 单体应用的锁,还行
java
synchronized (this) {
// 线程安全了
}
ReentrantLock
也好,synchronized
也罢,它们都只在当前JVM进程里奏效。
🧨 分布式应用的锁,就不灵了
假设你部署了两个实例在两台机器上:
时间 | 实例 A | 实例 B |
---|---|---|
T1 | 检查库存为 1,准备下单 | - |
T2 | - | 也检查库存为 1,准备下单 |
T3 | 下单成功,库存变为 0 | - |
T4 | - | 也下单成功,库存变成 -1 ❌ |
❗ 这就尴尬了 ------ 数据出现了"超卖"。
此时你需要一个能跨进程、跨JVM、所有实例可见的锁机制。
🎯 Redis 天生是分布式的,基于网络通信,天然跨 JVM,是锁的好载体。
二、Redis锁的实现原理(基础要讲透)
📌 本质:用 SETNX
+ 过期时间 来模拟"加锁"
sql
SETNX(Set if Not Exists)命令实现原子性地加锁
EXPIRE 设置过期时间避免死锁
典型实现(Java版):
javascript
Boolean isLocked = redisTemplate.opsForValue().setIfAbsent("lock:order", "uuid", 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(isLocked)) {
// 成功获取锁
try {
// 执行业务逻辑
} finally {
// 删除锁
redisTemplate.delete("lock:order");
}
} else {
// 获取锁失败,返回或重试
}
⚠️ 问题:删除锁时可能误删他人锁
如果A线程加锁成功,10秒后业务未完成,锁过期自动删除;B线程获取了锁并更新了值;但此时A线程仍在执行,直接 delete("lock:order")
就会误删B的锁!
✅ 解决:加锁时加唯一标识 + 删除时先比对标识
ini
String uuid = UUID.randomUUID().toString();
Boolean success = redisTemplate.opsForValue().setIfAbsent("lock:order", uuid, 10, TimeUnit.SECONDS);
...
// 删除锁之前先比较 uuid
String value = redisTemplate.opsForValue().get("lock:order");
if (uuid.equals(value)) {
redisTemplate.delete("lock:order");
}
❗注意:比较 + 删除要原子操作,否则还是不安全 ------ 所以需要 Lua 脚本!
三、Lua 脚本保证解锁原子性
vbnet
if redis.call("get", KEYS[1]) == ARGV[1]
then
return redis.call("del", KEYS[1])
else
return 0
end
Java中执行方式(SpringBoot + Jedis):
ini
String luaScript = "..."; // 上面那段
Object result = jedis.eval(luaScript, Collections.singletonList("lock:order"), Collections.singletonList(uuid));
四、加点味道:自旋锁实现(尝试多次获取)
Redis原生命令失败后,可以选择返回
false
,也可以 "死磕到底" ------ 自旋!
vbnet
public boolean tryLock(String key, String uuid, long timeoutMs) {
long start = System.currentTimeMillis();
do {
Boolean result = redisTemplate.opsForValue().setIfAbsent(key, uuid, 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(result)) {
return true;
}
try {
Thread.sleep(50); // 稍微等等
} catch (InterruptedException ignored) {}
} while (System.currentTimeMillis() - start < timeoutMs);
return false;
}
适用于秒杀、排行榜更新等短时间竞争激烈的场景。
五、Redisson:优雅而安全的分布式锁
不想手撸 Lua?不想维护自旋逻辑?Redisson 来拯救你!
Redisson封装了:
- 自动续期(看门狗机制)
- 可重入锁、读写锁、公平锁
- 集群下 RedLock 实现
- 异步锁机制
☂️ 自动续期(Watch Dog)
默认锁时间:30秒
如果线程未释放,看门狗会自动续期,直到业务逻辑完成!
csharp
RLock lock = redissonClient.getLock("lock:order");
try {
lock.lock(); // 默认30s自动续期
// 执行业务逻辑
} finally {
lock.unlock();
}
🧠 为什么推荐 Redisson?
功能 | 手撸 Redis | Redisson |
---|---|---|
原子性删除锁 | 需要 Lua | 内置封装 |
自动续期 | 需自己实现 | 默认支持 |
锁类型 | 基本锁 | 支持读写锁、公平锁、RedLock |
容错性 | 自己保证 | 高 |
易用性 | 一般 | 极佳 |
六、几个常见"锁"姿势比较
锁方式 | 原理 | 优点 | 缺点 |
---|---|---|---|
setnx + expire | 简单 | 实现快 | 容易死锁,删除不安全 |
setnx + uuid + lua | 更安全 | 原子删除锁 | 需要写Lua |
Redisson | 内置机制 | 稳定、高可用 | 引入第三方库,有一定重量级 |
自旋锁 | 重试获取 | 保证最终拿到锁 | 可能造成Redis压力、线程饥饿 |
七、结合 SpringBoot 实战小结
csharp
@Service
public class OrderService {
@Autowired
private RedissonClient redissonClient;
public void placeOrder() {
RLock lock = redissonClient.getLock("lock:order");
try {
if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
// 拿到锁,执行核心逻辑
doBusiness();
} else {
throw new RuntimeException("高并发下获取锁失败,请稍后再试");
}
} catch (InterruptedException e) {
throw new RuntimeException("线程中断", e);
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
Redisson 配置(application.yml
):
yaml
redisson:
config: |
singleServerConfig:
address: "redis://localhost:6379"
八、终章 · 思考与哲理
- 分布式锁不是万能药。Redis 宕机,锁就飞了------需搭配高可用集群;
- RedLock 并非绝对安全,需权衡 CAP;
- 锁粒度需控制好:太细无效,太粗浪费资源;
- **锁是否必须?**乐观锁(CAS)+ 队列控制,也许能化解锁带来的副作用;
- 锁得住逻辑,才锁得住业务。
🧠 总结一下
模块 | 内容 |
---|---|
为什么用锁 | 分布式环境无法靠 JVM 锁 |
原理 | SETNX + 过期时间 + 唯一ID + Lua 脚本 |
自旋锁 | 不甘失败,尝试多次 |
Redisson | 自动续期,优雅封装 |
SpringBoot 实践 | @Service + RedissonClient |
深度思考 | 锁与业务、性能、容错性的平衡 |
如果你看到这儿,说明你已经把 Redis 锁吃得七七八八了。愿你在高并发的世界里,锁得住代码、锁得住数据,也锁得住生活的稳定与从容。