Skip to content

商城系统经典问题分析

1. 重复下单

产生原因:

  • 前端按钮未禁用,用户连续点击
  • 网络延迟导致用户重复提交
  • 页面刷新或浏览器后退重新提交表单

解决方案:

java
// 接口幂等性 - 使用Redis分布式锁
@PostMapping("/createOrder")
public Result createOrder(@RequestBody OrderDTO orderDTO) {
    String lockKey = "order:" + userId + ":" + System.currentTimeMillis()/1000;
    if (!redisLock.tryLock(lockKey, 10)) {
        return Result.error("请勿重复下单");
    }
    // 订单创建逻辑
}

// 数据库唯一索引约束
ALTER TABLE orders ADD UNIQUE INDEX uk_user_product_time (user_id, product_id, create_time);

2. 重复扣减库存

产生原因:

  • 并发请求同时读取库存数据
  • 缺乏原子性操作保证
  • 分布式环境下数据一致性问题

解决方案:

java
// 数据库乐观锁
@Update("UPDATE product SET stock = stock - #{quantity}, version = version + 1 
         WHERE id = #{productId} AND stock >= #{quantity} AND version = #{version}")
int updateStock(@Param("productId") Long productId, @Param("quantity") Integer quantity, @Param("version") Integer version);

// Redis原子操作
public boolean deductStock(Long productId, Integer quantity) {
    String script = "if redis.call('get', KEYS[1]) >= ARGV[1] then " +
                   "return redis.call('decrby', KEYS[1], ARGV[1]) else return -1 end";
    Long result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), 
                                      Arrays.asList("stock:" + productId), quantity);
    return result >= 0;
}

问题场景分析

典型并发场景:

时间线:
T1: 用户A查询商品库存 = 10
T2: 用户B查询商品库存 = 10  
T3: 用户A扣减库存 -1,库存 = 9
T4: 用户B扣减库存 -1,库存 = 9 (错误!应该是8)

问题根本原因:

  • 读取-修改-写入 操作不是原子性的
  • 并发请求基于相同的"脏数据"进行计算
  • 缺乏并发控制机制

解决方案详解

1. 数据库层面解决

乐观锁方案(推荐)

java
// 商品表增加版本号字段
CREATE TABLE product (
    id BIGINT PRIMARY KEY,
    stock INT NOT NULL,
    version INT NOT NULL DEFAULT 0,
    update_time TIMESTAMP
);

// Service层实现
@Service
public class StockService {
    
    public boolean deductStock(Long productId, Integer quantity) {
        int retryCount = 0;
        int maxRetry = 3;
        
        while (retryCount < maxRetry) {
            // 1. 查询当前库存和版本号
            Product product = productMapper.selectById(productId);
            if (product.getStock() < quantity) {
                throw new BusinessException("库存不足");
            }
            
            // 2. 基于版本号更新库存
            int updateRows = productMapper.updateStockWithVersion(
                productId, quantity, product.getVersion()
            );
            
            if (updateRows > 0) {
                return true; // 更新成功
            }
            
            // 3. 更新失败,重试
            retryCount++;
            try {
                Thread.sleep(10 + new Random().nextInt(20)); // 随机延迟避免活锁
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return false;
            }
        }
        
        throw new BusinessException("系统繁忙,请稍后重试");
    }
}

// Mapper实现
@Update("UPDATE product SET stock = stock - #{quantity}, version = version + 1, update_time = NOW() " +
        "WHERE id = #{productId} AND stock >= #{quantity} AND version = #{version}")
int updateStockWithVersion(@Param("productId") Long productId, 
                          @Param("quantity") Integer quantity, 
                          @Param("version") Integer version);

悲观锁方案

java
// 使用SELECT FOR UPDATE
@Select("SELECT * FROM product WHERE id = #{productId} FOR UPDATE")
Product selectByIdForUpdate(@Param("productId") Long productId);

@Transactional
public boolean deductStockWithPessimisticLock(Long productId, Integer quantity) {
    // 1. 加行锁查询
    Product product = productMapper.selectByIdForUpdate(productId);
    if (product.getStock() < quantity) {
        throw new BusinessException("库存不足");
    }
    
    // 2. 更新库存
    productMapper.updateStock(productId, quantity);
    return true;
}

