1. 项目背景
1.1 业务场景
订单系统最怕重复提交。用户在支付页面连点两次"提交订单"按钮、客户端因超时自动重试、网关因网络抖动重复转发——如果后端不做防护,一次购买就会变成两张订单。虽然数据库唯一索引能兜底,但如果所有重复请求都打到数据库执行完整的插入—冲突—回滚流程,在秒杀高峰期仍然会浪费大量数据库资源。
Redis 分布式锁被广泛用于做"入口防重"——同一个用户、同一个业务单号,在一小段时间内只允许一个请求进入核心业务逻辑。相比于数据库行锁,Redis 锁的优势是轻量、快、跨进程可见。但它的风险也显而易见——锁会过期、业务会超时、客户端可能误删别人的锁、Redis 主从切换可能带来锁的一致性问题。
1.2 锁的演进史
| 阶段 | 实现 | 问题 |
|---|---|---|
| 阶段 1 | SETNX lock 1 + EXPIRE lock 10(两条命令) |
两步之间宕机 → 死锁 |
| 阶段 2 | SET lock token NX EX 10(原子命令) |
锁过期后业务没做完 → 误删别人锁 |
| 阶段 3 | 原子加锁 + Lua 校验 token 释放 | 锁过期与业务超时仍无法完美匹配 |
| 阶段 4 | 看门狗自动续期 | 续期线程本身有可靠性问题 |
| 阶段 5 | Redlock(多节点投票) | 有工程争议,时钟和 GC 影响安全性 |
1.3 本章目标
- 实现标准的 Redis 锁:
SET key token NX PX ttl+ Lua 释放。 - 讨论锁过期、业务超时、误删保护、可重入问题。
- 理解看门狗续期的收益与风险。
- 客观评价 Redlock——它能解决什么、不能解决什么。
- 明确:Redis 锁降低并发冲突概率,但不提供强一致保证。
2. 项目设计
2.1 第一幕:SETNX 怎么就不行了?
小胖:"锁不就是 SETNX lock:order 1 吗?拿到了就干活,干完 DEL 掉。"
小白:"如果你的服务拿到锁后宕机了——锁永远不会被删除,这个订单号就会被永久锁住。所以锁必须有过期时间。但如果你用两条命令——SETNX lock 1 然后 EXPIRE lock 10——在两条命令之间宕机,EXPIRE 没执行,又死锁了。"
大师:"所以加锁必须是一条原子命令。Redis 2.6.12 起,SET 支持 NX(不存在才设)和 EX/PX(过期时间),一条命令完成加锁+设 TTL。"
# 错误!两条命令不原子
SETNX mall:lock:order:9001 1
EXPIRE mall:lock:order:9001 10
# 正确!一条原子命令
SET mall:lock:order:9001 "token-uuid-001" NX EX 10
技术映射:原子加锁 = SET key token NX EX ttl。token 必须是唯一随机值(UUID),用于释放时校验"是不是我持有的锁"。
小胖又问:"那释放锁直接 DEL 不就完了?"
大师:"不能直接 DEL。如果你的业务执行超时了,锁自动过期了,另一个请求拿到了新锁——你再去 DEL,删的是别人的锁。所以释放前必须校验 token。"
# 错误!直接 DEL → 可能误删别人的锁
DEL mall:lock:order:9001
# 正确!Lua 校验 token 后删除
EVAL "if redis.call('GET',KEYS[1]) == ARGV[1] then return redis.call('DEL',KEYS[1]) else return 0 end" 1 mall:lock:order:9001 "token-uuid-001"
2.2 第二幕:锁过期了业务没做完怎么办?
小胖:"那我 TTL 设长一点不就行了?设 30 秒肯定够了。"
小白:"你永远不知道业务会多慢。数据库突然慢查询、下游服务超时、Full GC——任何一点抖动都可能导致业务耗时超过 TTL。TTL 设太长——服务宕机后别人等太久;设太短——业务没完成锁就过期了。"
大师:"这里有两个策略:"
策略 A: 不做续期,TTL 略大于 P99 执行时间
适用: 业务执行时间稳定、可预测
风险: 如果偶尔超过 TTL → 锁提前释放 → 可能出现两个请求同时执行
防护: 业务内部仍有数据库唯一约束兜底
策略 B: 看门狗自动续期
实现: 获取锁后启动后台线程,每隔 TTL/3 检查并续期(PEXPIRE)
适用: 业务执行时间波动大
风险: 续期线程宕机/GC 暂停 → 锁仍可能过期;异常任务被无限续期
防护: 设置最大续期次数或最大锁持有时间
// 看门狗续期示意
public class WatchDogLock implements AutoCloseable {
private final String lockKey;
private final String token;
private final ScheduledExecutorService watchdog;
private final AtomicBoolean running = new AtomicBoolean(true);
public WatchDogLock(String lockKey, String token, int ttlMs) {
this.lockKey = lockKey;
this.token = token;
this.watchdog = Executors.newSingleThreadScheduledExecutor();
// 每 ttl/3 毫秒检查并续期
watchdog.scheduleAtFixedRate(() -> {
if (running.get()) {
// 续期:仅当锁还是自己的
redis.eval("if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('PEXPIRE', KEYS[1], ARGV[2]) else return 0 end",
Collections.singletonList(lockKey),
Arrays.asList(token, String.valueOf(ttlMs)));
}
}, ttlMs / 3, ttlMs / 3, TimeUnit.MILLISECONDS);
}
@Override
public void close() {
running.set(false);
watchdog.shutdown();
// Lua 安全释放
redis.eval(UNLOCK_LUA, Collections.singletonList(lockKey), Collections.singletonList(token));
}
}
技术映射:看门狗不是银弹——它让锁更可靠,但也让故障恢复更慢。如果你使用了看门狗,必须设定最大持有时间,防止异常任务无限占用锁。
2.3 第三幕:Redlock——五个 Redis 投票,更安全了吗?
小胖:"我听说 Redlock 算法——向 5 个独立 Redis 主节点申请同一把锁,超过半数成功才算获取。这样即使一个 Redis 挂了,锁还是安全的。"
大师:"Redlock 确实能降低单点 Redis 故障导致的锁失效。但它有两个前提条件,在现实环境中很难完美满足——第一,所有节点的时钟漂移必须在极小范围内(这本身就很难保证);第二,客户端不会因为 GC 暂停而导致锁过期后仍认为自己持有锁。"
Redlock 流程:
Client Redis1 Redis2 Redis3 Redis4 Redis5
│ │ │ │ │ │
├── SET lock token NX PX 10 ──▶ ✓ │ │ │ │
├── SET lock token NX PX 10 ──▶ ✓ │ │ │
├── SET lock token NX PX 10 ──▶ ✓ │ │
├── SET lock token NX PX 10 ──▶ ✗ (超时)│
├── SET lock token NX PX 10 ──▶ ✓
│ │
│ 3/5 成功 → 锁获取成功 │
│ 实际有效时间 = TTL - 网络耗时 - 时钟漂移预算 │
大师的结论:"Redlock 减少了但未消除风险。Martin Kleppmann 和 Redis 作者 antirez 之间的著名辩论已经说明了——Redlock 不能提供像 ZooKeeper 或 etcd 那样的强一致保证。我的工程建议是——如果操作的失败代价可接受(如重复发了一封邮件),单实例 Redis 锁足够。如果涉及资金或关键状态,以数据库事务和唯一约束为准,Redis 锁只做前置削峰。"
| 方案 | 安全性 | 复杂度 | 适用场景 |
|---|---|---|---|
| 单实例 Redis 锁 | 中等 | 低 | 防重提交、定时任务互斥 |
| 单实例 + 看门狗 | 中高 | 中 | 业务耗时不确定的互斥 |
| Redlock | 高(但非强一致) | 高 | 对单点故障敏感的场景 |
| 数据库唯一约束 | 强 | 低 | 最终一致性兜底 |
3. 项目实战
3.1 环境准备
docker exec -it redis-lab redis-cli -a redis123
3.2 实战一:标准加锁与释放
# 加锁
127.0.0.1:6379> SET mall:lock:submit:order:9001 "token-abc-123" NX EX 10
OK
# 第二个请求尝试加锁
127.0.0.1:6379> SET mall:lock:submit:order:9001 "token-xyz-456" NX EX 10
(nil) # 锁已存在,获取失败
# 查看锁的持有者
127.0.0.1:6379> GET mall:lock:submit:order:9001
"token-abc-123"
# 安全释放(Lua 比较后删除)
127.0.0.1:6379> EVAL "if redis.call('GET',KEYS[1])==ARGV[1] then return redis.call('DEL',KEYS[1]) else return 0 end" 1 mall:lock:submit:order:9001 "token-abc-123"
(integer) 1 # 释放成功
# 用错误的 token 尝试释放
127.0.0.1:6379> EVAL "if redis.call('GET',KEYS[1])==ARGV[1] then return redis.call('DEL',KEYS[1]) else return 0 end" 1 mall:lock:submit:order:9001 "wrong-token"
(integer) 0 # 释放失败
3.3 实战二:订单防重复提交完整实现
public class OrderSubmitService {
private static final int LOCK_TTL_MS = 5000;
private static final String UNLOCK_LUA =
"if redis.call('GET',KEYS[1])==ARGV[1] then return redis.call('DEL',KEYS[1]) else return 0 end";
public SubmitResult submit(String userId, String requestId, OrderData data) {
String lockKey = "mall:lock:submit:user:" + userId + ":request:" + requestId;
String token = UUID.randomUUID().toString();
// 1. 尝试获取防重锁
Boolean locked = redis.setIfAbsent(lockKey, token, Duration.ofMillis(LOCK_TTL_MS));
if (!Boolean.TRUE.equals(locked)) {
return SubmitResult.DUPLICATE_REQUEST; // 重复提交
}
try {
// 2. 幂等检查(数据库唯一约束)
if (orderRepository.existsByRequestId(requestId)) {
return SubmitResult.ALREADY_PROCESSED;
}
// 3. 执行核心业务
Order order = createOrder(userId, data);
orderRepository.insertWithRequestId(order, requestId); // 唯一约束防重
return SubmitResult.success(order.getId());
} finally {
// 4. 释放锁
redis.eval(UNLOCK_LUA,
Collections.singletonList(lockKey),
Collections.singletonList(token));
}
}
}
3.4 实战三:模拟锁过期与误删保护
# 终端 A: 加一个 3 秒过期的锁
127.0.0.1:6379> SET mall:lock:demo "token-A" NX EX 3
OK
# 等待 3 秒后锁过期...
# 终端 B: 获取新锁
127.0.0.1:6379> SET mall:lock:demo "token-B" NX EX 10
OK
# 终端 A(还不知道自己锁过期了)尝试释放锁:
127.0.0.1:6379> EVAL "if redis.call('GET',KEYS[1])==ARGV[1] then return redis.call('DEL',KEYS[1]) else return 0 end" 1 mall:lock:demo "token-A"
(integer) 0 # ← 不会误删!因为 token 不匹配了
3.5 实战四:不同锁方案的对比压测
# 测试锁的获取性能
# 无竞争时 SET NX EX 的延迟约 0.2-0.3ms
redis-benchmark -a redis123 -t set -n 10000 -q
# 有竞争时(模拟多个客户端同时尝试同一把锁):
# 用 Lua 脚本模拟竞争:
# redis-cli -a redis123 EVAL "return redis.call('SET',KEYS[1],ARGV[1],'NX','EX',ARGV[2])" 1 lock:test "token" 10
3.6 实战五:生产级锁工具封装
public class RedisLock implements AutoCloseable {
private final StringRedisTemplate redis;
private final String lockKey;
private final String token;
private final long ttlMs;
private static final String UNLOCK_LUA =
"if redis.call('GET',KEYS[1])==ARGV[1] then return redis.call('DEL',KEYS[1]) else return 0 end";
public static RedisLock tryLock(StringRedisTemplate redis, String lockKey, long ttlMs) {
String token = UUID.randomUUID().toString();
Boolean ok = redis.opsForValue()
.setIfAbsent(lockKey, token, Duration.ofMillis(ttlMs));
if (!Boolean.TRUE.equals(ok)) return null;
return new RedisLock(redis, lockKey, token, ttlMs);
}
private RedisLock(StringRedisTemplate redis, String lockKey, String token, long ttlMs) {
this.redis = redis;
this.lockKey = lockKey;
this.token = token;
this.ttlMs = ttlMs;
}
@Override
public void close() {
redis.execute((RedisCallback<Object>) conn ->
conn.eval(UNLOCK_LUA.getBytes(),
org.springframework.data.redis.connection.ReturnType.INTEGER,
1,
lockKey.getBytes(), token.getBytes()));
}
// Try-with-resources 用法:
// try (RedisLock lock = RedisLock.tryLock(redis, "lock:order:123", 5000)) {
// if (lock == null) return "重复请求";
// // 执行业务...
// }
}
3.7 实战六:可重入锁实现
有时同一个线程需要多次获取同一把锁(如在递归业务中),需要可重入锁:
-- reentrant_lock.lua
-- KEYS[1]: 锁 key
-- ARGV[1]: token(线程标识)
-- ARGV[2]: TTL(毫秒)
-- 返回: 1=加锁成功(首次或重入), 0=锁被持有且不是同一线程
local key = KEYS[1]
local token = ARGV[1]
local ttl = tonumber(ARGV[2])
local currentToken = redis.call('GET', key)
if currentToken == false then
-- 锁空闲 → 加锁,重入计数=1
redis.call('HINCRBY', key .. ':reentry', token, 1)
redis.call('SET', key, token, 'PX', ttl)
return 1
end
if currentToken == token then
-- 同一线程 → 可重入
redis.call('HINCRBY', key .. ':reentry', token, 1)
redis.call('PEXPIRE', key, ttl) -- 续期
return 1
end
return 0 -- 锁被其他线程持有
3.8 实战七:锁的监控与告警
@Component
public class LockMonitor {
// 监控锁的持有时间分布
public void recordLockMetrics(String lockKey, long holdMs) {
metricsCollector.recordTimer("redis.lock.hold_time", holdMs);
if (holdMs > 3000) {
log.warn("锁持有时间过长: key={}, holdMs={}", lockKey, holdMs);
}
}
// 定时扫描无 TTL 的锁(死锁检测)
@Scheduled(fixedDelay = 60000)
public void scanDeadLocks() {
String cursor = "0";
do {
ScanResult<String> result = redis.scan(cursor,
ScanOptions.scanOptions().match("mall:lock:*").count(100).build());
for (String key : result.getValues()) {
Long ttl = redis.ttl(key);
if (ttl != null && ttl == -1) {
log.error("发现无 TTL 的锁: {}", key);
alerts.send("Redis 死锁告警: " + key);
}
}
cursor = result.getCursor();
} while (!"0".equals(cursor));
}
}
3.9 实战八:ZooKeeper/etcd 与 Redis 锁的选型决策
| 维度 | Redis 锁 | ZooKeeper/etcd |
|---|---|---|
| 一致性 | 最终一致(主从异步) | 强一致(Raft/ZAB) |
| 性能 | 极高(0.2ms) | 中等(数毫秒) |
| 运维 | 已有 Redis 即可 | 需要独立集群 |
| 锁的安全性 | 可能丢失(主从切换) | 不会丢失 |
| 适合场景 | 性能敏感、可容忍极小概率失效 | 资金安全、强一致要求 |
决策树:
锁失败代价是什么?
├── 重复发了一封邮件/重复记录日志 → Redis 锁足够
├── 可能导致短暂数据不一致(可自动修复) → Redis 锁 + 数据库兜底
└── 涉及资金/关键状态/安全事故 → ZooKeeper/etcd 锁 + 数据库事务
4.3 分布式锁故障案例
| 案例 | 表象 | 根因 | 修复 |
|---|---|---|---|
| 定时任务重复执行 | 每天凌晨的数据报表生成两次 | 锁 TTL 300s,但任务运行了 400s,锁过期后被第二个实例获取 | 加看门狗续期,TTL 设为任务最大时长的 2 倍 |
| 订单重复创建 | 同一个 requestId 创建了两张订单 | 释放锁时用 DEL 没校验 token,误删了别人的锁 |
改用 Lua 校验 token 后删除 |
| 服务重启后死锁 | 部署后某功能一直提示"处理中" | 旧代码用 SETNX+EXPIRE 两条命令,服务在两者之间被 kill |
改为 SET key token NX EX 一条原子命令 |
| GC 导致双重锁 | 用户投诉被重复扣款 | Full GC 暂停 5s,锁过期,另一个线程拿到锁,GC 结束后旧线程继续执行 | 锁内操作加"锁持有校验"——每次操作前重新检查 token |
4.4 自测清单
## 分布式锁自测清单
- [ ] 加锁是否使用原子命令(`SET NX EX/PX`)?
- [ ] token 是否是随机唯一值(UUID)?
- [ ] 释放锁是否用 Lua 校验 token?
- [ ] 锁 TTL 是否大于 P99 业务执行时间?
- [ ] 业务超时后是否有兜底机制?
- [ ] 最终一致性是否有数据库唯一约束兜底?
- [ ] 是否有无 TTL 的锁 key 巡检?
- [ ] 如果涉及资金,是否依赖了 Redis 锁做唯一保障?
5. 锁的实战检查脚本
#!/bin/bash
# 分布式锁验证脚本
echo "=== 测试 1: 原子加锁 ==="
TOKEN=$(uuidgen)
redis-cli -a redis123 SET "test:lock:1" "$TOKEN" NX EX 10
redis-cli -a redis123 GET "test:lock:1"
echo "加锁 token: $TOKEN"
echo "=== 测试 2: 重复加锁拒绝 ==="
redis-cli -a redis123 SET "test:lock:1" "another-token" NX EX 10
echo "预期返回 (nil) → 已锁定"
echo "=== 测试 3: 误删保护 ==="
redis-cli -a redis123 EVAL "if redis.call('GET',KEYS[1])==ARGV[1] then return redis.call('DEL',KEYS[1]) else return 0 end" 1 "test:lock:1" "wrong-token"
echo "预期返回 0 → 不是我的锁,不删"
echo "=== 测试 4: 正确释放 ==="
redis-cli -a redis123 EVAL "if redis.call('GET',KEYS[1])==ARGV[1] then return redis.call('DEL',KEYS[1]) else return 0 end" 1 "test:lock:1" "$TOKEN"
echo "预期返回 1 → 释放成功"
echo "=== 测试 5: 锁过期 ==="
redis-cli -a redis123 SET "test:lock:2" "token-expire" NX EX 1
sleep 2
redis-cli -a redis123 GET "test:lock:2"
echo "预期返回 (nil) → 已过期"
6. 各种锁方案代码速查
| 场景 | 加锁 | 释放 | 续期 |
|---|---|---|---|
| 简单互斥 | SET key token NX EX 10 |
Lua: GET==token → DEL |
不需要 |
| 防重提交 | 同上 + 业务幂等表 | 同上 | 不需要 |
| 长任务 | SET key token NX EX 30 |
Lua 释放 | 每 10s: GET==token → PEXPIRE 30s |
| 可重入 | Lua: GET==false→SET 或 GET==token→HINCRBY |
Lua: HINCRBY -1, if 0→DEL |
每次重入刷新 TTL |
| 公平锁 | ZSet: ZADD queue now+ttl token + ZPOPMIN |
ZREM |
不需要(锁按优先级排序) |
| Redlock | 向 N/2+1 个 Redis 节点申请 | 向所有节点执行 Lua 释放 | 不推荐续期(增加时钟风险) |
6.1 公平锁 Lua 实现
-- fair_lock.lua: 基于 ZSet 的公平排队锁
-- KEYS[1]: 锁队列 key
-- KEYS[2]: 锁标识 key
-- ARGV[1]: token
-- ARGV[2]: TTL (毫秒)
-- ARGV[3]: 当前时间戳 (毫秒)
local queueKey = KEYS[1]
local lockKey = KEYS[2]
local token = ARGV[1]
local ttl = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
-- 1. 清理已过期的排队者
redis.call('ZREMRANGEBYSCORE', queueKey, '-inf', now)
-- 2. 加入排队队列 (score = 期望最晚获取时间)
redis.call('ZADD', queueKey, now + ttl, token)
-- 3. 检查是否轮到自己(队列中 score 最小的是自己吗?)
local first = redis.call('ZRANGE', queueKey, 0, 0)
if first[1] ~= token then
-- 不是自己 → 返回排队位置
local rank = redis.call('ZRANK', queueKey, token)
return -rank -- 负数表示还需等待的人数
end
-- 4. 轮到我了 → 尝试获取锁
local ok = redis.call('SET', lockKey, token, 'NX', 'PX', ttl)
if ok then
redis.call('ZREM', queueKey, token)
return 1 -- 获取成功
end
return 0 -- 获取失败(异常情况,可能已有其他持有者)
6.2 锁的性能基线
| 操作 | 单 Redis 实例 | Redlock (5 节点) |
|---|---|---|
| 无竞争加锁 | 0.2ms | 1-2ms |
| 有竞争加锁 | 0.3ms | 2-5ms |
| 安全释放 | 0.1ms | 0.5-1ms |
| 续期 | 0.1ms | 不推荐续期 |
关键结论:单实例 Redis 锁的性能是 Redlock 的 5-10 倍。只有在对单点故障有零容忍要求的场景下才考虑 Redlock。
6.3 代码审查检查点
当你在 Code Review 中看到 Redis 锁代码时,重点检查:
□ 加锁是原子命令吗? → SET ... NX EX
□ token 是随机值吗? → UUID, 不是 "1" 或线程名
□ 释放锁有 token 校验吗? → Lua GET-then-DEL
□ 锁 TTL 设置合理吗? → 大于 P99 但小于业务 SLA
□ 有数据库兜底吗? → 唯一约束、幂等表
□ 如果锁获取不到,有什么降级策略? → 直接拒绝? 等待重试?
□ 锁 key 命名是否符合规范? → 包含业务含义
6.4 锁的常见反模式
| 反模式 | 代码 | 为什么危险 | 正确做法 |
|---|---|---|---|
| 固定 token | SET lock "1" NX EX 10 |
释放时无法校验持有者 | SET lock UUID NX EX 10 |
| 两步加锁 | SETNX lock 1 → EXPIRE lock 10 |
两步之间宕机=死锁 | SET lock token NX EX 10 |
| 直接 DEL | DEL lock |
可能误删别人的锁 | Lua: if GET==token then DEL |
| 无业务兜底 | 只依赖 Redis 锁做唯一性 | Redis 锁可能因主从切换丢失 | 数据库唯一约束+幂等表 |
| 锁内长耗时 | 锁内查数据库、调外部 API | 超过 TTL 后锁自动释放 | 锁只包短流程,长任务用状态机 |
| 锁 key 无前缀 | lock:1 |
无法识别归属 | mall:lock:order:{id} |
| 释放锁后继续操作 | unlock() 后继续用锁保护的数据 |
数据可能已被其他线程修改 | 锁内一次性完成数据操作,释放后不再使用 |
扩展阅读:关于 Redlock 的学术争议,推荐阅读 Martin Kleppmann 的《How to do distributed locking》和 antirez 的回应《Is Redlock safe?》。这场辩论的核心启示是:分布式系统中没有银弹——任何依赖于"时间"和"网络"假设的方案都有边界条件。在 Redis 锁的使用上,记住三点核心法则:原子加锁(一条命令完成 SET+EXPIRE)、校验释放(Lua 比较 token)、业务兜底(数据库唯一约束)。这三点做到,95% 的 Redis 锁场景就已经安全了。锁是工程工具,不是学术命题——实践中的正确性比理论上的完美更重要。
6.5 锁选型决策流程总结
你需要分布式锁吗?
├── 单进程内 → synchronized / ReentrantLock 足够
├── 跨进程 + 性能敏感 + 可容忍极低概率失效 → Redis 锁
├── 跨进程 + 强一致要求 + 资金安全 → ZooKeeper / etcd
└── 跨进程 + 最终一致性 → 数据库唯一约束 + 乐观锁
记住,最好的锁是没有锁——如果业务可以通过幂等设计或数据库唯一约束避免并发冲突,那比任何分布式锁都更可靠。
4. 项目总结
4.1 锁的黄金法则
| 原则 | 说明 |
|---|---|
| 原子加锁 | SET key token NX EX ttl,不能分两步 |
| 唯一令牌 | value 必须是 UUID,不能是固定值 |
| Lua 释放 | 先 GET 比较再 DEL,不能直接 DEL |
| 业务兜底 | 锁只降低并发冲突概率,真正的一致性靠数据库唯一约束 |
| TTL 合理 | TTL 略大于 P99 执行时间,不能是业务补偿时间 |
4.2 Redis 锁的"不能"清单
| 不能做的事 | 原因 |
|---|---|
| 不能替代数据库事务 | Redis 锁没有回滚能力 |
| 不能锁跨系统的资源 | 锁只保护 Redis 客户端之间的互斥 |
| 不能在锁内做慢操作 | 业务超过 TTL 后锁自动释放 |
| 不能依赖锁做资金安全 | 资金安全必须以数据库事务为准 |
| 不能把锁 TTL 当业务超时 | 锁 TTL 是避免死锁,不是业务 SLA |
4.3 思考题
-
设计题:你需要在分布式环境中实现一个"定时任务只在一台机器上执行"的能力。如果使用 Redis 锁,应该怎么设计锁的 key、TTL 和续期策略?如果执行任务的机器宕机,如何保证锁被释放?
-
辩论题:如果你的公司目前使用单实例 Redis 锁保护订单提交,CTO 听说 Redlock 更安全,要求全部迁移。你会怎么回应?请从成本、收益和风险三个维度分析。
(答案提示将在下一章末尾给出。)
上一章思考题答案提示(第17章):
第1题(Bloom Filter 内存+兜底):100 万数据+0.1% 误判率 ≈ 1.7MB 内存。即使 Bloom Filter 误判放行,后续还有第三层"空值缓存"兜底——数据库返回 null 时会写入短 TTL 的空标记,后续请求在 TTL 内不会重复穿透。
第2题(逻辑过期不可接受的场景):① 支付金额展示——用户看到的金额必须是实时的;② 库存数量——虽然可以短暂不一致但可能导致超卖;③ 用户实名认证状态——认证通过后必须立即生效。
延伸阅读与资源
Redis 8 实战精讲:从 CRUD 到源码,构建高可用缓存系统
Java 工程师进阶:从 JVM 生产排障到OpenJDK原理
MySQL Server 9从入门到进阶的实战之旅
Nacos 3.x 注册配置中心实战修炼:从入门到源码扩展
Elasticsearch从入门到进阶的实战之旅
RabbitMQ从入门到进阶的实战之旅:从单机到大促高可用架构
Celery 入门到进阶之路:从异步任务到自研调度平台
LangGraph 生产级实战进阶:从零到生产级Agent工作流开发
Dify 从入门到源码:LLM 应用平台实战修炼
从零到生产级:FastAPI 异步高并发、源码与 SRE 实战
实战SQLAlchemy 2.0: 从 CRUD 到生产级架构
从零打造企业级 AI 助手:LangChain RAG、Agent 与生产实战
后端工程师 AI 转型课:Ollama 私有化大模型从入门到生产
MongoDB 实战进阶与内核修炼
NumPy 从入门到生产落地:全链路实战指南(科学计算/向量化/性能调优)
Milvus向量数据库实战修炼:从 0 到 1 精通向量检索与生产落地
Python 3实战精进:从脚本到高并发订单引擎
python入门:Rquests从菜鸟脚本到企业级SDK的网络实战圣经

微信公众号: 架构师日常笔记 欢迎关注!
浙公网安备 33010602011771号