前言
"线上秒杀活动开始3秒,系统直接崩溃!"
------ 一次价值百万的Redis分布式锁故障复盘
那个让我彻夜难眠的夜晚
去年双十一,我们团队负责一个知名品牌的线上秒杀活动。活动开始前,我自信地告诉老板:"系统绝对没问题,我用Redis分布式锁保证不会超卖!"
结果,活动开始第3秒,监控大屏全线飘红:
- 订单服务响应时间从50ms飙升到30秒
- Redis连接池全部占满
- 80%的请求返回"系统繁忙"
- 最终只成交了23单,库存还有大量剩余
第一版"教科书式"的错误实现
错误代码示范:
java
@Component
public class SeckillService {
@Autowired
private RedisTemplate redisTemplate;
public boolean seckill(Long productId, Long userId) {
String lockKey = "seckill_lock:" + productId;
try {
// 问题1:简单的setIfAbsent,没有设置过期时间
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "locked");
if (Boolean.TRUE.equals(result)) {
// 问题2:业务逻辑执行期间锁不会自动释放
return handleSeckillBusiness(productId, userId);
}
return false;
} finally {
// 问题3:直接删除,可能删除其他线程的锁
redisTemplate.delete(lockKey);
}
}
}
Redis分布式锁的三大致命陷阱
陷阱1:死锁 - 锁没有过期时间
问题:如果获取锁的线程在执行业务逻辑时崩溃,锁永远无法释放
解决方案:设置合理的过期时间
java
// 正确姿势:原子操作设置锁和过期时间
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "locked", 30, TimeUnit.SECONDS);
陷阱2:误删 - 删了别人的锁
问题:线程A执行时间超过锁过期时间,锁自动释放;线程B获取锁;此时线程A执行完成,删除了线程B的锁
解决方案:给锁设置唯一标识
java
@Component
public class RedisDistributedLock {
public boolean tryLock(String lockKey, long expireSeconds) {
String lockValue = UUID.randomUUID().toString();
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, expireSeconds, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(result)) {
// 将锁值保存在ThreadLocal中,用于后续释放
lockValueHolder.set(lockValue);
return true;
}
return false;
}
public void unlock(String lockKey) {
String currentLockValue = lockValueHolder.get();
String lockValueInRedis = (String) redisTemplate.opsForValue().get(lockKey);
// 只有锁的值匹配时才删除
if (currentLockValue != null && currentLockValue.equals(lockValueInRedis)) {
redisTemplate.delete(lockKey);
}
}
}
陷阱3:锁续期 - 业务执行时间不确定
问题:设置过期时间太短,业务没执行完锁就释放了;设置太长,线程崩溃后其他线程要等待很久
解决方案:守护线程自动续期
java
@Component
public class RedisLockWithRenewal {
private final ScheduledExecutorService scheduler =
Executors.newScheduledThreadPool(10);
public boolean tryLockWithRenewal(String lockKey, long expireSeconds) {
String lockValue = UUID.randomUUID().toString();
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, expireSeconds, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(result)) {
// 启动续期任务
ScheduledFuture<?> renewalTask = scheduler.scheduleAtFixedRate(
() -> renewLock(lockKey, lockValue, expireSeconds),
expireSeconds / 3, // 第一次续期间隔
expireSeconds / 3, // 后续续期间隔
TimeUnit.SECONDS
);
renewalTasks.put(lockKey, renewalTask);
return true;
}
return false;
}
private void renewLock(String lockKey, String lockValue, long expireSeconds) {
// 只有锁还存在且值匹配时才续期
String currentValue = (String) redisTemplate.opsForValue().get(lockKey);
if (lockValue.equals(currentValue)) {
redisTemplate.expire(lockKey, expireSeconds, TimeUnit.SECONDS);
}
}
}
生产环境终极解决方案
方案1:Redisson框架(推荐)
java
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379")
.setDatabase(0);
return Redisson.create(config);
}
}
@Service
public class SeckillServiceV2 {
@Autowired
private RedissonClient redissonClient;
public boolean seckill(Long productId, Long userId) {
String lockKey = "seckill_lock:" + productId;
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试获取锁,最多等待100ms,锁持有时间30s
if (lock.tryLock(100, 30000, TimeUnit.MILLISECONDS)) {
return handleSeckillBusiness(productId, userId);
}
return false;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
方案2:Lua脚本保证原子性
java
@Component
public class LuaScriptLock {
private final String UNLOCK_SCRIPT =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
public boolean unlockWithLua(String lockKey, String lockValue) {
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptText(UNLOCK_SCRIPT);
script.setResultType(Long.class);
Long result = redisTemplate.execute(script,
Collections.singletonList(lockKey), lockValue);
return result != null && result == 1;
}
}
性能优化实战经验
1. 锁粒度控制
错误 :整个秒杀活动一个锁
正确:按商品ID分锁,不同商品可以并行处理
2. 等待策略优化
java
// 非阻塞尝试
if (lock.tryLock()) {
// ...
}
// 有限时间等待
if (lock.tryLock(50, TimeUnit.MILLISECONDS)) {
// ...
}
// 快速失败+重试机制
int retryCount = 0;
while (retryCount < 3) {
if (lock.tryLock(10, TimeUnit.MILLISECONDS)) {
try {
return doBusiness();
} finally {
lock.unlock();
}
}
retryCount++;
Thread.sleep(20); // 指数退避更好
}
3. 监控与告警
java
@Component
public class LockMonitor {
@Scheduled(fixedRate = 60000) // 每分钟检查一次
public void monitorLock() {
// 检查是否有锁持有时间过长
Set<String> keys = redisTemplate.keys("seckill_lock:*");
for (String key : keys) {
Long ttl = redisTemplate.getExpire(key);
if (ttl != null && ttl > 180) { // 持有超过3分钟
sendAlert("锁持有时间过长: " + key);
}
}
}
}
从崩溃到稳定的架构演进
最终架构:
- 前端层:按钮防重复点击,随机延迟提交
- 网关层:用户级别限流,恶意请求过滤
- 服务层:Redis集群 + Redisson分布式锁
- 数据层:Redis库存预扣 + MySQL最终落地
- 监控层:锁状态实时监控,自动告警
成果展示:
- 峰值QPS:从崩溃到支撑5万+
- 超卖率:从无法统计到0%
- 响应时间:从30秒到200毫秒内
- 系统可用性:从崩溃到99.99%
血泪教训总结
- 不要重复造轮子:优先使用成熟的Redisson框架
- 锁的粒度要细:按业务维度拆分锁
- 一定要设置超时:避免死锁发生
- 监控不能少:实时掌握锁的使用情况
- 压力测试必须做:提前发现性能瓶颈
思考题:在你的项目中,还有哪些场景适合使用分布式锁?如何避免锁成为系统瓶颈?