避免超卖

避免商品超卖是电商系统,尤其是秒杀场景下的核心挑战。下面我将详细说明几种具体的技术实现方案,从基础到高级层层递进。

1. 数据库层面解决方案

1.1 悲观锁方案

-- 使用 SELECT ... FOR UPDATE 锁定记录
BEGIN TRANSACTION;
SELECT stock FROM products WHERE id = ? FOR UPDATE;
-- 检查库存并扣减
UPDATE products SET stock = stock - 1 WHERE id = ? AND stock > 0;
COMMIT;

缺点:性能较差,在高并发下容易成为瓶颈。

1.2 乐观锁方案

-- 基于版本号的乐观锁
UPDATE products 
SET stock = stock - 1, version = version + 1 
WHERE id = ? AND stock > 0 AND version = ?;

-- 或者直接基于库存值的乐观锁
UPDATE products 
SET stock = stock - 1 
WHERE id = ? AND stock = current_stock AND stock > 0;

优点:性能较好,适合读多写少的场景。

1.3 直接库存扣减

-- 最简单的库存扣减,利用数据库的行级锁
UPDATE products 
SET stock = stock - 1 
WHERE id = ? AND stock > 0;

通过返回值判断是否成功:

int affectedRows = productMapper.decreaseStock(productId, quantity);
if (affectedRows > 0) {
    // 扣减成功,创建订单
} else {
    // 库存不足
}

2. Redis 原子操作方案

2.1 基于 Lua 脚本的原子操作

-- decrease_stock.lua
local key = KEYS[1]
local decrease = tonumber(ARGV[1])
local current = tonumber(redis.call('GET', key))

if current == nil then
    return -1  -- 商品不存在
end

if current < decrease then
    return -2  -- 库存不足
end

redis.call('DECRBY', key, decrease)
return current - decrease  -- 返回剩余库存

Java 调用示例:

public boolean decreaseStock(String productId, int quantity) {
    String script = "local key = KEYS[1] local decrease = tonumber(ARGV[1]) " +
                   "local current = tonumber(redis.call('GET', key)) " +
                   "if current == nil then return -1 end " +
                   "if current < decrease then return -2 end " +
                   "redis.call('DECRBY', key, decrease) return current - decrease";
    
    Long result = jedis.eval(script, 
        Collections.singletonList("stock:" + productId), 
        Collections.singletonList(String.valueOf(quantity)));
    
    return result >= 0;
}

2.2 Redis 事务方案

public boolean decreaseStockWithWatch(String productId, int quantity) {
    String stockKey = "stock:" + productId;
    
    try (Jedis jedis = jedisPool.getResource()) {
        // 监视库存key
        jedis.watch(stockKey);
        
        int currentStock = Integer.parseInt(jedis.get(stockKey));
        if (currentStock < quantity) {
            jedis.unwatch();
            return false;
        }
        
        // 开始事务
        Transaction transaction = jedis.multi();
        transaction.decrBy(stockKey, quantity);
        
        // 执行事务
        List<Object> results = transaction.exec();
        
        // 如果事务执行失败(被其他客户端修改),results为null
        return results != null && !results.isEmpty();
    }
}

3. 分布式锁方案

3.1 Redisson 分布式锁

@Component
public class InventoryService {
    
    @Autowired
    private RedissonClient redisson;
    
