1. 电商数据库设计核心挑战
做电商系统最头疼的就是数据库设计,我经历过3个日订单量10万+的电商平台重构,发现90%的性能问题都源于早期数据库设计缺陷。一个典型的电商系统需要同时处理商品信息、用户行为、交易流水、库存变动等高并发写入,还要支撑复杂的多维度查询。比如大促期间每秒要处理上千次库存扣减,同时还得保证实时显示准确的剩余库存数。
去年我们团队接手过一个案例:某母婴电商在用户量突破50万后,首页加载时间从1秒飙升到8秒。经过排查发现,问题出在商品表的冗余字段和缺乏索引上。这个教训让我意识到,电商数据库设计必须同时考虑业务扩展性、查询性能和事务一致性三大维度。
2. 核心表结构设计详解
2.1 商品系统的"三表分离"原则
主流电商平台通常采用SPU-SKU的架构,但具体实现有很多坑。我们实践下来最稳定的方案是:
CREATE TABLE `spu` ( `id` bigint NOT NULL COMMENT '标准产品单元ID', `category_id` int NOT NULL COMMENT '类目ID', `brand_id` int DEFAULT NULL COMMENT '品牌ID', `title` varchar(120) NOT NULL COMMENT '商品标题', `subtitle` varchar(200) DEFAULT NULL COMMENT '副标题', `main_image` varchar(255) DEFAULT NULL COMMENT '主图URL', `status` tinyint DEFAULT '1' COMMENT '状态:1-在售 0-下架', `created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_category` (`category_id`), KEY `idx_brand` (`brand_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品SPU表'; CREATE TABLE `sku` ( `id` bigint NOT NULL COMMENT '库存单元ID', `spu_id` bigint NOT NULL COMMENT '关联的SPUID', `specs` json NOT NULL COMMENT '规格属性JSON', `price` decimal(10,2) NOT NULL COMMENT '售价', `cost_price` decimal(10,2) DEFAULT NULL COMMENT '成本价', `stock` int NOT NULL DEFAULT '0' COMMENT '库存', `low_stock` int DEFAULT NULL COMMENT '库存预警值', `image` varchar(255) DEFAULT NULL COMMENT 'SKU主图', PRIMARY KEY (`id`), KEY `idx_spu` (`spu_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品SKU表'; CREATE TABLE `sku_spec` ( `id` bigint NOT NULL, `sku_id` bigint NOT NULL, `spec_id` int NOT NULL COMMENT '规格ID', `spec_value_id` int NOT NULL COMMENT '规格值ID', PRIMARY KEY (`id`), UNIQUE KEY `uk_sku_spec` (`sku_id`,`spec_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='SKU规格关联表';关键经验:将规格属性拆分为JSON存储和关系表存储两种方式。JSON用于快速读取展示,关系表用于精确搜索过滤。这种混合方案比纯JSON或纯关系表的性能提升40%以上。
2.2 订单系统的分库分表策略
订单表是最容易成为性能瓶颈的,我们的解决方案是:
- 按用户ID哈希分库(user_id % 8)
- 按创建时间范围分表(每月一个表)
- 关键字段包括:
- 订单基础信息(编号、状态、金额)
- 用户信息快照(避免关联查询)
- 商品信息快照(包括当时的价格、规格)
- 物流信息(单独拆表)
-- 分表示例 CREATE TABLE `order_202307` ( `id` bigint NOT NULL COMMENT '订单ID', `order_no` varchar(32) NOT NULL COMMENT '订单编号', `user_id` bigint NOT NULL COMMENT '用户ID', `payment_amount` decimal(10,2) NOT NULL COMMENT '实付金额', `payment_type` tinyint DEFAULT NULL COMMENT '支付方式', `status` tinyint NOT NULL COMMENT '订单状态', `shipping_info` json DEFAULT NULL COMMENT '物流信息JSON', `created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_user` (`user_id`), KEY `idx_create_time` (`created_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单主表-2023年7月';3. 高并发场景优化方案
3.1 库存扣减的防超卖设计
我们采用"预扣库存+异步确认"的二级方案:
- 前端展示:缓存中的可售库存(可能有短暂延迟)
- 下单时:先扣减redis中的预扣库存
- 支付成功后:同步扣减DB真实库存
- 定时任务:每小时同步redis与DB库存
// 伪代码示例 public boolean reduceStock(Long skuId, Integer num) { // 1. redis原子操作扣减 Long remain = redisTemplate.opsForValue().decrement("stock:"+skuId, num); if (remain < 0) { // 回滚 redisTemplate.opsForValue().increment("stock:"+skuId, num); return false; } // 2. 异步写入MQ mqTemplate.send("stock.update", new StockMessage(skuId, num, StockOperationType.REDUCE)); return true; }3.2 购物车优化方案
购物车需要特别处理未登录用户的情况,我们的方案是:
| 方案 | 存储位置 | 优点 | 缺点 |
|---|---|---|---|
| 未登录用户 | Cookie+LocalStorage | 不占用服务端资源 | 容量有限(4KB) |
| 已登录用户 | Redis+MySQL | 多端同步 | 需要维护数据一致性 |
具体实现时要注意:
- 用户登录后需要合并本地和服务器购物车
- 商品价格变动时需要提示用户
- 失效商品要自动清理
4. 查询性能优化实战
4.1 商品搜索的索引策略
针对不同的搜索场景,我们建立了多级索引:
- 类目筛选:category_id + brand_id 联合索引
- 价格区间:price字段单独索引
- 关键词搜索:使用Elasticsearch的倒排索引
- 属性筛选:使用spec表的组合索引
-- 典型查询优化案例 EXPLAIN SELECT s.id, s.title, s.main_image, MIN(sk.price) AS min_price FROM spu s JOIN sku sk ON s.id = sk.spu_id WHERE s.category_id = 123 AND s.status = 1 GROUP BY s.id ORDER BY min_price ASC LIMIT 20;4.2 订单列表查询优化
我们采用"冷热数据分离"策略:
- 热数据(3个月内):走主库+缓存
- 冷数据(3个月以上):走ES搜索集群
同时使用"宽表+垂直分表"设计:
- 订单主表:核心字段+常用查询字段
- 订单扩展表:JSON格式存储动态字段
- 订单商品表:关联的商品快照信息
5. 数据一致性保障
5.1 分布式事务方案选型
根据业务场景选择不同方案:
| 场景 | 方案 | 实现方式 | 适用条件 |
|---|---|---|---|
| 下单扣库存 | TCC | Try-Confirm-Cancel | 高一致性要求 |
| 支付成功通知 | 本地消息表 | 事务+异步消息 | 最终一致性 |
| 优惠券核销 | SAGA | 事件驱动补偿 | 长流程业务 |
5.2 对账系统设计
我们建立了三级对账机制:
- 实时对账:核心交易流水(如支付单和订单)
- 定时对账:每小时全量检查库存一致性
- 离线对账:每日凌晨跑数据仓库校验
# 伪代码示例:库存对账 def check_stock(): # 从DB获取基准数据 db_stock = execute_sql("SELECT sku_id,stock FROM sku") # 从Redis获取缓存数据 redis_stock = {} for sku in redis.keys("stock:*"): redis_stock[sku] = redis.get(sku) # 对比差异 for sku in db_stock: if abs(db_stock[sku] - redis_stock.get(sku,0)) > 5: # 允许5个误差 alert_admin(sku)6. 扩展性设计经验
6.1 字段预留策略
我们在所有核心表都增加了:
- 3个备用VARCHAR字段
- 2个备用INT字段
- 1个备用JSON字段
ALTER TABLE `spu` ADD COLUMN `ext_field1` varchar(255) DEFAULT NULL, ADD COLUMN `ext_field2` varchar(255) DEFAULT NULL, ADD COLUMN `ext_json` json DEFAULT NULL;6.2 分库分表中间件选型
经过对比测试,我们最终选择ShardingSphere的方案:
| 中间件 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| ShardingSphere | 功能全面 | 学习成本高 | 复杂分片规则 |
| MyCat | 简单易用 | 性能一般 | 简单分库分表 |
| TDDL | 阿里背书 | 文档较少 | 阿里云环境 |
7. 监控与运维要点
7.1 必须监控的关键指标
数据库负载:
- QPS/TPS波动
- 连接数使用率
- 慢查询占比
业务指标:
- 下单到支付的平均时延
- 库存扣减失败率
- 购物车转化率
7.2 日常维护建议
我们团队执行的维护规范:
- 每周检查一次索引使用率(使用sys.schema_unused_indexes)
- 每月进行一次统计信息更新
- 每季度做一次大表归档
- 建立SQL审核流程,禁止全表扫描
8. 典型问题排查实录
8.1 商品搜索变慢案例
现象:关键词搜索响应时间从200ms飙升到2s 排查过程:
- 发现ES集群负载均衡异常
- 检查发现有个别节点磁盘IO饱和
- 进一步排查是日志文件未轮转 解决方案:
- 调整ES分片分布
- 增加日志清理定时任务
- 添加磁盘IO监控告警
8.2 订单超时支付bug
现象:部分用户支付成功后订单状态未更新 根本原因:
- 本地事务先更新订单状态
- 然后发MQ通知其他系统
- 极端情况下MQ发送失败 最终方案:
- 改为事务消息表方案
- 增加补偿job定时检查
- 关键操作添加操作日志
9. 架构演进建议
根据我们的经验,电商数据库架构通常需要经历这几个阶段:
初创期(日单<1万):
- 单库单表
- 读写不分离
- 简单缓存
发展期(日单1万-10万):
- 主从分离
- 热点数据缓存
- 垂直分库
成熟期(日单>10万):
- 水平分库分表
- 分布式事务
- 多级缓存体系
建议每个阶段预留3-6个月的扩展空间,避免过早优化带来的复杂度。我们最近在为某客户设计架构时,就采用了"分阶段路线图"的方式,既满足当前需求,又为未来扩展留好接口。