从秒杀系统崩溃到支撑千万流量:我的Redis分布式锁踩坑实录

前言

"线上秒杀活动开始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);
            }
        }
    }
}

从崩溃到稳定的架构演进

最终架构:

  1. 前端层:按钮防重复点击,随机延迟提交
  2. 网关层:用户级别限流,恶意请求过滤
  3. 服务层:Redis集群 + Redisson分布式锁
  4. 数据层:Redis库存预扣 + MySQL最终落地
  5. 监控层:锁状态实时监控,自动告警

成果展示:

  • 峰值QPS:从崩溃到支撑5万+
  • 超卖率:从无法统计到0%
  • 响应时间:从30秒到200毫秒内
  • 系统可用性:从崩溃到99.99%

血泪教训总结

  1. 不要重复造轮子:优先使用成熟的Redisson框架
  2. 锁的粒度要细:按业务维度拆分锁
  3. 一定要设置超时:避免死锁发生
  4. 监控不能少:实时掌握锁的使用情况
  5. 压力测试必须做:提前发现性能瓶颈

思考题:在你的项目中,还有哪些场景适合使用分布式锁?如何避免锁成为系统瓶颈?

相关推荐
葫芦和十三44 分钟前
图解 MongoDB 21|选举与 failover:Primary 是怎么选出来的
后端·mongodb·agent
GetcharZp1 小时前
26k Star 开源内网穿透神器 NetBird,一分钟实现全球设备互联!
后端
考虑考虑2 小时前
Mybatis实现批量插入
java·后端·mybatis
咖啡八杯3 小时前
GoF设计模式——中介者模式
java·后端·spring·设计模式
lizhongxuan5 小时前
多Agent之间的区别
后端
青石路6 小时前
记一次多JDK版本问题的排查,一坑套一坑,差点没爬上来
java
杨充7 小时前
1.面向对象设计思想
后端
IT_陈寒7 小时前
Java的Date类又坑了我一次,改用时间戳真香
前端·人工智能·后端
systemPro8 小时前
2.6亿条设备数据,历史查询从超时到50ms,我做了什么
后端
要阿尔卑斯吗8 小时前
提示词优化启示:为什么“按顺序输出“比“关键度评分“更有效
后端