高并发秒杀系统实战(Redis+Lua分布式锁防超卖与库存扣减优化)

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
简介: 秒杀系统面临瞬时高并发、资源竞争和数据一致性挑战。传统方案如数据库锁或应用层锁存在性能瓶颈或分布式问题,而基于Redis的分布式锁与Lua脚本原子操作成为高效解决方案。通过Redis的`SETNX`实现分布式锁,结合Lua脚本完成库存扣减,确保操作原子性并大幅提升性能(QPS从120提升至8,200)。此外,分段库存策略、多级限流及服务降级机制进一步优化系统稳定性。最佳实践包括分层防控、黄金扣减法则与容灾设计,强调根据业务特性灵活组合技术手段以应对高并发场景。

1 秒杀场景

电商秒杀场景具有瞬时高并发资源竞争激烈数据一致性要求高三大特征。当数万用户同时抢购少量商品时(如1000件商品承受10万QPS),系统面临多重挑战:

  1. 超卖风险:多个并发请求同时查询库存并扣减,导致库存扣减数超过实际库存量
  2. 数据库压力:传统数据库难以承受瞬时高并发读写
  3. 用户体验:响应延迟或服务崩溃导致用户请求失败
// 典型超卖代码示例
@GetMapping("/reduce")
public String reduce() {
   
    int stock = (Integer) redisTemplate.opsForValue().get("stock");
    if (stock > 0) {
   
        redisTemplate.opsForValue().set("stock", stock - 1);
        return "秒杀成功";
    }
    return "库存不足";
}

上述代码在并发场景下必然出现超卖问题。当多个请求同时通过stock > 0检查后,每个请求都会执行减库存操作,导致库存变为负数。

2 超卖问题根源分析

2.1 并发冲突的本质

超卖问题的本质是非原子性操作在分布式环境下的并发冲突:

当用户A和用户B同时查询库存(均为10),用户A先扣减5个库存成功,用户B随后扣减6个库存导致库存变为-1,即发生了超卖。

2.2 传统解决方案的局限

  1. 数据库悲观锁SELECT FOR UPDATE导致性能瓶颈
  2. 应用层锁:仅限单机有效,无法解决分布式问题
  3. 乐观锁:高并发下重试率高,用户体验差

3 Redis分布式锁深度优化

3.1 基础分布式锁实现

基于Redis的SETNX命令实现分布式锁:

public class RedisLock {
   
    private RedisTemplate<String, String> redisTemplate;

    public boolean tryLock(String key, String value, long expireTime) {
   
        return redisTemplate.opsForValue()
                .setIfAbsent(key, value, expireTime, TimeUnit.SECONDS);
    }

    public boolean releaseLock(String key, String 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<>(luaScript, Long.class);
        Long result = redisTemplate.execute(redisScript, Collections.singletonList(key), value);
        return result != null && result == 1;
    }
}

此实现解决了锁释放安全性问题,确保只有锁持有者才能释放锁。

3.2 锁续命机制

为解决长任务执行时锁过期问题,需实现锁续命机制:

具体实现:

private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

public void renewLock(String key, String value, long renewInterval) {
   
    scheduler.scheduleAtFixedRate(() -> {
   
        String currentValue = redisTemplate.opsForValue().get(key);
        if (value.equals(currentValue)) {
   
            redisTemplate.expire(key, 30, TimeUnit.SECONDS);
        }
    }, 0, renewInterval, TimeUnit.SECONDS);
}

3.3 集群环境下的RedLock算法

在Redis集群环境下,为避免主从切换导致锁丢失,需采用RedLock算法:

image.png

实现要点:

  1. 同时向5个独立Redis实例请求锁
  2. 当获取超过半数的锁(≥3)且总耗时小于锁有效期时视为成功
  3. 锁的实际有效时间 = 初始有效时间 - 获取锁耗时

4 基于Lua的原子库存扣减

4.1 Lua脚本原子性原理

Redis执行Lua脚本具有原子性特性:

  • 脚本作为一个整体执行
  • 执行期间不会插入其他命令
  • 无需显式加锁即可实现事务

4.2 库存扣减Lua脚本

-- KEYS[1]: 商品库存key
-- ARGV[1]: 扣减数量
local key = KEYS[1]
local val = tonumber(ARGV[1])

-- 获取当前库存
local stock = tonumber(redis.call('get', key))

if stock <= 0 then
    return -1 -- 库存不足
end

if stock < val then
    return -2 -- 库存不足指定数量
end

-- 扣减库存
local newStock = redis.call('decrby', key, val)

if newStock >= 0 then
    return newStock -- 扣减成功
else
    -- 回滚操作
    redis.call('incrby', key, val)
    return -3 -- 扣减后库存为负,操作回滚
end

脚本关键点:

  1. 使用tonumber()转换字符串为数字
  2. 在扣减前进行库存充足性检查
  3. 扣减后出现负库存时自动回滚

4.3 Spring Boot集成Lua脚本

@Configuration
public class LuaScriptConfig {
   

