面试官:如何实现分布式锁?
候选人:用Redis的SETNX!
面试官:如果Redis挂了怎么办?锁超时了业务还没执行完怎么办?
候选人:😰💦(这...)
别慌!今天我们深入剖析分布式锁的三大流派,从原理到实战全搞定!
🎬 开篇:为什么需要分布式锁?
单机时代(一个人的江湖)
// JVM内的synchronized,轻松搞定
public synchronized void deductStock() {
int stock = getStock();
if (stock > 0) {
setStock(stock - 1);
}
}
分布式时代(群雄争霸)
用户1请求 → 服务器A (JVM1) ─┐
用户2请求 → 服务器B (JVM2) ─┤→ 同一个商品库存
用户3请求 → 服务器C (JVM3) ─┘
问题:synchronized只能锁住单个JVM,无法跨服务器!
结果:超卖!😱
解决方案:需要一个所有服务器都能访问的"锁"!
🥇 第一章:Redis分布式锁 - 速度之王
方案一:基础版(不要用!❌)
// 错误示例:只用SETNX
public boolean tryLock(String key) {
return redisTemplate.opsForValue().setIfAbsent(key, "locked");
}
public void unlock(String key) {
redisTemplate.delete(key);
}
// 问题:如果程序崩溃,锁永远不会释放!💀
方案二:加超时(还不够!⚠️)
// 改进一点:加上过期时间
public boolean tryLock(String key, long expireTime) {
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(key, "locked");
if (result) {
// 设置过期时间
redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
return true;
}
return false;
}
// 问题:SETNX和EXPIRE不是原子操作!
// 如果SETNX成功后,程序崩溃,还是会死锁!
方案三:原子操作(还不够!⚠️)
// 再改进:SET命令同时设置值和过期时间(原子操作)
public boolean tryLock(String key, long expireSeconds) {
return redisTemplate.opsForValue()
.setIfAbsent(key, "locked", expireSeconds, TimeUnit.SECONDS);
}
public void unlock(String key) {
redisTemplate.delete(key);
}
// 问题:任何人都能删除锁!
// 场景:
// 1. 线程A获取锁
// 2. 线程A业务执行超时,锁过期自动释放
// 3. 线程B获取锁
// 4. 线程A业务完成,删除锁(但删的是线程B的锁!)
🌟 方案四:完整版(推荐!✅)
@Service
public class RedisDistributedLock {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String LOCK_PREFIX = "distributed:lock:";
/**
* 加锁
* @param lockKey 锁的key
* @param requestId 请求ID(唯一标识,用于释放锁时校验)
* @param expireTime 过期时间(秒)
* @return 是否成功
*/
public boolean tryLock(String lockKey, String requestId, long expireTime) {
String key = LOCK_PREFIX + lockKey;
// SET key value NX EX expireTime
// NX:只在key不存在时设置
// EX:设置过期时间(秒)
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(key, requestId, expireTime, TimeUnit.SECONDS);
return Boolean.TRUE.equals(result);
}
/**
* 释放锁(使用Lua脚本保证原子性)
*/
public boolean unlock(String lockKey, String requestId) {
String key = LOCK_PREFIX + lockKey;
// Lua脚本:先比较value,相同才删除
String luaScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(luaScript);
redisScript.setResultType(Long.class);
Long result = redisTemplate.execute(
redisScript,
Collections.singletonList(key),
requestId
);
return Long.valueOf(1).equals(result);
}
}
// 使用示例
@Service
public class StockService {
@Autowired
private RedisDistributedLock distributedLock;
public void deductStock(Long productId) {
String lockKey = "stock:" + productId;
String requestId = UUID.randomUUID().toString();
try {
// 尝试加锁,超时时间10秒
if (distributedLock.tryLock(lockKey, requestId, 10)) {
try {
// 业务逻辑
int stock = getStock(productId);
if (stock > 0) {
setStock(productId, stock - 1);
}
} finally {
// 释放锁
distributedLock.unlock(lockKey, requestId);
}
} else {
throw new RuntimeException("获取锁失败");
}
} catch (Exception e) {
log.error("扣减库存失败", e);
throw e;
}
}
}
🎭 生活比喻:共享单车
1. 扫码开锁(tryLock):
- 你扫码时,系统记录是"你"锁的车(requestId)
- 设置30分钟超时(expireTime)
2. 骑行:
- 只有你能骑这辆车
- 其他人扫码会显示"车辆使用中"
3. 还车(unlock):
- 你还车时,系统检查是不是你锁的
- 只有你能还车,别人无法替你还
4. 超时处理:
- 如果你骑了超过30分钟还没还车
- 系统自动解锁(防止忘记还车导致永久锁定)
🔥 进阶:Redisson实现(自动续期!)
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://localhost:6379")
.setPassword("password");
return Redisson.create(config);
}
}
@Service
public class StockService {
@Autowired
private RedissonClient redissonClient;
public void deductStock(Long productId) {
String lockKey = "stock:" + productId;
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试加锁,最多等待10秒,锁30秒后自动释放
boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (locked) {
try {
// Redisson的看门狗机制:
// 如果业务还在执行,会自动续期锁的过期时间!
Thread.sleep(40000); // 模拟长时间业务
// 锁不会过期,因为看门狗会每10秒续期一次!
// 业务逻辑
int stock = getStock(productId);
if (stock > 0) {
setStock(productId, stock - 1);
}
} finally {
lock.unlock(); // 手动释放锁
}
}
} catch (InterruptedException e) {
log.error("获取锁失败", e);
}
}
}
📊 Redisson看门狗机制
时间轴:
T0: 获取锁,过期时间30秒
T10: 看门狗检查:业务还在执行 → 续期到40秒
T20: 看门狗检查:业务还在执行 → 续期到50秒
T30: 看门狗检查:业务还在执行 → 续期到60秒
T35: 业务完成,手动释放锁
优点:不用担心业务执行时间超过锁过期时间!
🌐 进阶:RedLock算法(多Redis实例)
问题:单个Redis挂了怎么办?
方案:RedLock(使用多个独立的Redis实例)
算法:
1. 获取当前时间(毫秒)
2. 依次尝试从N个Redis实例获取锁
3. 如果从超过半数(N/2+1)的实例获取到锁,且总耗时<锁过期时间,则成功
4. 否则,释放所有已获取的锁
示例(5个Redis实例):
Redis1: ✅ 成功
Redis2: ❌ 失败
Redis3: ✅ 成功
Redis4: ✅ 成功(3/5 > 半数)
Redis5: ❌ 失败
→ 获取锁成功!
@Service
public class RedLockService {
@Autowired
private RedissonClient redisson1;
@Autowired
private RedissonClient redisson2;
@Autowired
private RedissonClient redisson3;
@Autowired
private RedissonClient redisson4;
@Autowired
private RedissonClient redisson5;
public void businessWithRedLock(Long productId) {
String lockKey = "stock:" + productId;
// 创建RedLock(需要至少3个实例成功才算获取锁)
RLock lock1 = redisson1.getLock(lockKey);
RLock lock2 = redisson2.getLock(lockKey);
RLock lock3 = redisson3.getLock(lockKey);
RLock lock4 = redisson4.getLock(lockKey);
RLock lock5 = redisson5.getLock(lockKey);
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3, lock4, lock5);
try {
// 尝试获取锁
boolean locked = redLock.tryLock(10, 30, TimeUnit.SECONDS);
if (locked) {
try {
// 业务逻辑
deductStock(productId);
} finally {
redLock.unlock();
}
}
} catch (InterruptedException e) {
log.error("获取RedLock失败", e);
}
}
}
⚖️ Redis锁的优缺点
✅ 优点
- 性能极高 👍👍👍
- 内存操作,QPS可达10万+
- 实现简单
- 几行代码搞定
- 支持自动过期
- 不怕死锁
❌ 缺点
- 可靠性问题
- Redis挂了,锁就失效
- 主从切换可能丢失锁
- 时钟依赖
- RedLock依赖系统时钟,时钟漂移可能出问题
性能评分:⭐⭐⭐⭐⭐ (5/5)
可靠性评分:⭐⭐⭐ (3/5)
适用场景:高性能、允许偶尔失效
🐘 第二章:Zookeeper分布式锁 - 可靠之王
核心原理:临时顺序节点 + Watch机制
ZooKeeper锁的实现:
/locks
├── lock_0000000001 (客户端A创建)
├── lock_0000000002 (客户端B创建)
└── lock_0000000003 (客户端C创建)
规则:
1. 序号最小的客户端获得锁
2. 其他客户端监听前一个节点的删除事件
3. 前一个节点删除后,自己变成最小,获得锁
🎭 生活比喻:医院排队叫号
1. 取号:
- 你到医院,取号机给你一个号码(创建临时顺序节点)
- 号码:A001、A002、A003...
2. 等待:
- 显示屏显示"正在叫号:A001"(序号最小的获得锁)
- 你拿的是A003,需要等待
3. 监听:
- 你只需要关注A002什么时候叫完(监听前一个节点)
- 不用一直盯着显示屏
4. 轮到你:
- A002叫完了(前一个节点删除)
- 你变成最小号码,轮到你了(获得锁)
5. 看完病离开:
- 你离开医院(释放锁,删除节点)
- A004自动获得锁
💻 代码实现
@Component
public class ZookeeperDistributedLock {
@Autowired
private CuratorFramework zkClient;
private static final String LOCK_PATH = "/distributed-locks";
/**
* 获取锁
*/
public InterProcessMutex getLock(String lockName) {
String lockPath = LOCK_PATH + "/" + lockName;
return new InterProcessMutex(zkClient, lockPath);
}
}
@Service
public class StockService {
@Autowired
private ZookeeperDistributedLock distributedLock;
public void deductStock(Long productId) {
InterProcessMutex lock = distributedLock.getLock("stock:" + productId);
try {
// 尝试获取锁,最多等待10秒
if (lock.acquire(10, TimeUnit.SECONDS)) {
try {
// 业务逻辑
int stock = getStock(productId);
if (stock > 0) {
setStock(productId, stock - 1);
}
} finally {
// 释放锁
lock.release();
}
} else {
throw new RuntimeException("获取锁超时");
}
} catch (Exception e) {
log.error("扣减库存失败", e);
throw new RuntimeException(e);
}
}
}
🔍 Zookeeper锁的工作流程
1. 客户端A请求锁:
创建 /locks/stock_0000000001 (临时顺序节点)
检查自己是否最小 → 是 → 获得锁 ✅
2. 客户端B请求锁:
创建 /locks/stock_0000000002
检查自己是否最小 → 否 → 等待
监听 /locks/stock_0000000001 的删除事件
3. 客户端C请求锁:
创建 /locks/stock_0000000003
检查自己是否最小 → 否 → 等待
监听 /locks/stock_0000000002 的删除事件
4. 客户端A释放锁:
删除 /locks/stock_0000000001
触发客户端B的Watch事件
5. 客户端B被唤醒:
检查自己是否最小 → 是 → 获得锁 ✅
优点:避免"惊群效应"(每个客户端只监听前一个节点)
🛡️ 临时节点的保护机制
场景:客户端A获取锁后,服务器宕机
传统方案:
锁永久存在 → 死锁 ❌
Zookeeper方案:
1. 客户端A与Zookeeper保持心跳(Session)
2. 服务器宕机 → 心跳断开 → Session失效
3. Zookeeper自动删除临时节点
4. 客户端B自动获得锁 ✅
完美解决死锁问题!
⚖️ Zookeeper锁的优缺点
✅ 优点
- 可靠性高 👍👍👍
- 基于Paxos/ZAB协议,强一致性
- 临时节点自动删除,不会死锁
- 公平性好
- 先来先得(FIFO)
- 避免饥饿问题
- 无需手动设置超时
- 自动检测客户端存活
❌ 缺点
- 性能较低
- 写操作需要过半节点确认
- QPS只有几千
- 运维复杂
- 需要部署Zookeeper集群
- 需要监控Session超时
- 依赖外部系统
- Zookeeper挂了,锁服务不可用
性能评分:⭐⭐⭐ (3/5)
可靠性评分:⭐⭐⭐⭐⭐ (5/5)
适用场景:可靠性要求高、并发量中等
🗄️ 第三章:数据库分布式锁 - 传统之选
方案一:基于唯一索引
-- 创建锁表
CREATE TABLE distributed_lock (
lock_key VARCHAR(64) NOT NULL COMMENT '锁的key',
request_id VARCHAR(64) NOT NULL COMMENT '请求ID',
expire_time DATETIME NOT NULL COMMENT '过期时间',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (lock_key)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分布式锁表';
@Repository
public interface DistributedLockMapper {
/**
* 尝试获取锁(利用主键唯一性)
*/
@Insert("INSERT INTO distributed_lock (lock_key, request_id, expire_time) " +
"VALUES (#{lockKey}, #{requestId}, #{expireTime})")
int tryLock(@Param("lockKey") String lockKey,
@Param("requestId") String requestId,
@Param("expireTime") Date expireTime);
/**
* 释放锁(只能释放自己的锁)
*/
@Delete("DELETE FROM distributed_lock " +
"WHERE lock_key = #{lockKey} AND request_id = #{requestId}")
int unlock(@Param("lockKey") String lockKey,
@Param("requestId") String requestId);
/**
* 清理过期锁(定时任务)
*/
@Delete("DELETE FROM distributed_lock WHERE expire_time < NOW()")
int cleanExpiredLocks();
}
@Service
public class DatabaseDistributedLock {
@Autowired
private DistributedLockMapper lockMapper;
/**
* 尝试获取锁
*/
public boolean tryLock(String lockKey, String requestId, long expireSeconds) {
try {
Date expireTime = new Date(System.currentTimeMillis() + expireSeconds * 1000);
int rows = lockMapper.tryLock(lockKey, requestId, expireTime);
return rows > 0;
} catch (DuplicateKeyException e) {
// 主键冲突,说明锁已存在
return false;
}
}
/**
* 释放锁
*/
public boolean unlock(String lockKey, String requestId) {
int rows = lockMapper.unlock(lockKey, requestId);
return rows > 0;
}
}
// 定时清理过期锁
@Component
public class LockCleanTask {
@Autowired
private DistributedLockMapper lockMapper;
@Scheduled(fixedRate = 60000) // 每分钟执行一次
public void cleanExpiredLocks() {
int count = lockMapper.cleanExpiredLocks();
if (count > 0) {
log.info("清理过期锁:{} 个", count);
}
}
}
方案二:基于悲观锁(FOR UPDATE)
@Service
public class DatabasePessimisticLock {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 获取锁(使用FOR UPDATE)
*/
@Transactional
public void executeWithLock(String lockKey, Runnable business) {
// 1. 查询锁记录,加行锁
String sql = "SELECT * FROM distributed_lock WHERE lock_key = ? FOR UPDATE";
try {
jdbcTemplate.queryForMap(sql, lockKey);
} catch (EmptyResultDataAccessException e) {
// 记录不存在,插入
jdbcTemplate.update(
"INSERT INTO distributed_lock (lock_key, request_id) VALUES (?, ?)",
lockKey, UUID.randomUUID().toString()
);
}
// 2. 执行业务逻辑(在事务中,持有行锁)
business.run();
// 3. 事务提交,自动释放锁
}
}
// 使用示例
@Service
public class StockService {
@Autowired
private DatabasePessimisticLock pessimisticLock;
public void deductStock(Long productId) {
String lockKey = "stock:" + productId;
pessimisticLock.executeWithLock(lockKey, () -> {
// 业务逻辑
int stock = getStock(productId);
if (stock > 0) {
setStock(productId, stock - 1);
}
});
}
}
🎭 生活比喻:停车位
方案一:停车牌(唯一索引)
1. 你开车到停车场
2. 尝试放置你的停车牌(INSERT)
3. 成功 → 你占用了车位 ✅
4. 失败(车位已被占用) → 等待或离开 ❌
5. 离开时取走停车牌(DELETE)
方案二:栏杆(FOR UPDATE)
1. 你开车到停车场
2. 栏杆挡住你(SELECT FOR UPDATE 加行锁)
3. 你停车、办事(执行业务)
4. 离开时栏杆抬起(事务提交,释放锁)
5. 下一辆车才能进入
⚖️ 数据库锁的优缺点
✅ 优点
- 实现简单
- 不需要额外部署中间件
- 利用现有的数据库
- 可靠性高
- 基于事务,强一致性
- 易于理解和调试
❌ 缺点
- 性能最差 👎
- 依赖数据库,QPS有限
- 会增加数据库负载
- 死锁风险
- FOR UPDATE可能导致死锁
- 单点故障
- 数据库挂了,锁服务不可用
性能评分:⭐⭐ (2/5)
可靠性评分:⭐⭐⭐⭐ (4/5)
适用场景:并发量低、已有数据库
📊 第四章:三种方案全方位对比
| 维度 | Redis | Zookeeper | 数据库 |
|---|---|---|---|
| 性能 | ⭐⭐⭐⭐⭐ 10万QPS | ⭐⭐⭐ 几千QPS | ⭐⭐ 几百QPS |
| 可靠性 | ⭐⭐⭐ 中等 | ⭐⭐⭐⭐⭐ 很高 | ⭐⭐⭐⭐ 高 |
| 实现复杂度 | 简单 | 中等 | 简单 |
| 运维成本 | 中等 | 高 | 低 |
| 死锁防护 | 超时自动释放 | Session自动删除 | 需定时清理 |
| 公平性 | 不保证 | FIFO公平 | 不保证 |
| 可重入 | 需自己实现 | Curator自带 | 需自己实现 |
| 阻塞锁 | 需轮询 | Watch机制 | 需轮询 |
🎯 选型决策树
graph TD
A[开始选型] --> B{对性能要求极高?}
B -->|是| C{能接受偶尔失效?}
B -->|否| D{需要强一致性?}
C -->|是| E[选择Redis]
C -->|否| F[选择Zookeeper]
D -->|是| G[选择Zookeeper]
D -->|否| H{并发量大吗?}
H -->|大| E
H -->|小| I[选择数据库]
🎮 实际场景选择
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 秒杀系统 | Redis | 需要极高QPS |
| 订单号生成 | Zookeeper | 需要严格顺序 |
| 定时任务调度 | Zookeeper | 需要可靠选主 |
| 库存扣减 | Redis + Redisson | 高性能+看门狗 |
| 分布式ID生成 | Zookeeper | 顺序性+可靠性 |
| 配置变更 | Zookeeper | Watch机制 |
| 小型项目 | 数据库 | 简单够用 |
💼 第五章:生产环境最佳实践
实践1:混合使用
/**
* 高可用方案:Redis主 + Zookeeper备
*/
@Service
public class HybridDistributedLock {
@Autowired
private RedissonClient redisson;
@Autowired
private CuratorFramework zkClient;
public void executeWithLock(String lockKey, Runnable business) {
RLock redisLock = redisson.getLock(lockKey);
InterProcessMutex zkLock = new InterProcessMutex(zkClient, "/locks/" + lockKey);
try {
// 优先使用Redis锁(高性能)
if (redisLock.tryLock(5, 30, TimeUnit.SECONDS)) {
try {
business.run();
} finally {
redisLock.unlock();
}
} else {
// Redis获取失败,降级使用Zookeeper(高可靠)
log.warn("Redis锁获取失败,降级使用Zookeeper锁");
if (zkLock.acquire(10, TimeUnit.SECONDS)) {
try {
business.run();
} finally {
zkLock.release();
}
}
}
} catch (Exception e) {
log.error("执行业务失败", e);
throw new RuntimeException(e);
}
}
}
实践2:锁续期(看门狗)
/**
* 自己实现锁续期(如果不用Redisson)
*/
@Service
public class LockRenewalService {
@Autowired
private StringRedisTemplate redisTemplate;
private final ScheduledExecutorService scheduler =
Executors.newScheduledThreadPool(1);
public void executeWithAutoRenewal(String lockKey, Runnable business) {
String requestId = UUID.randomUUID().toString();
String key = "lock:" + lockKey;
// 获取锁,30秒过期
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(key, requestId, 30, TimeUnit.SECONDS);
if (!locked) {
throw new RuntimeException("获取锁失败");
}
// 启动续期任务(每10秒续期一次)
ScheduledFuture<?> renewalTask = scheduler.scheduleAtFixedRate(() -> {
// 检查锁是否还是自己的
String currentValue = redisTemplate.opsForValue().get(key);
if (requestId.equals(currentValue)) {
// 续期30秒
redisTemplate.expire(key, 30, TimeUnit.SECONDS);
log.debug("锁续期成功:{}", lockKey);
}
}, 10, 10, TimeUnit.SECONDS);
try {
// 执行业务
business.run();
} finally {
// 停止续期任务
renewalTask.cancel(true);
// 释放锁
releaseLock(key, requestId);
}
}
private void releaseLock(String key, String requestId) {
String luaScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptText(luaScript);
script.setResultType(Long.class);
redisTemplate.execute(script, Collections.singletonList(key), requestId);
}
}
实践3:锁降级
/**
* 获取锁失败时的降级策略
*/
@Service
public class StockServiceWithDegradation {
@Autowired
private RedisDistributedLock distributedLock;
@Autowired
private StockMapper stockMapper;
public void deductStock(Long productId, Integer quantity) {
String lockKey = "stock:" + productId;
String requestId = UUID.randomUUID().toString();
// 尝试获取锁,等待时间0秒(不等待)
if (distributedLock.tryLock(lockKey, requestId, 30)) {
try {
// 方案1:获取锁成功,正常扣减
doDeductStock(productId, quantity);
} finally {
distributedLock.unlock(lockKey, requestId);
}
} else {
// 方案2:获取锁失败,降级为数据库乐观锁
log.warn("分布式锁获取失败,降级使用乐观锁");
deductStockWithOptimisticLock(productId, quantity);
}
}
private void deductStockWithOptimisticLock(Long productId, Integer quantity) {
int maxRetry = 3;
for (int i = 0; i < maxRetry; i++) {
Stock stock = stockMapper.selectById(productId);
if (stock.getQuantity() < quantity) {
throw new StockNotEnoughException();
}
// 使用版本号乐观锁
int rows = stockMapper.deductWithVersion(
productId, quantity, stock.getVersion()
);
if (rows > 0) {
return; // 成功
}
// 失败,重试
}
throw new RuntimeException("扣减库存失败");
}
}
🎓 第六章:面试高分回答
问题:如何实现分布式锁?
标准回答(STAR法则):
S(场景):"我们的电商系统在秒杀活动时,多个服务器会同时扣减库存,存在超卖风险。"
T(任务):"需要实现一个分布式锁,保证同一时刻只有一个请求能扣减库存。"
A(方案):"我们对比了Redis、Zookeeper、数据库三种方案,最终选择了Redis + Redisson:
- 使用Redisson的RLock,自带看门狗机制,不用担心锁超时
- 使用Lua脚本保证释放锁的原子性
- 为了高可用,部署了Redis Sentinel主从模式
- 核心代码只需要几行:
RLock lock = redisson.getLock(lockKey); lock.lock(); try { // 业务逻辑 } finally { lock.unlock(); }"
R(结果):"上线后,秒杀场景下没有出现超卖,性能也很好,锁的QPS达到了5万+。"
常见追问及回答
Q1:Redis主从切换时,锁会丢失吗?
A:会的!
场景:
1. 客户端A在主节点上获取锁
2. 主节点挂掉,还未同步到从节点
3. 从节点升级为主节点
4. 客户端B在新主节点上获取同一个锁(成功)
5. 两个客户端同时持有锁!
解决方案:
1. 使用RedLock算法(多个独立的Redis实例)
2. 使用Zookeeper替代
3. 业务层面做幂等性校验
Q2:Redisson的看门狗是怎么实现的?
A:
1. 默认锁过期时间30秒
2. 启动一个后台线程(看门狗)
3. 每隔10秒(lockWatchdogTimeout/3)检查一次
4. 如果锁还存在,续期到30秒
5. 直到业务执行完,手动unlock时停止续期
源码:
org.redisson.RedissonLock#scheduleExpirationRenewal
Q3:如何避免死锁?
A:三种方案都有应对措施:
1. Redis:设置过期时间,自动释放
2. Zookeeper:临时节点,Session失效自动删除
3. 数据库:定时任务清理过期锁
另外,代码层面:
- 一定要在finally中释放锁
- 使用try-with-resources
- 设置合理的超时时间
🎁 总结:一句话记住
- Redis:跑车(快但可能翻车)🏎️
- Zookeeper:坦克(慢但非常稳)🚜
- 数据库:自行车(简单够用)🚲
📚 扩展阅读
- Redisson文档:github.com/redisson/re…
- Curator文档:curator.apache.org/
- Martin Kleppmann对RedLock的质疑:martin.kleppmann.com/2016/02/08/…
记住:选择合适的方案,比追求完美更重要!🎯
祝你面试顺利!💪✨