从秒杀系统崩溃到支撑千万流量:我的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. 压力测试必须做:提前发现性能瓶颈

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

相关推荐
我登哥MVP5 小时前
Apache Tomcat 详解
java·笔记·tomcat
SXJR5 小时前
Spring前置准备(八)——ConfigurableApplicationContext和DefaultListableBeanFactory的区别
java·后端·spring
G探险者5 小时前
深入理解 KeepAlive:从 TCP 到连接池再到线程池的多层语义解析
后端
Takklin5 小时前
Java 面试笔记:深入理解接口
后端·面试
右子6 小时前
理解响应式设计—理念、实践与常见误解
前端·后端·响应式设计
濑户川6 小时前
深入理解Django 视图与 URL 路由:从基础到实战
后端·python·django
武子康6 小时前
大数据-120 - Flink滑动窗口(Sliding Window)详解:原理、应用场景与实现示例 基于时间驱动&基于事件驱动
大数据·后端·flink
IccBoY6 小时前
Java采用easyexcel组件进行excel表格单元格的自动合并
java·开发语言·excel
用户281113022216 小时前
分布式事务总结
后端