    @Bean
    public DefaultRedisScript<Long> stockDeductScript() {
   
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setLocation(new ClassPathResource("lua/stock_deduct.lua"));
        script.setResultType(Long.class);
        return script;
    }
}

@Service
public class SecKillService {
   
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    @Autowired
    private DefaultRedisScript<Long> stockDeductScript;

    public String handleSecKill(String productId, int quantity) {
   
        List<String> keys = Collections.singletonList("stock:" + productId);
        Long result = redisTemplate.execute(
                stockDeductScript, 
                keys, 
                String.valueOf(quantity));

        if (result == -1 || result == -2) {
   
            return "库存不足";
        } else if (result == -3) {
   
            return "扣减失败,库存不足";
        }

        // 扣减成功,创建订单
        createOrder(productId, quantity);
        return "秒杀成功";
    }
}

5 库存扣减优化策略

5.1 缓存与数据库双写一致性

image.png

操作要点

  1. 先操作缓存再操作数据库
  2. 数据库操作异步化(通过消息队列)
  3. 设置库存操作流水表用于对账

5.2 库存分段策略

借鉴ConcurrentHashMap的分段锁思想,适用于超高并发场景:

// 初始化分段库存
public void initSegmentStock(String productId, int totalStock, int segment) {
   
    int base = totalStock / segment;
    for (int i = 0; i < segment; i++) {
   
        int segmentStock = (i == segment - 1) ? 
                base + totalStock % segment : base;
        redisTemplate.opsForValue().set(
                "stock:" + productId + ":" + i, 
                segmentStock);
    }
}

// 分段扣减
public boolean segmentDeduct(String productId, int quantity) {
   
    int segment = getSegmentCount(productId);
    for (int i = 0; i < segment; i++) {
   
        String key = "stock:" + productId + ":" + i;
        Long result = redisTemplate.execute(
                stockDeductScript, 
                Collections.singletonList(key), 
                String.valueOf(quantity));
        if (result != null && result >= 0) {
   
            return true;
        }
    }
    return false;
}

适用场景

  • 库存量较大(≥1000)
  • 允许少量库存碎片(最后一段可能有剩余库存)

6 限流与降级机制

6.1 多级限流策略

  1. 网关层限流:基于令牌桶算法控制全局流量

    http {
         
        limit_req_zone $binary_remote_addr zone=secKill:10m rate=100r/s;
    
        server {
         
            location /seckill {
         
                limit_req zone=secKill burst=50;
                proxy_pass http://backend_server;
            }
        }
    }
    
  2. 服务层限流:基于Redis实现分布式限流

    public boolean tryAcquire(String key, int max, int timeout) {
         
        String luaScript = "local current = redis.call('incr', KEYS[1])\n" +
                "if current == 1 then\n" +
                "    redis.call('expire', KEYS[1], ARGV[1])\n" +
                "end\n" +
                "if current > tonumber(ARGV[2]) then\n" +
                "    return 0\n" +
                "else\n" +
                "    return 1\n" +
                "end";
    
        DefaultRedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
        Long result = redisTemplate.execute(script, 
                Collections.singletonList(key), 
                String.valueOf(timeout), 
                String.valueOf(max));
        return result != null && result == 1;
    }
    
  3. 商品维度限流:针对热点商品设置独立阈值

6.2 服务降级策略

根据系统负载动态调整服务能力:

image.png

降级措施

  1. 关闭商品详情页的推荐模块
  2. 简化购物车逻辑
  3. 关闭评论功能
  4. 仅保留核心交易链路

7 性能测试与优化效果

7.1 测试环境配置

组件 配置
应用服务器 4核8G × 3台(负载均衡)
Redis 6核16G 主从哨兵模式
数据库 MySQL 8.0 读写分离
压测工具 JMeter 1000并发线程

7.2 不同方案性能对比

方案 吞吐量(QPS) 平均响应时间(ms) 超卖率
数据库悲观锁 120 450 0%
应用层锁 980 105 单机0%
Redis+Lua原子操作 5,600 18 0%
库存分段+Redis Lua 8,200 12 0%

7.3 资源消耗对比

测试结果表明:

  1. Redis+Lua方案相比传统方案吞吐量提升46倍
  2. 响应时间从450ms降至18ms,提升25倍
  3. CPU利用率降低50%以上

8 总结与最佳实践

经过实战验证的秒杀系统最佳实践:

  1. 分层防控

    • 前端:按钮置灰、验证码、请求间隔限制
    • 网关:请求过滤、全局限流
    • 服务:分布式锁、Lua原子操作
    • 数据层:缓存预热、异步写库
  2. 库存扣减黄金法则
    image.png

  1. 容灾设计

    • Redis宕机时降级到数据库乐观锁
    • 设置库存操作开关
    • 实现自动对账机制
  2. 持续优化方向

    • 库存碎片回收(定期合并分段库存)
    • 基于机器学习的动态限流
    • 区域化库存分配

