秒杀系统中:如何防止超卖和库存超扣?

作为一名拥有八年 Java 后端开发经验的技术人,我参与过多个大型电商秒杀系统的设计与优化。在这篇博客中,我将分享如何设计一个支持高并发的秒杀系统,并重点探讨如何防止超卖和库存超扣问题。

业务场景分析

秒杀系统的核心特点是:短时间内大量用户争抢少量商品,导致瞬时并发量极高。典型的业务场景包括:

  1. 瞬时高并发:活动开始瞬间,并发请求可能达到数万甚至数十万
  2. 库存有限:秒杀商品数量通常较少,一般在几十到几千件
  3. 高一致性要求:库存数量必须精确,不能出现超卖(卖出超过库存数量的商品)
  4. 防刷需求:需要防止机器人和恶意用户刷单

架构设计概览

一个高并发的秒杀系统通常采用以下分层架构:

  1. 流量层:CDN、负载均衡、网关限流
  2. 应用层:秒杀服务、订单服务、库存服务
  3. 数据层:缓存(Redis)、数据库(MySQL)
  4. 辅助服务:消息队列、分布式锁、监控系统

超卖和库存超扣问题分析

超卖是指系统卖出的商品数量超过了实际库存数量,例如库存只有 100 件,但最终却卖出了 200 件。库存超扣则是指库存被多次扣减,导致库存变为负数。这两个问题是秒杀系统中最核心的挑战,可能由以下原因导致:

  1. 数据库行锁竞争:传统的数据库事务在高并发下会成为性能瓶颈
  2. 缓存与数据库不一致:缓存与数据库的数据同步不及时
  3. 并发处理不当:多个请求同时处理库存扣减,导致判断失误
  4. 重试机制不完善:失败请求的重试可能导致重复扣减

解决方案详解

1. 限流与防刷

在流量入口处进行限流和防刷,减少无效请求到达核心服务:

typescript 复制代码
/**
 * 限流与防刷服务
 * 使用令牌桶算法和验证码防止恶意请求
 */
@Service
public class RateLimitService {
    
    // 基于Google Guava的令牌桶实现
    private final RateLimiter rateLimiter = RateLimiter.create(1000.0); // 每秒生成1000个令牌
    
    // 验证码服务
    @Autowired
    private CaptchaService captchaService;
    
    // 用户访问频率控制(滑动窗口算法)
    private final LoadingCache<String, AtomicInteger> accessCountCache = Caffeine.newBuilder()
        .expireAfterWrite(1, TimeUnit.MINUTES)
        .build(key -> new AtomicInteger(0));
    
    /**
     * 检查请求是否允许通过限流
     */
    public boolean allowRequest(String userId) {
        // 1. 检查用户访问频率
        if (checkUserAccessFrequency(userId)) {
            return false;
        }
        
        // 2. 检查验证码(必要时)
        if (needCaptcha(userId)) {
            // 验证验证码逻辑
            if (!verifyCaptcha(userId)) {
                return false;
            }
        }
        
        // 3. 获取令牌
        return rateLimiter.tryAcquire(500, TimeUnit.MILLISECONDS); // 等待500ms获取令牌
    }
    
    private boolean checkUserAccessFrequency(String userId) {
        AtomicInteger count = accessCountCache.get(userId);
        int accessCount = count.incrementAndGet();
        
        // 限制用户每分钟最多请求100次
        return accessCount > 100;
    }
    
    private boolean needCaptcha(String userId) {
        // 根据用户行为动态判断是否需要验证码
        // 例如:短时间内频繁访问但未成功购买的用户
        return false;
    }
    
    private boolean verifyCaptcha(String userId) {
        // 验证用户输入的验证码
        // 实际实现中需要从前端获取验证码并验证
        return true;
    }
}

2. 库存预加载与扣减

将库存信息提前加载到 Redis 中,利用 Redis 的原子操作扣减库存:

java 复制代码
/**
 * 库存服务实现
 * 负责库存的预加载、扣减和回滚操作
 */
@Service
public class InventoryServiceImpl implements InventoryService {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    @Autowired
    private InventoryMapper inventoryMapper;
    
    // 库存Key前缀
    private static final String INVENTORY_KEY_PREFIX = "seckill:inventory:";
    
    // 库存锁定Key前缀
    private static final String INVENTORY_LOCKED_KEY_PREFIX = "seckill:locked_inventory:";
    
    @Override
    public void preloadInventory(Long productId, int quantity) {
        // 1. 加载库存到Redis
        String inventoryKey = getInventoryKey(productId);
        redisTemplate.opsForValue().set(inventoryKey, String.valueOf(quantity));
        
        // 2. 初始化锁定库存为0
        String lockedKey = getLockedInventoryKey(productId);
        redisTemplate.opsForValue().set(lockedKey, "0");
        
        log.info("商品ID={} 库存预加载完成,数量={}", productId, quantity);
    }
    