2. Redis分布式锁方案

基础分布式锁

java
@Component
public class RedisStockService {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    public boolean deductStock(Long productId, Integer quantity) {
        String lockKey = "stock_lock:" + productId;
        String lockValue = UUID.randomUUID().toString();
        
        try {
            // 1. 获取分布式锁
            Boolean locked = redisTemplate.opsForValue()
                .setIfAbsent(lockKey, lockValue, Duration.ofSeconds(10));
            
            if (!locked) {
                throw new BusinessException("系统繁忙,请稍后重试");
            }
            
            // 2. 查询Redis中的库存
            String stockStr = redisTemplate.opsForValue().get("stock:" + productId);
            int currentStock = Integer.parseInt(stockStr);
            
            if (currentStock < quantity) {
                throw new BusinessException("库存不足");
            }
            
            // 3. 扣减库存
            redisTemplate.opsForValue().set("stock:" + productId, 
                String.valueOf(currentStock - quantity));
            
            // 4. 异步同步到数据库
            asyncUpdateDatabase(productId, quantity);
            
            return true;
            
        } finally {
            // 5. 释放锁
            releaseLock(lockKey, lockValue);
        }
    }
    
    private void releaseLock(String lockKey, String lockValue) {
        String script = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "return redis.call('del', KEYS[1]) else return 0 end";
        
        redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
            Arrays.asList(lockKey), lockValue);
    }
}

3. Redis原子操作方案(最优)

Lua脚本实现原子扣减

java
@Component
public class RedisAtomicStockService {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    // Lua脚本保证原子性
    private static final String DEDUCT_STOCK_SCRIPT = 
        "local stock = redis.call('get', KEYS[1]) " +
        "if not stock then " +
        "    return -1 " +  // 商品不存在
        "end " +
        "if tonumber(stock) < tonumber(ARGV[1]) then " +
        "    return -2 " +  // 库存不足
        "end " +
        "return redis.call('decrby', KEYS[1], ARGV[1])"; // 扣减成功,返回剩余库存
    
    public boolean deductStock(Long productId, Integer quantity) {
        String stockKey = "stock:" + productId;
        
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setScriptText(DEDUCT_STOCK_SCRIPT);
        script.setResultType(Long.class);
        
        Long result = redisTemplate.execute(script, 
            Arrays.asList(stockKey), quantity.toString());
        
        if (result == -1) {
            throw new BusinessException("商品不存在");
        } else if (result == -2) {
            throw new BusinessException("库存不足");
        } else if (result >= 0) {
            // 异步同步到数据库
            asyncUpdateDatabase(productId, quantity);
            return true;
        }
        
        return false;
    }
    
    // 异步同步数据库
    @Async
    public void asyncUpdateDatabase(Long productId, Integer quantity) {
        try {
            productMapper.updateStock(productId, quantity);
        } catch (Exception e) {
            // 记录日志,后续补偿
            log.error("同步数据库失败: productId={}, quantity={}", productId, quantity, e);
        }
    }
}

4. 消息队列削峰方案

对于超高并发场景

java
@Component
public class MQStockService {
    
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    // 下单时发送扣库存消息
    public void deductStockAsync(Long productId, Integer quantity, String orderNo) {
        StockDeductMessage message = new StockDeductMessage();
        message.setProductId(productId);
        message.setQuantity(quantity);
        message.setOrderNo(orderNo);
        
        rabbitTemplate.convertAndSend("stock.exchange", "stock.deduct", message);
    }
    
    // 消费者串行处理扣库存
    @RabbitListener(queues = "stock.deduct.queue")
    public void handleStockDeduct(StockDeductMessage message) {
        try {
            boolean success = redisAtomicStockService.deductStock(
                message.getProductId(), message.getQuantity()
            );
            
            if (success) {
                // 通知订单服务库存扣减成功
                orderService.confirmStockDeducted(message.getOrderNo());
            } else {
                // 通知订单服务库存不足,取消订单
                orderService.cancelOrder(message.getOrderNo(), "库存不足");
            }
        } catch (Exception e) {
            // 重试或进入死信队列
            throw new AmqpRejectAndDontRequeueException("扣减库存失败", e);
        }
    }
}