总结:在高并发秒杀系统中,原子性操作分层限流是两大核心支柱。Redis结合Lua脚本提供了高效的原子操作能力,而合理设计的分布式限流策略则是系统稳定的保障。实际生产中,没有银弹解决方案,需要根据业务特性和流量规模灵活组合多种技术手段。

相关文章
|
1月前
|
缓存 监控 Cloud Native
Java Solon v3.2.0 高并发与低内存实战指南之解决方案优化
本文深入解析了Java Solon v3.2.0框架的实战应用,聚焦高并发与低内存消耗场景。通过响应式编程、云原生支持、内存优化等特性,结合API网关、数据库操作及分布式缓存实例,展示其在秒杀系统中的性能优势。文章还提供了Docker部署、监控方案及实际效果数据,助力开发者构建高效稳定的应用系统。代码示例详尽,适合希望提升系统性能的Java开发者参考。
59 4
Java Solon v3.2.0 高并发与低内存实战指南之解决方案优化
Java 大视界 -- 基于 Java 的大数据分布式存储在视频监控数据管理中的应用优化(170)
本文围绕基于 Java 的大数据分布式存储在视频监控数据管理中的应用展开,分析管理现状与挑战,阐述技术应用,结合案例和代码给出实操方案。
|
2月前
|
NoSQL 算法 安全
redis分布式锁在高并发场景下的方案设计与性能提升
本文探讨了Redis分布式锁在主从架构下失效的问题及其解决方案。首先通过CAP理论分析,Redis遵循AP原则,导致锁可能失效。针对此问题,提出两种解决方案:Zookeeper分布式锁(追求CP一致性)和Redlock算法(基于多个Redis实例提升可靠性)。文章还讨论了可能遇到的“坑”,如加从节点引发超卖问题、建议Redis节点数为奇数以及持久化策略对锁的影响。最后,从性能优化角度出发,介绍了减少锁粒度和分段锁的策略,并结合实际场景(如下单重复提交、支付与取消订单冲突)展示了分布式锁的应用方法。
189 3
|
2月前
|
存储 NoSQL Java
从扣减库存场景来讲讲redis分布式锁中的那些“坑”
本文从一个简单的库存扣减场景出发,深入分析了高并发下的超卖问题,并逐步优化解决方案。首先通过本地锁解决单机并发问题,但集群环境下失效;接着引入Redis分布式锁,利用SETNX命令实现加锁,但仍存在死锁、锁过期等隐患。文章详细探讨了通过设置唯一标识、续命机制等方法完善锁的可靠性,并最终引出Redisson工具,其内置的锁续命和原子性操作极大简化了分布式锁的实现。最后,作者剖析了Redisson源码,揭示其实现原理,并预告后续关于主从架构下分布式锁的应用与性能优化内容。
123 0
|
2月前
|
缓存 NoSQL 关系型数据库
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
|
2月前
|
缓存 NoSQL Java
Redis+Caffeine构建高性能二级缓存
大家好,我是摘星。今天为大家带来的是Redis+Caffeine构建高性能二级缓存,废话不多说直接开始~
366 0
|
2月前
|
消息中间件 缓存 NoSQL
基于Spring Data Redis与RabbitMQ实现字符串缓存和计数功能(数据同步)
总的来说,借助Spring Data Redis和RabbitMQ,我们可以轻松实现字符串缓存和计数的功能。而关键的部分不过是一些"厨房的套路",一旦你掌握了这些套路,那么你就像厨师一样可以准备出一道道饕餮美食了。通过这种方式促进数据处理效率无疑将大大提高我们的生产力。
125 32
|
2月前
|
缓存 NoSQL Java
Redis:现代服务端开发的缓存基石与电商实践-优雅草卓伊凡
Redis:现代服务端开发的缓存基石与电商实践-优雅草卓伊凡
76 5
Redis:现代服务端开发的缓存基石与电商实践-优雅草卓伊凡
|
4月前
|
缓存 监控 NoSQL
Redis--缓存击穿、缓存穿透、缓存雪崩
缓存击穿、缓存穿透和缓存雪崩是Redis使用过程中可能遇到的常见问题。理解这些问题的成因并采取相应的解决措施,可以有效提升系统的稳定性和性能。在实际应用中,应根据具体场景,选择合适的解决方案,并持续监控和优化缓存策略,以应对不断变化的业务需求。
216 29
|
4月前
|
缓存 NoSQL Java
Redis应用—8.相关的缓存框架
本文介绍了Ehcache和Guava Cache两个缓存框架及其使用方法,以及如何自定义缓存。主要内容包括:Ehcache缓存框架、Guava Cache缓存框架、自定义缓存。总结:Ehcache适合用作本地缓存或与Redis结合使用,Guava Cache则提供了更灵活的缓存管理和更高的并发性能。自定义缓存可以根据具体需求选择不同的数据结构和引用类型来实现特定的缓存策略。
227 16
Redis应用—8.相关的缓存框架

相关产品

  • 云数据库 Tair(兼容 Redis)