电商交易系统 MySQL + Java 开发实践指南


一、核心原则:先理解业务,再写代码

电商交易的核心流程通常包括:
下单 → 支付 → 发货 → 售后,每个环节都涉及资金、库存、状态机的强一致性要求。

牢记三不要

  • 不要假设数据库是“单机玩具”
  • 不要忽略并发场景下的数据竞争
  • 不要让业务逻辑散落在 SQL 里

二、数据库设计规范

1. 表结构设计

  • 主键必须用 BIGINT UNSIGNED AUTO_INCREMENT
    避免 INT 溢出(订单量超 21 亿时会崩)
  • 金额字段用 DECIMAL(18,2)
    禁止用 FLOAT/DOUBLE(精度丢失会导致资损)
  • 状态字段用 TINYINT + 注释枚举
    例:order_status TINYINT NOT NULL COMMENT '0-待支付 1-已支付 2-已取消...'
  • 关键索引必须覆盖查询条件
    如订单表:(user_id, create_time)(order_no)(pay_status, update_time)

2. 避免“宽表陷阱”

  • 订单主表只存核心字段(用户 ID、订单号、金额、状态)
  • 扩展信息(收货地址、商品快照)拆到 order_ext
  • 商品详情、SKU 信息不要冗余到订单表(用快照表解耦)

三、Java 事务与并发控制

1. 事务边界必须清晰

@Service
public class OrderService {
    
    @Transactional(rollbackFor = Exception.class)
    public void createOrder(CreateOrderRequest request) {
        // 1. 扣库存(带锁)
        inventoryService.lockStock(request.getSkuId(), request.getQuantity());
        
        // 2. 创建订单
        Order order = buildOrder(request);
        orderMapper.insert(order);
        
        // 3. 发送MQ(异步解耦,避免事务过长)
        mqProducer.send(new OrderCreatedEvent(order.getId()));
    }
}

2. “先读再写”必须加锁!

❌ 错误示例(超卖风险)
// 查询库存
int stock = inventoryMapper.selectStock(skuId);
if (stock < buyQty) throw new BizException("库存不足");
// 扣减库存
inventoryMapper.updateStock(skuId, stock - buyQty);
✅ 正确做法(行级锁)
@Transactional
public void deductStock(Long skuId, Integer qty) {
    // 加锁读取(当前读)
    Inventory inventory = inventoryMapper.selectForUpdate(skuId);
    if (inventory.getStock() < qty) {
        throw new BizException("库存不足");
    }
    inventoryMapper.updateStock(skuId, inventory.getStock() - qty);
}

MyBatis Mapper 示例

<select id="selectForUpdate" resultType="Inventory">
    SELECT * FROM inventory WHERE sku_id = #{skuId} FOR UPDATE
</select>

3. 高频场景用乐观锁

适用于低冲突场景(如用户积分变动):

// 数据库字段: version INT DEFAULT 0
int updated = userMapper.updatePoints(
    userId, 
    newPoints, 
    currentVersion // WHERE version = #{currentVersion}
);
if (updated == 0) throw new BizException("数据已被修改,请重试");

四、防幻读:范围操作必须显式加锁

电商场景常见需求:“同一用户 5 分钟内不能重复下单相同商品”

❌ 无锁查询(幻读风险)
// 普通查询不会加间隙锁!
List<Order> orders = orderMapper.selectRecentOrders(userId, skuId);
if (!orders.isEmpty()) throw new BizException("请勿重复下单");
orderMapper.insert(order);
✅ RR 隔离级别下正确做法
@Transactional
public void createOrder(Order order) {
    // 显式加锁(触发 Next-Key Lock)
    List<Order> lockedOrders = orderMapper.selectRecentOrdersForUpdate(
        order.getUserId(), 
        order.getSkuId()
    );
    if (!lockedOrders.isEmpty()) {
        throw new BizException("请勿重复下单");
    }
    orderMapper.insert(order);
}

Mapper SQL

SELECT * FROM orders 
WHERE user_id = #{userId} 
  AND sku_id = #{skuId}
  AND create_time > NOW() - INTERVAL 5 MINUTE
FOR UPDATE  -- 关键!

💡 前提user_id + sku_id + create_time 必须有联合索引,否则锁会退化为表锁!


五、支付状态一致性:最终一致性方案

支付回调可能重复、延迟,绝对不要在回调里直接改订单状态!

推荐流程:

支付宝订单服务MQ支付对账服务告警系统支付成功回调发送“支付结果事件”消费事件主动查询支付状态(防伪造)更新订单为“已支付”触发人工介入alt[状态一致][状态不一致]支付宝订单服务MQ支付对账服务告警系统

关键代码:

// 支付回调入口(幂等处理!)
public void handlePayCallback(PayCallbackDTO dto) {
    // 1. 验签(略)
    // 2. 幂等:用 order_no + trade_no 作为唯一键存入 redis/setnx
    if (idempotentService.isProcessed(dto.getOrderNo(), dto.getTradeNo())) {
        return; // 已处理过
    }
    // 3. 发送MQ(不直接操作订单)
    mqProducer.send(new PaymentResultEvent(dto));
}

六、性能与可维护性

1. 禁止 N+1 查询

// ❌ 错误:循环查数据库
List<Order> orders = orderMapper.selectAll();
for (Order order : orders) {
    order.setUserInfo(userMapper.selectById(order.getUserId())); // N+1!
}

// ✅ 正确:JOIN 或批量查询
List<OrderVO> orders = orderMapper.selectWithUserInfo(); 

2. 大表分页用“游标分页”

-- ❌ 深分页性能差
SELECT * FROM orders LIMIT 100000, 20;

-- ✅ 用上一次最大ID
SELECT * FROM orders WHERE id > #{lastId} ORDER BY id LIMIT 20;

3. 敏感操作留痕

  • 订单状态变更必须记录 order_status_log
  • 库存变动必须记录 inventory_log
  • 字段:操作人、旧值、新值、时间、业务单据号

七、新人避坑清单

问题后果解决方案
SELECT 代替 SELECT ... FOR UPDATE超卖/重复下单明确“读-写”依赖必须加锁
事务里调用远程接口(如支付)事务长时间不提交,DB 连接耗尽异步化:事务提交后再发 MQ
忽略 MySQL 隔离级别幻读导致数据错乱确认环境为 REPEATABLE READ
金额用 double 计算资损(0.1+0.2≠0.3)BigDecimal + DECIMAL 字段
没有幂等设计重复支付/重复发货关键接口用唯一键 + redis/setnx

八、推荐工具链

  • SQL 审核SOAR / Yearning(上线前自动检查)
  • 慢查询监控Arthas + SkyWalking
  • 数据一致性核对:每日跑 订单-支付-库存 对账任务
  • 压测验证:用 JMeter 模拟秒杀场景(重点测库存扣减)

结语

电商交易系统是资金与信任的载体,每一行代码都可能影响真金白银。
记住:

“宁可慢一点,不可错一步” —— 数据一致性永远优先于性能优化。

新人遇到不确定的场景,务必:

  1. 画时序图/状态机
  2. 和 DBA 讨论索引与锁
  3. 写单元测试覆盖并发 case

附:MySQL 隔离级别检查命令

SELECT @@transaction_isolation; -- 确保是 REPEATABLE-READ
SHOW ENGINE INNODB STATUS\G -- 查看锁等待
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值