方案对比

方案优点缺点适用场景
数据库乐观锁实现简单,数据一致性强高并发下重试频繁中等并发场景
数据库悲观锁数据强一致性能差,容易死锁低并发场景
Redis分布式锁性能好实现复杂,需处理锁超时高并发场景
Redis原子操作性能最优,实现优雅需要数据同步机制超高并发场景
消息队列系统解耦,削峰填谷实现复杂,最终一致性秒杀等极端场景

生产环境最佳实践

java
@Service
public class ProductStockService {
    
    // 组合方案:Redis + 数据库双重保险
    @Transactional
    public boolean deductStock(Long productId, Integer quantity) {
        // 1. Redis快速校验和扣减
        boolean redisSuccess = redisAtomicStockService.deductStock(productId, quantity);
        if (!redisSuccess) {
            return false;
        }
        
        try {
            // 2. 数据库兜底扣减(乐观锁)
            boolean dbSuccess = databaseStockService.deductStockWithOptimisticLock(productId, quantity);
            if (!dbSuccess) {
                // 回滚Redis库存
                redisTemplate.opsForValue().increment("stock:" + productId, quantity);
                return false;
            }
            return true;
        } catch (Exception e) {
            // 异常时回滚Redis
            redisTemplate.opsForValue().increment("stock:" + productId, quantity);
            throw e;
        }
    }
}

这样既保证了高性能,又确保了数据的最终一致性。

3. 重复支付

产生原因:

  • 支付页面重复提交
  • 网络超时后用户重试
  • 第三方支付回调重复通知

解决方案:

java
// 支付订单状态机控制
@Transactional
public PayResult processPayment(String orderNo) {
    // 1. 查询订单状态
    Order order = orderMapper.selectByOrderNo(orderNo);
    if (order.getStatus() != OrderStatus.PENDING) {
        return PayResult.error("订单状态异常");
    }
    
    // 2. 分布式锁防重复
    String lockKey = "pay:" + orderNo;
    return redisLock.executeWithLock(lockKey, () -> {
        // 调用第三方支付
        // 更新订单状态
    });
}

// 支付回调幂等处理
@PostMapping("/payCallback")
public String payCallback(@RequestBody PayNotifyDTO notify) {
    String key = "callback:" + notify.getTradeNo();
    if (redisTemplate.hasKey(key)) {
        return "SUCCESS"; // 已处理过
    }
    // 处理支付结果
    redisTemplate.setex(key, 300, "processed");
    return "SUCCESS";
}

4. 网络异常支付失败

产生原因:

  • 网络超时或连接中断
  • 第三方支付服务不稳定
  • 系统异常导致支付流程中断

解决方案:

java
// 异步补偿机制
@Component
public class PaymentCompensator {
    
    @Scheduled(fixedDelay = 60000) // 每分钟执行
    public void compensateFailedPayments() {
        // 查询支付中状态超过5分钟的订单
        List<Order> timeoutOrders = orderMapper.selectTimeoutPayOrders();
        
        for (Order order : timeoutOrders) {
            // 查询第三方支付状态
            PaymentStatus status = paymentService.queryPaymentStatus(order.getOrderNo());
            if (status == PaymentStatus.SUCCESS) {
                // 补偿更新订单状态
                orderService.updateOrderPaid(order.getOrderNo());
            }
        }
    }
}

// 重试机制
@Retryable(value = Exception.class, maxAttempts = 3, backoff = @Backoff(delay = 2000))
public PayResult callPaymentApi(PayRequest request) {
    // 调用第三方支付API
}

综合防护策略

技术层面:

  • 分布式锁 + 数据库约束双重保险
  • 状态机模式控制业务流程
  • 异步补偿处理异常情况

业务层面:

  • 设置合理的超时时间
  • 建立完善的监控告警
  • 定期对账确保数据一致性

通过多层防护,可以有效解决商城系统中的这些经典问题。

所有文章版权皆归博主所有,仅供学习参考。