    public boolean decreaseStock(String productId, int quantity) {
        String lockKey = "lock:stock:" + productId;
        RLock lock = redisson.getLock(lockKey);
        
        try {
            // 尝试获取锁,等待时间5秒,锁过期时间10秒
            boolean locked = lock.tryLock(5, 10, TimeUnit.SECONDS);
            if (!locked) {
                return false;
            }
            
            // 执行库存扣减
            int currentStock = getCurrentStock(productId);
            if (currentStock >= quantity) {
                updateStock(productId, currentStock - quantity);
                return true;
            }
            return false;
            
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

4. 令牌桶 + Redis 方案(推荐)

4.1 预扣库存方案

@Service
public class SeckillService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    /**
     * 初始化秒杀库存
     */
    public void initSeckillStock(String productId, int totalStock) {
        String stockKey = "seckill:stock:" + productId;
        String soldKey = "seckill:sold:" + productId;
        
        redisTemplate.opsForValue().set(stockKey, totalStock);
        redisTemplate.opsForValue().set(soldKey, 0);
    }
    
    /**
     * 尝试预扣库存
     */
    public boolean tryPreDeductStock(String productId, String userId) {
        String stockKey = "seckill:stock:" + productId;
        String soldKey = "seckill:sold:" + productId;
        String userOrderKey = "seckill:user_order:" + productId + ":" + userId;
        
        // 使用Lua脚本保证原子性
        String luaScript = 
            "local stockKey = KEYS[1] " +
            "local soldKey = KEYS[2] " +
            "local userOrderKey = KEYS[3] " +
            "local userId = ARGV[1] " +
            " " +
            "-- 检查用户是否已经下单 " +
            "if redis.call('EXISTS', userOrderKey) == 1 then " +
            "    return -1 " +
            "end " +
            " " +
            "-- 检查库存 " +
            "local stock = tonumber(redis.call('GET', stockKey)) " +
            "if stock <= 0 then " +
            "    return -2 " +
            "end " +
            " " +
            "-- 扣减库存 " +
            "redis.call('DECR', stockKey) " +
            "redis.call('INCR', soldKey) " +
            " " +
            "-- 标记用户已下单,设置过期时间(如15分钟) " +
            "redis.call('SETEX', userOrderKey, 900, '1') " +
            " " +
            "return 1";
        
        Long result = redisTemplate.execute(
            new DefaultRedisScript<>(luaScript, Long.class),
            Arrays.asList(stockKey, soldKey, userOrderKey),
            userId
        );
        
        return result == 1;
    }
    
    /**
     * 确认扣减库存(创建订单后)
     */
    public boolean confirmDeductStock(String productId, String userId) {
        // 将预扣库存正式扣减,写入数据库
        // 这里可以异步处理,保证最终一致性
        return true;
    }
    
    /**
     * 回滚预扣库存
     */
    public boolean rollbackPreDeductStock(String productId, String userId) {
        String stockKey = "seckill:stock:" + productId;
        String soldKey = "seckill:sold:" + productId;
        String userOrderKey = "seckill:user_order:" + productId + ":" + userId;
        
        String luaScript = 
            "local stockKey = KEYS[1] " +
            "local soldKey = KEYS[2] " +
            "local userOrderKey = KEYS[3] " +
            " " +
            "-- 检查用户是否有预扣记录 " +
            "if redis.call('EXISTS', userOrderKey) == 0 then " +
            "    return -1 " +
            "end " +
            " " +
            "-- 恢复库存 " +
            "redis.call('INCR', stockKey) " +
            "redis.call('DECR', soldKey) " +
            "redis.call('DEL', userOrderKey) " +
            " " +
            "return 1";
        
        Long result = redisTemplate.execute(
            new DefaultRedisScript<>(luaScript, Long.class),
            Arrays.asList(stockKey, soldKey, userOrderKey)
        );
        
        return result == 1;
    }
}

5. 消息队列异步方案

5.1 基于 RocketMQ 的事务消息

@Service
public class SeckillOrderService {
    
    @Autowired
    private RocketMQTemplate rocketMQTemplate;
    
    /**
     * 发送秒杀请求
     */
    public boolean sendSeckillRequest(String productId, String userId) {
        SeckillMessage message = new SeckillMessage(productId, userId);
        
        // 发送事务消息
        TransactionSendResult result = rocketMQTemplate.sendMessageInTransaction(
            "seckill-topic",
            MessageBuilder.withPayload(message).build(),
            null
        );
        
        return result.getLocalTransactionState() == LocalTransactionState.COMMIT_MESSAGE;
    }
    
    /**
     * 事务消息监听器 - 执行本地事务
     */
    @RocketMQTransactionListener
    class SeckillTransactionListener implements RocketMQLocalTransactionListener {
        
        @Override
        public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
            try {
                SeckillMessage message = (SeckillMessage) ((GenericMessage) msg).getPayload();
                
                // 预扣库存
                boolean success = tryPreDeductStock(message.getProductId(), message.getUserId());
                
                if (success) {
                    return RocketMQLocalTransactionState.COMMIT;
                } else {
                    return RocketMQLocalTransactionState.ROLLBACK;
                }
            } catch (Exception e) {
                return RocketMQLocalTransactionState.ROLLBACK;
            }
        }
        
        @Override
        public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
            // 检查本地事务状态
            SeckillMessage message = (SeckillMessage) ((GenericMessage) msg).getPayload();
            // 根据业务逻辑检查订单状态
            return RocketMQLocalTransactionState.COMMIT;
        }
    }
}

6. 完整架构方案

在实际生产环境中,通常采用分层校验 + 多种技术组合的方案:

用户请求 → 限流层 → 缓存校验层 → 队列层 → 服务层 → 数据库层

6.1 分层防护架构

@Component
public class SeckillProtectionLayer {
    
    /**
     * 分层校验秒杀请求
     */
    public boolean validateSeckillRequest(String productId, String userId) {
        // 第一层:基础校验
        if (!validateBasicInfo(productId, userId)) {
            return false;
        }
        
        // 第二层:频率限制
        if (!rateLimit(userId, productId)) {
            return false;
        }
        
        // 第三层:库存预检(快速失败)
        if (!preCheckStock(productId)) {
            return false;
        }
        
        // 第四层:Redis原子扣减
        if (!tryPreDeductStock(productId, userId)) {
            return false;
        }
        
        // 第五层:进入消息队列异步处理
        return sendToQueue(productId, userId);
    }
    
    private boolean preCheckStock(String productId) {
        // 快速库存检查,不保证强一致性
        Integer stock = redisTemplate.opsForValue().get("seckill:stock:" + productId);
        return stock != null && stock > 0;
    }
    
    private boolean rateLimit(String userId, String productId) {
        String rateLimitKey = "rate_limit:" + productId + ":" + userId;
        Long count = redisTemplate.opsForValue().increment(rateLimitKey, 1);
        if (count == 1) {
            redisTemplate.expire(rateLimitKey, 1, TimeUnit.SECONDS); // 1秒窗口
        }
        return count <= 5; // 每秒最多5次请求
    }
}

总结

避免超卖的核心技术要点:

  1. 原子性操作:使用数据库行锁、Redis Lua脚本等保证操作的原子性
  2. 预扣库存:在Redis中预扣减,异步同步到数据库
  3. 分层校验:多级防护,快速失败
  4. 流量削峰:使用消息队列缓冲瞬时高并发
  5. 最终一致性:通过异步方式保证数据的最终一致性

在实际项目中,推荐使用 Redis Lua脚本 + 消息队列 的组合方案,既保证了性能,又确保了数据的一致性。

posted @ 2025-10-21 15:12  程煕  阅读(16)  评论(0)    收藏  举报