    @Override
    public boolean deductInventory(Long productId, int count) {
        String inventoryKey = getInventoryKey(productId);
        
        // 使用Lua脚本保证原子性
        String script = 
            "local stock = tonumber(redis.call('get', KEYS[1]))\n" +
            "if stock >= tonumber(ARGV[1]) then\n" +
            "    redis.call('decrby', KEYS[1], ARGV[1])\n" +
            "    return 1\n" +
            "else\n" +
            "    return 0\n" +
            "end";
        
        // 执行Lua脚本
        Long result = redisTemplate.execute(
            new DefaultRedisScript<>(script, Long.class),
            Collections.singletonList(inventoryKey),
            String.valueOf(count)
        );
        
        return result != null && result == 1;
    }
    
    @Override
    public void revertInventory(Long productId, int count) {
        String inventoryKey = getInventoryKey(productId);
        
        // 增加库存(原子操作)
        redisTemplate.opsForValue().increment(inventoryKey, count);
        
        log.info("商品ID={} 库存回滚完成,数量={}", productId, count);
    }
    
    @Override
    public int getAvailableInventory(Long productId) {
        String inventoryKey = getInventoryKey(productId);
        String value = redisTemplate.opsForValue().get(inventoryKey);
        
        return value != null ? Integer.parseInt(value) : 0;
    }
    
    private String getInventoryKey(Long productId) {
        return INVENTORY_KEY_PREFIX + productId;
    }
    
    private String getLockedInventoryKey(Long productId) {
        return INVENTORY_LOCKED_KEY_PREFIX + productId;
    }
}

3. 分布式锁与幂等性保障

使用 Redis 分布式锁确保同一商品的库存扣减操作串行化,并保证操作的幂等性:

typescript 复制代码
/**
 * 分布式锁服务
 * 使用Redis实现分布式锁,确保库存操作的原子性
 */
@Service
public class DistributedLockServiceImpl implements DistributedLockService {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    // 锁过期时间(毫秒)
    private static final long LOCK_EXPIRE_TIME = 30000;
    
    @Override
    public boolean acquireLock(String lockKey, String requestId) {
        // 使用setIfAbsent方法(即SETNX)尝试获取锁
        Boolean result = redisTemplate.opsForValue().setIfAbsent(
            lockKey, 
            requestId, 
            LOCK_EXPIRE_TIME, 
            TimeUnit.MILLISECONDS
        );
        
        return result != null && result;
    }
    
    @Override
    public void releaseLock(String lockKey, String requestId) {
        // 使用Lua脚本保证原子性释放锁
        String script = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then\n" +
            "    return redis.call('del', KEYS[1])\n" +
            "else\n" +
            "    return 0\n" +
            "end";
        
        redisTemplate.execute(
            new DefaultRedisScript<>(script, Long.class),
            Collections.singletonList(lockKey),
            requestId
        );
    }
    
    @Override
    public String generateRequestId() {
        // 生成唯一请求ID
        return UUID.randomUUID().toString();
    }
}

/**
 * 幂等性服务
 * 确保同一请求不会被重复处理
 */
@Service
public class IdempotencyService {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    // 幂等性Key前缀
    private static final String IDEMPOTENCY_KEY_PREFIX = "seckill:idempotency:";
    
    // 幂等性记录有效期(秒)
    private static final long IDEMPOTENCY_EXPIRE_TIME = 60 * 60; // 1小时
    
    @Override
    public boolean checkAndSet(String requestId) {
        String key = IDEMPOTENCY_KEY_PREFIX + requestId;
        
        // 使用SETNX原子操作检查并设置请求ID
        Boolean result = redisTemplate.opsForValue().setIfAbsent(
            key, 
            "processed", 
            IDEMPOTENCY_EXPIRE_TIME, 
            TimeUnit.SECONDS
        );
        
        return result != null && result;
    }
}

4. 秒杀核心服务

整合上述服务,实现秒杀核心逻辑:

java 复制代码
/**
 * 秒杀服务实现
 * 处理秒杀请求的核心业务逻辑
 */
@Service
public class SeckillServiceImpl implements SeckillService {
    
    @Autowired
    private InventoryService inventoryService;
    
    @Autowired
    private DistributedLockService lockService;
    
    @Autowired
    private IdempotencyService idempotencyService;
    
    @Autowired
    private OrderService orderService;
    
    @Override
    public SeckillResult seckill(SeckillRequest request) {
        // 1. 验证请求参数
        if (!validateRequest(request)) {
            return SeckillResult.failure("无效请求参数");
        }
        
        // 2. 检查幂等性
        if (!idempotencyService.checkAndSet(request.getRequestId())) {
            return SeckillResult.failure("请勿重复提交请求");
        }
        
        Long productId = request.getProductId();
        String lockKey = "seckill:lock:" + productId;
        String requestId = lockService.generateRequestId();
        
        try {
            // 3. 获取分布式锁
            boolean locked = lockService.acquireLock(lockKey, requestId);
            if (!locked) {
                return SeckillResult.failure("系统繁忙,请稍后再试");
            }
            
            try {
                // 4. 检查库存
                int availableStock = inventoryService.getAvailableInventory(productId);
                if (availableStock <= 0) {
                    return SeckillResult.failure("商品已售罄");
                }
                
                // 5. 扣减库存
                boolean success = inventoryService.deductInventory(productId, 1);
                if (!success) {
                    return SeckillResult.failure("商品已售罄");
                }
                
                // 6. 创建订单
                Order order = createOrder(request);
                
                return SeckillResult.success(order.getOrderId());
            } finally {
                // 7. 释放锁
                lockService.releaseLock(lockKey, requestId);
            }
        } catch (Exception e) {
            // 异常处理:回滚库存
            inventoryService.revertInventory(productId, 1);
            log.error("秒杀处理异常", e);
            return SeckillResult.failure("系统异常,请稍后再试");
        }
    }
    
