SpringBoot电商购物车、结算、下单、库存方案设计与落地实施步骤
一、整体业务架构与核心流程概述
1.1 核心业务链路
用户购物全链路:浏览商品 → 加入购物车 → 购物车修改/删除 → 确认结算 → 生成订单 → 扣减库存 → 支付 → 订单完成。
核心难点:并发库存超卖、下单事务一致性、购物车数据同步、结算价格准确性、订单幂等性、库存回滚机制,本方案针对电商高并发、数据一致性核心问题做生产级设计。
1.2 技术栈选型(生产标配)
- 核心框架:SpringBoot 2.7.x / 3.2.x、Spring MVC、Spring Transaction
- 数据库:MySQL 8.0(InnoDB,支持事务、行锁)
- 缓存:Redis(购物车缓存、库存缓存、分布式锁、防重令牌)
- 消息队列:RabbitMQ(订单延时关闭、库存异步回滚、削峰填谷)
- 持久层:MyBatis-Plus
- 并发控制:Redis分布式锁、数据库行锁、乐观锁
- 幂等性:唯一订单号、防重Token、请求幂等校验
二、数据库表核心设计
仅展示购物车、订单、库存核心业务表,精简冗余字段,贴合实战需求。
2.1 购物车表(shop_cart)
核心说明:购物车数据优先缓存,数据库做持久化,支持用户多设备同步
|-------------|----------|-------------------------|
| 字段名 | 类型 | 说明 |
| id | bigint | 主键ID |
| user_id | bigint | 用户ID(唯一索引) |
| sku_id | bigint | 商品SKU ID(唯一索引,用户+sku唯一) |
| sku_num | int | 购买数量 |
| price | decimal | 加入购物车时单价(留存历史价格) |
| checked | tinyint | 是否选中 0-未选中 1-选中(结算依据) |
| create_time | datetime | 创建时间 |
| update_time | datetime | 更新时间 |
2.2 商品库存表(shop_sku_stock)
核心说明:采用预扣库存+可售库存双字段设计,解决超卖与库存回滚问题
|--------------|--------|----------------|
| 字段名 | 类型 | 说明 |
| sku_id | bigint | SKU主键(唯一索引) |
| stock | int | 可售库存(真实可下单库存) |
| locked_stock | int | 锁定库存(下单预扣、未支付) |
| version | int | 乐观锁版本号(解决并发超卖) |
2.3 订单主表、订单明细表
订单主表存储订单整体信息,订单明细表存储下单商品明细,一对多关联,此处省略基础字段,核心关键字段:订单号、用户ID、订单状态、实付金额、收货地址、创建时间。
三、核心模块详细方案设计
3.1 购物车模块设计(缓存+数据库双存)
3.1.1 存储方案
未登录用户:数据存储在浏览器LocalStorage,本地临时存储,不同步服务端;登录用户:数据同步至Redis+MySQL,Redis做热点查询,MySQL做持久化兜底。
Redis存储结构:CART:{userId},Hash结构,field=skuId,value=购物车商品信息(数量、价格、选中状态),设置过期时间(30天),自动清理无效数据。
3.1.2 核心功能逻辑
- 加入购物车:先查Redis,存在则更新数量,不存在则新增,异步同步至MySQL,提升响应速度
- 修改购物车:更新数量、选中状态,优先更新缓存,异步落库
- 删除购物车:删除缓存数据,异步删除数据库数据
- 购物车合并:用户登录时,合并本地LocalStorage数据与服务端缓存数据,去重求和
优势:避开数据库频繁读写,支撑高并发购物车操作,提升用户体验。
3.2 结算模块设计(价格校验+数据预校验)
3.2.1 结算前置校验
用户点击结算时,批量校验选中购物车商品:商品是否下架、SKU是否有效、库存是否充足、价格是否变动,无效商品自动过滤并提示用户。
3.2.2 价格计算规则
- 商品原价:以实时商品价格为准,不使用购物车历史价格,避免价格滞后问题
- 优惠计算:满减、优惠券、折扣叠加计算,统一结算总价、优惠金额、实付金额
- 价格防篡改:服务端重新计算所有价格,拒绝前端传参价格,杜绝前端篡改价格漏洞
3.3 下单模块核心设计(幂等+事务+防重)
3.3.1 下单核心流程
获取防重Token → 校验购物车选中数据 → 校验库存 → 分布式锁锁定商品 → 生成订单号 → 开启事务 → 保存订单信息 → 预扣锁定库存 → 清除已结算购物车 → 提交事务 → 发送延时消息 → 跳转支付
3.3.2 核心解决方案
- 幂等性设计:基于Redis防重Token,下单前获取唯一Token,下单成功后删除Token,防止重复提交订单;订单号采用雪花算法,全局唯一
- 事务一致性:Spring声明式事务,保证订单创建、库存锁定、购物车清除原子性,任意异常全部回滚
- 并发防重:Redis分布式锁(Lock+skuId),防止同一商品并发下单导致数据异常
3.4 库存核心方案(生产级防超卖)
3.4.1 库存状态流转
可售库存 → 锁定库存(下单预扣)→ 真实扣减(支付成功) / 解锁回滚(超时未支付/取消订单)
3.4.2 防超卖双重机制
- 数据库乐观锁兜底:更新库存时校验version版本号,同时判断可售库存≥扣减数量,防止并发超卖
- Redis库存缓存预拦截:热点商品库存缓存至Redis,下单先校验缓存库存,不足直接拦截,减少数据库压力
3.4.3 库存回滚机制
- 超时未支付:下单后15分钟未支付,RabbitMQ延时队列监听,自动解锁锁定库存、关闭订单
- 主动取消订单:用户手动取消,立即回滚锁定库存,恢复可售库存
- 支付失败:支付回调异常,触发库存补偿回滚机制
四、完整落地实施步骤(可直接落地执行)
步骤1:环境搭建与基础配置
- 搭建SpringBoot项目,引入依赖:web、mybatis-plus、redis、rabbitmq、lombok
- 配置MySQL、Redis、RabbitMQ连接参数,开启事务管理、Redis序列化配置
- 生成数据库对应实体类、Mapper、Service基础代码
- 封装Redis工具类、分布式锁工具类、雪花算法订单号工具类
步骤2:购物车模块开发落地
- 实现未登录用户LocalStorage购物车基础交互
- 开发登录用户Redis购物车CRUD接口:新增、修改、删除、查询
- 开发用户登录购物车合并功能,解决多设备数据同步问题
- 实现缓存异步落库、过期清理机制
- 接口测试:并发加入购物车、重复添加、数量修改场景验证
步骤3:结算模块开发落地
- 开发购物车选中商品批量校验接口(商品状态、库存、价格)
- 开发服务端价格计算逻辑,封装优惠、满减、总价计算工具类
- 实现结算数据组装、无效商品过滤、价格防篡改校验
- 测试场景:商品涨价、下架、库存不足时结算异常处理
步骤4:下单核心模块开发落地
- 开发防重Token生成、校验、删除接口,实现订单防重提交
- 封装Redis分布式锁工具类,实现商品并发锁定控制
- 开发下单核心事务逻辑:订单生成、明细保存、库存预锁定
- 开发购物车结算后清空逻辑,只删除已选中结算商品
- 集成RabbitMQ,配置订单延时关闭队列、库存回滚队列
步骤5:库存模块开发与防超卖落地
- 实现Redis库存缓存预热,热点商品库存同步至缓存
- 开发库存预扣、真实扣减、库存解锁回滚接口
- 实现数据库乐观锁库存更新逻辑,兜底防超卖
- 开发延时订单关闭、库存自动释放消费者逻辑
- 库存异常补偿机制:定时任务校对缓存库存与数据库库存一致性
步骤6:支付联动与订单状态流转
- 对接支付回调接口,支付成功后完成真实库存扣减、订单状态更新
- 支付失败/退款触发库存回滚、订单关闭逻辑
- 完善订单全状态流转:待支付、已支付、已取消、已完成、已退款
步骤7:压力测试与BUG修复
- JMeter压测:高并发下单场景,验证无超卖、无重复订单
- 异常测试:网络中断、事务回滚、MQ消息丢失、缓存失效场景
- 数据一致性校验:库存、订单、购物车数据联动准确性
五、核心问题解决方案与优化策略
5.1 超卖问题终极解决
Redis缓存拦截(前置)+ 分布式锁(并发控制)+ 数据库乐观锁(最终兜底),三层防护,彻底杜绝超卖问题,适配秒杀、普通下单场景。
5.2 订单重复提交问题
前端按钮置灰 + Redis防重Token + 订单幂等校验,三层拦截,完全避免重复下单。
5.3 库存数据不一致问题
- 实时校验:下单、支付、回滚全流程数据校验
- 定时补偿:每日定时任务校对Redis缓存库存与数据库库存,修复偏差数据
- MQ保证可靠消息:消息确认机制、重试机制、死信队列处理异常消息
5.4 性能优化
- 购物车、热点库存全部缓存化,减少DB查询压力
- 下单核心逻辑异步化、MQ削峰,应对流量峰值
- 数据库加索引:用户ID、SKU ID、订单号、状态字段,提升查询效率
- 读写分离:查询走缓存/从库,写入走主库
六、生产落地注意事项
- 禁止前端传任何价格、库存相关参数,所有核心数据服务端自主计算校验
- 分布式锁必须设置过期时间,防止死锁,同时适配锁续期逻辑
- 延时队列必须配置死信队列,处理异常未消费消息,避免库存卡死
- 所有库存、订单操作日志留存,方便线上问题排查
- 缓存穿透、击穿、雪崩问题做兜底:空值缓存、互斥锁、过期时间随机化
SpringBoot 电商购物车‑结算‑下单‑库存 核心代码实现
依赖:SpringBoot2.7、MyBatis‑Plus、RedisTemplate、RabbitMQ、Lombok 业务前提:
- sku_stock:sku_id,stock (可售),locked_stock (锁定),version (乐观锁)
- 防重 token:下单前获取 token 放入 redis,下单消费删除;防止重复提交
- 库存模式:下单预扣锁定库存;支付成功真实扣可售;超时 / 取消解锁回滚
- Redis 分布式锁;RabbitMQ 延时队列关闭订单 + 释放锁定库存
1. Redis 工具类、分布式锁工具
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
@Component
public class RedisLockUtil {
@Resource
private RedisTemplate<String,Object> redisTemplate;
/**
* 获取分布式锁
* @param key 锁key
* @param value 线程标识
* @param expire 过期时间秒
*/
public boolean tryLock(String key,String value,long expire){
String script = "if redis.call('set',KEYS[1],ARGV[1],'NX','EX',ARGV[2]) then return 1 else return 0 end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
Long res = redisTemplate.execute(redisScript, Collections.singletonList(key),value,expire);
return res != null && res ==1;
}
/**
* 释放锁 lua脚本保证原子性:只能释放自己持有的锁
*/
public boolean unLock(String key,String value){
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
Long res = redisTemplate.execute(redisScript, Collections.singletonList(key),value);
return res != null && res ==1;
}
/**
* 生成下单防重token
*/
public String generateOrderToken(Long userId){
String token = java.util.UUID.randomUUID().toString().replace("-","");
redisTemplate.opsForValue().set("order:token:"+userId,token,15, TimeUnit.MINUTES);
return token;
}
/**
* 校验并消费token(一次性,校验通过立即删除)
*/
public boolean checkAndConsumeToken(Long userId,String token){
String key = "order:token:"+userId;
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
Long res = redisTemplate.execute(redisScript, Collections.singletonList(key),token);
return res != null && res ==1;
}
}
2. SKU 库存 Mapper(乐观锁预扣锁定库存、真实扣减、解锁回滚)
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
public interface SkuStockMapper extends BaseMapper<SkuStock> {
/**
* 下单:预扣锁定库存
* stock 可售减少 num,locked_stock增加num,乐观锁version
* 返回更新行数,0代表扣减失败(库存不足/版本冲突)
*/
int lockStock(@Param("skuId") Long skuId, @Param("num") Integer num, @Param("version") Integer version);
/**
* 支付成功:真实扣减锁定库存
* locked_stock - num
*/
int realDeductLockedStock(@Param("skuId") Long skuId, @Param("num") Integer num);
/**
* 取消/超时未支付:解锁回滚库存
* locked_stock - num ; stock + num
*/
int unlockStock(@Param("skuId") Long skuId, @Param("num") Integer num);
}
<!-- SkuStockMapper.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xxx.mapper.SkuStockMapper">
<update id="lockStock">
update sku_stock
set stock = stock - #{num},
locked_stock = locked_stock + #{num},
version = version +1
where sku_id = #{skuId}
and version = #{version}
and stock >= #{num}
</update>
<update id="realDeductLockedStock">
update sku_stock
set locked_stock = locked_stock - #{num}
where sku_id = #{skuId}
and locked_stock >= #{num}
</update>
<update id="unlockStock">
update sku_stock
set locked_stock = locked_stock - #{num},
stock = stock + #{num}
where sku_id = #{skuId}
and locked_stock >= #{num}
</update>
</mapper>
3. 购物车 Service 核心片段(Redis Hash 为主,异步落库 MySQL)
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Map;
@Service
public class CartService {
@Resource
private RedisTemplate<String,Object> redisTemplate;
private static final String CART_PREFIX = "CART:";
/**
* 添加购物车
*/
public void addCart(Long userId,Long skuId,Integer num){
String cartKey = CART_PREFIX + userId;
// hash field:skuId ; value json:{num,price,checked}
Map<String,Object> cartItem = redisTemplate.opsForHash().entries(cartKey);
if(cartItem.containsKey(skuId.toString())){
//累加数量
redisTemplate.opsForHash().increment(cartKey,skuId.toString(),num);
}else{
redisTemplate.opsForHash().put(cartKey,skuId.toString(),num);
}
// 异步线程池同步mysql,不阻塞主流程
}
/**
* 获取用户选中购物车商品(用于结算)
*/
public Map<Object,Object> getCheckedCartItems(Long userId){
String cartKey = CART_PREFIX + userId;
return redisTemplate.opsForHash().entries(cartKey);
}
/**
* 下单成功:清除已结算购物车商品
*/
public void clearCheckedCart(Long userId,java.util.List<Long> skuIdList){
String cartKey = CART_PREFIX + userId;
for(Long skuId : skuIdList){
redisTemplate.opsForHash().delete(cartKey,skuId.toString());
}
}
}
4. 下单核心 Service(事务、防重 Token、分布式锁、预扣库存、发送延时消息)
业务入参:userId、token、结算选中 sku 列表 (skuId,num) 说明:
- 先消费防重 token;
- 循环商品加分布式锁;
- 查询 sku_stock 拿到 version;乐观锁 lockStock;
- @Transactional 保证订单主表、订单明细表、库存锁定、购物车清理原子;
- 事务提交后发送 RabbitMQ 延时消息(15 分钟)关闭订单
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.List;
import java.util.UUID;
@Service
public class OrderCreateService {
@Resource
private RedisLockUtil redisLockUtil;
@Resource
private SkuStockMapper skuStockMapper;
@Resource
private CartService cartService;
@Resource
private OrderMapper orderMapper;
@Resource
private OrderItemMapper orderItemMapper;
@Resource
private RabbitMqProducer rabbitMqProducer;
@Resource
private SkuStockService skuStockService;
/**
* 创建订单入口
* @param userId 用户id
* @param token 前端从接口获取的下单防重token
* @param orderItemDTOList 结算商品 skuId,num
* @return orderNo 订单号
*/
@Transactional(rollbackFor = Exception.class)
public String createOrder(Long userId, String token, List<OrderItemDTO> orderItemDTOList){
//1.校验并消费防重Token,失败拒绝下单
boolean tokenOk = redisLockUtil.checkAndConsumeToken(userId, token);
if(!tokenOk){
throw new RuntimeException("请勿重复提交订单");
}
// 生成雪花订单号
String orderNo = generateOrderNo();
for (OrderItemDTO dto : orderItemDTOList) {
Long skuId = dto.getSkuId();
Integer num = dto.getNum();
String lockKey = "lock:sku:" + skuId;
String lockValue = UUID.randomUUID().toString();
boolean lock = redisLockUtil.tryLock(lockKey, lockValue, 30);
if(!lock){
throw new RuntimeException("商品繁忙,请稍后重试");
}
try {
//查询库存+version
SkuStock skuStock = skuStockMapper.selectById(skuId);
if(skuStock == null || skuStock.getStock() < num){
throw new RuntimeException("商品库存不足");
}
//乐观锁预扣锁定库存
int affect = skuStockMapper.lockStock(skuId, num, skuStock.getVersion());
if(affect <=0){
throw new RuntimeException("并发下单,库存扣减失败");
}
}finally {
redisLockUtil.unLock(lockKey,lockValue);
}
}
// 构建订单主表、订单明细表入库
Order order = buildOrder(userId,orderNo);
orderMapper.insert(order);
List<OrderItem> itemList = buildOrderItem(order.getId(),orderNo,orderItemDTOList);
orderItemMapper.insertBatchSomeColumn(itemList);
// 清除购物车已结算商品
List<Long> skuIds = orderItemDTOList.stream().map(OrderItemDTO::getSkuId).collect(java.util.stream.Collectors.toList());
cartService.clearCheckedCart(userId,skuIds);
// 事务提交之后发送延时消息(注意:不要写在事务内部,用TransactionSynchronization)
org.springframework.transaction.support.TransactionSynchronizationManager.registerSynchronization(
new org.springframework.transaction.support.TransactionSynchronization() {
@Override
public void afterCommit() {
//15分钟延时消息:关闭订单、回滚锁定库存
rabbitMqProducer.sendOrderCloseDelayMsg(orderNo,15*60*1000);
}
}
);
return orderNo;
}
private String generateOrderNo(){
//雪花算法,这里简易演示
return System.currentTimeMillis()+"";
}
}
5. MQ 消费者:延时消息,关闭订单,解锁库存
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
@Component
public class OrderCloseConsumer {
@Resource
private OrderService orderService;
@RabbitListener(queues = "order.delay.close.queue")
public void consume(String orderNo, Message message, Channel channel) throws Exception{
try {
orderService.closeExpireOrder(orderNo);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}catch (Exception e){
//异常,nack重回队列/死信
channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false);
}
}
}
// OrderService 关闭过期订单逻辑
@Service
public class OrderService {
@Resource
private OrderMapper orderMapper;
@Resource
private OrderItemMapper orderItemMapper;
@Resource
private SkuStockMapper skuStockMapper;
@Transactional(rollbackFor = Exception.class)
public void closeExpireOrder(String orderNo){
//查询订单状态,只有待支付才执行关闭
Order order = orderMapper.selectOne(new LambdaQueryWrapper<Order>().eq(Order::getOrderNo, orderNo));
if(!order.getStatus().equals(OrderStatusEnum.WAIT_PAY.getCode())){
return;
}
//修改订单状态为已取消
order.setStatus(OrderStatusEnum.CANCEL.getCode());
orderMapper.updateById(order);
//查询订单明细,循环解锁库存
List<OrderItem> itemList = orderItemMapper.selectList(new LambdaQueryWrapper<OrderItem>().eq(OrderItem::getOrderNo, orderNo));
for(OrderItem item : itemList){
skuStockMapper.unlockStock(item.getSkuId(),item.getNum());
}
}
}
6. 支付回调成功:真实扣减锁定库存
@Service
public class PayCallbackService {
@Resource
private OrderMapper orderMapper;
@Resource
private OrderItemMapper orderItemMapper;
@Resource
private SkuStockMapper skuStockMapper;
@Transactional(rollbackFor = Exception.class)
public void paySuccess(String orderNo){
Order order = orderMapper.selectOne(new LambdaQueryWrapper<Order>().eq(Order::getOrderNo, orderNo));
if(!OrderStatusEnum.WAIT_PAY.getCode().equals(order.getStatus())){
return;
}
//更新订单状态为已支付
order.setStatus(OrderStatusEnum.PAID.getCode());
orderMapper.updateById(order);
//真实扣减锁定库存
List<OrderItem> items = orderItemMapper.selectList(new LambdaQueryWrapper<OrderItem>().eq(OrderItem::getOrderNo, orderNo));
for(OrderItem item : items){
skuStockMapper.realDeductLockedStock(item.getSkuId(),item.getNum());
}
}
}
7. Controller 接口层
@RestController
@RequestMapping("/order")
public class OrderController {
@Resource
private RedisLockUtil redisLockUtil;
@Resource
private OrderCreateService orderCreateService;
/**
* 获取下单防重token,结算页调用
*/
@GetMapping("/token")
public String getOrderToken(@RequestParam Long userId){
return redisLockUtil.generateOrderToken(userId);
}
/**
* 提交下单
*/
@PostMapping("/create")
public String createOrder(@RequestParam Long userId,
@RequestParam String token,
@RequestBody List<OrderItemDTO> itemList){
return orderCreateService.createOrder(userId,token,itemList);
}
}
关键坑点说明(生产必须注意)
- 延时消息不要放在事务内发送 ,必须用
TransactionSynchronization在事务提交后发 MQ,防止事务回滚消息已经发出。 - 分布式锁不能用
try‑finally直接 unlock;必须 lua 脚本判断持有者,避免释放别人锁。 - 乐观锁
lockStock返回行数判断;返回 0 代表:库存不足 /version 冲突并发抢占,直接抛异常。 - Redis 库存缓存:热点商品把库存放入 Redis 做前置拦截,减少 DB 压力;Redis 库存仅做拦截,扣减以 mysql 为准,Redis 库存做异步同步,不要直接拿 Redis 做库存扣减源。
- MQ 一定要配置死信队列,消息丢失、消费失败走死信,定时任务扫描异常订单做补偿兜底。
- 价格全部服务端计算,禁止前端传 price;结算时实时查商品最新价格。