商城系统经典问题分析
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
}
综合防护策略
技术层面:
- 分布式锁 + 数据库约束双重保险
- 状态机模式控制业务流程
- 异步补偿处理异常情况
业务层面:
- 设置合理的超时时间
- 建立完善的监控告警
- 定期对账确保数据一致性
通过多层防护,可以有效解决商城系统中的这些经典问题。