    private boolean validateRequest(SeckillRequest request) {
        // 验证请求参数逻辑
        return request != null && 
               request.getUserId() != null && 
               request.getProductId() != null;
    }
    
    private Order createOrder(SeckillRequest request) {
        // 创建订单逻辑
        Order order = new Order();
        order.setOrderId(UUID.randomUUID().toString());
        order.setUserId(request.getUserId());
        order.setProductId(request.getProductId());
        order.setStatus(OrderStatus.CREATED);
        order.setCreateTime(new Date());
        
        // 保存订单到数据库
        orderService.saveOrder(order);
        
        return order;
    }
}

数据库层的保障措施

虽然我们在应用层做了很多防超卖的措施,但数据库层的最终一致性保障也是必不可少的:

less 复制代码
/**
 * 数据库层库存扣减
 * 使用数据库乐观锁确保库存扣减的原子性
 */
@Repository
public interface InventoryMapper {
    
    /**
     * 查询库存信息
     */
    @Select("SELECT * FROM inventory WHERE product_id = #{productId}")
    Inventory getInventoryByProductId(@Param("productId") Long productId);
    
    /**
     * 使用乐观锁扣减库存
     * @param productId 商品ID
     * @param count 扣减数量
     * @param version 当前版本号
     * @return 影响行数
     */
    @Update("UPDATE inventory " +
            "SET stock = stock - #{count}, version = version + 1 " +
            "WHERE product_id = #{productId} " +
            "AND stock >= #{count} " +
            "AND version = #{version}")
    int deductInventory(@Param("productId") Long productId, 
                        @Param("count") int count, 
                        @Param("version") int version);
}

性能优化与扩展性设计

针对高并发秒杀系统,还需要考虑以下性能优化和扩展性设计:

  1. 读写分离:对读多写少的场景,使用数据库读写分离
  2. 异步处理:订单创建、库存扣减等操作采用异步队列处理
  3. 熔断降级:使用 Sentinel 或 Hystrix 对非核心服务进行熔断降级
  4. 多级缓存:使用本地缓存(Caffeine)+ 分布式缓存(Redis)减少外部依赖
  5. 分库分表:根据业务增长情况,考虑分库分表提高数据库吞吐量

监控与告警

完善的监控系统是保证高并发系统稳定运行的关键:

  1. 性能监控:监控 QPS、响应时间、错误率等核心指标
  2. 库存监控:实时监控库存数量变化,设置安全阈值
  3. 系统资源监控:监控服务器 CPU、内存、网络等资源使用情况
  4. 告警机制:设置告警规则,如库存不足、QPS 突增、错误率上升等

总结

设计一个高并发的秒杀系统并防止超卖和库存超扣是一个复杂的工程问题,需要从多个层面进行保障:

  1. 流量控制:在入口处进行限流和防刷,减少无效请求

  2. 缓存优先:利用 Redis 等缓存进行库存预加载和快速扣减

  3. 原子操作:使用 Redis 的原子操作或分布式锁保证库存扣减的原子性

  4. 幂等性保障:确保同一请求不会被重复处理

  5. 数据库最终一致性:在数据库层使用乐观锁等机制确保数据最终一致性

相关推荐
无限大68 分钟前
🎯 算法精讲:二分查找(一)—— 基础原理与实现 🔍
后端
Re2759 分钟前
为什么ThreadLocal内存泄露:从原理到实践
后端
玄妙尽在颠倒间9 分钟前
雪花算法:从 64 位到 128 位 —— 超大规模分布式 ID 生成器的设计与实现
后端·算法
Code季风12 分钟前
Spring 异常处理最佳实践:从基础配置到生产级应用
java·spring boot·spring
回家路上绕了弯12 分钟前
Java 堆深度解析:内存管理的核心战场
java·jvm
Code季风13 分钟前
Spring IoC 容器性能提升指南:启动速度与运行效率优化策略
java·spring·性能优化
谦行22 分钟前
前端视角 Java Web 入门手册 5.10:真实世界 Web 开发—— 单元测试
java·spring boot·后端
hhua012334 分钟前
理解“无界队列”与“有界队列”及其适用场景
java·队列
LZQqqqqo37 分钟前
C# 接口(interface 定义接口的关键字)
java·开发语言·c#
寒水馨1 小时前
Java 9 新特性解析
java·开发语言·新特性·java9·jdk9