1. 项目背景与业务挑战
去年夏天,我参与了一个电商平台的库存系统重构项目。这个平台在全国有8个主要仓库,每天要处理超过50万次库存查询请求。原有的MySQL分库分表方案在业务高峰期经常出现响应延迟,某些热门商品的库存查询耗时甚至超过3秒,严重影响了用户体验和转化率。
最典型的痛点出现在去年双11大促期间。当时某个爆款球鞋的库存查询QPS突然飙升到2000+,数据库连接池直接被撑爆,导致整个库存服务雪崩。事后分析发现,传统的分库分表方案存在几个致命缺陷:
- 跨地域查询需要多次路由跳转,网络延迟不可控
- 热点商品会导致单个分片过载
- 分布式事务处理效率低下
- 扩容需要停机迁移数据
2. 技术选型与架构设计
2.1 为什么选择PolarDB
经过多轮技术对比,我们最终选择了阿里云的PolarDB-X作为核心数据库引擎,主要基于以下考量:
-
全局二级索引:支持在任意列上创建索引,彻底解决了跨分片查询的性能问题。实测显示,带有GSI的查询比传统分库分表快8-12倍。
-
计算存储分离:计算节点可以按需扩展,存储层采用多副本机制。我们在压力测试中实现了计算节点30秒内弹性扩容。
-
智能路由:通过内置的SQL优化器,可以自动识别最优查询路径。比如对于
WHERE region='华东' AND sku_id='123'这样的条件,会优先走region分片键。 -
混合负载隔离:通过资源组技术,将OLTP和OLAP流量物理隔离,避免相互干扰。
2.2 核心架构设计
整个库存系统的架构分为四层:
code复制[客户端]
↓ HTTP/2
[API网关] → [缓存集群(Redis)]
↓ gRPC
[库存服务]
↓ JDBC
[PolarDB-X] → [数据同步] → [数据分析平台]
关键设计要点:
-
读写分离:所有查询走只读实例,写入走主实例。通过Proxy自动路由。
-
多级缓存:
- 本地缓存:使用Caffeine缓存热点商品,TTL 500ms
- 分布式缓存:Redis集群缓存全量商品,TTL 2s
- 数据库缓存:PolarDB的Buffer Pool调优
-
智能降级:
- 当Redis超时,自动降级到数据库查询
- 数据库超时,返回最后一次缓存值
- 全链路超时设置:API网关(50ms)→服务(30ms)→DB(20ms)
3. 关键实现细节
3.1 分片策略设计
库存表的分片键选择经历了多次迭代:
sql复制-- 初始方案:按商品ID哈希分片
CREATE TABLE inventory (
sku_id VARCHAR(32) PRIMARY KEY,
region VARCHAR(16),
stock INT,
...
) PARTITION BY HASH(sku_id) PARTITIONS 16;
-- 优化方案:按地域范围分片+商品ID哈希
CREATE TABLE inventory (
sku_id VARCHAR(32),
region VARCHAR(16),
stock INT,
...
PRIMARY KEY(region, sku_id)
) PARTITION BY LIST COLUMNS(region) (
PARTITION p_east VALUES IN ('上海','江苏','浙江'),
PARTITION p_north VALUES IN ('北京','天津'),
...
);
最终采用的分片策略特点:
- 一级分片:按地域范围(符合80%的查询场景)
- 二级分片:在各地域内按商品ID哈希
- 本地索引:在region+sku_id上创建联合索引
- 全局索引:在sku_id上创建GSI用于跨地域查询
3.2 毫秒级查询优化
实现<10ms查询响应的关键技术:
-
执行计划优化:
sql复制EXPLAIN SELECT stock FROM inventory WHERE region='华东' AND sku_id='123';通过
FORCE_INDEX提示强制走最优索引 -
连接池调优:
java复制HikariConfig config = new HikariConfig(); config.setMaximumPoolSize(200); // 根据压测结果调整 config.setConnectionTimeout(3000); config.setIdleTimeout(60000); -
批量查询优化:
sql复制-- 低效方案 SELECT stock FROM inventory WHERE sku_id='123'; SELECT stock FROM inventory WHERE sku_id='456'; -- 高效方案 SELECT sku_id, stock FROM inventory WHERE sku_id IN ('123','456') ORDER BY FIELD(sku_id, '123','456');
3.3 分布式事务处理
库存扣减的完整事务流程:
java复制@Transactional
public boolean deductStock(String skuId, int quantity) {
// 1. 查询当前库存
Inventory inv = inventoryMapper.selectForUpdate(skuId);
// 2. 预扣减检查
if (inv.getStock() < quantity) {
throw new BizException("库存不足");
}
// 3. 记录操作日志
operationLogMapper.insert(new OperationLog(skuId, -quantity));
// 4. 实际扣减
return inventoryMapper.updateStock(skuId, -quantity) > 0;
}
关键保障措施:
- 使用
SELECT ... FOR UPDATE避免超卖 - 事务超时设置为500ms
- 异步补偿机制处理失败场景
- 通过XA事务保证跨库操作一致性
4. 性能优化实战
4.1 压测数据对比
优化前后的关键指标对比:
| 指标 | 原方案(MySQL) | PolarDB方案 | 提升幅度 |
|---|---|---|---|
| 平均响应时间 | 320ms | 8ms | 40x |
| P99响应时间 | 1.2s | 25ms | 48x |
| 最大QPS | 5,000 | 32,000 | 6.4x |
| CPU利用率(峰值) | 90% | 65% | -28% |
4.2 典型问题排查案例
案例1:热点商品查询延迟
现象:某款限量球鞋发布时,库存查询P99从15ms飙升到800ms
排查过程:
- 监控发现华东region的CPU使用率达到95%
- 慢查询日志显示大量
FOR UPDATE语句 - 线程堆栈显示锁竞争激烈
解决方案:
- 对该SKU启用单独缓存,TTL 100ms
- 将库存分成逻辑库存和实际库存
- 引入本地库存扣减+异步同步机制
案例2:跨地域查询超时
现象:全局库存查询接口频繁超时
排查过程:
- 发现GSI索引的
region列区分度不足 - 执行计划显示全分片扫描
解决方案:
- 在GSI上增加
category_id作为联合索引 - 使用
/*+TDDL:cmd_extra(ENABLE_GSI_OPTIMIZATION=true)*/提示 - 对结果集实现流式获取
5. 运维与监控体系
5.1 监控指标配置
核心监控看板配置:
yaml复制metrics:
- db_connections_active
- query_duration_seconds
- tps
- qps
- lock_wait_time
alerts:
- 连接数超过80%阈值
- P99延迟>50ms持续5分钟
- 锁等待超时率>1%
5.2 扩缩容实践
扩容操作步骤:
- 在控制台添加只读节点
- 自动数据同步(约10分钟/TB)
- 修改连接池配置
- 灰度切流验证
关键经验:
- 选择业务低峰期操作
- 先扩容再缩容
- 保留至少30%的buffer
6. 项目收益与展望
系统上线后取得的核心收益:
- 大促期间零故障
- 库存查询API的SLA从99.9%提升到99.99%
- 服务器成本降低40%
- 新仓库接入时间从2周缩短到2天
未来优化方向:
- 尝试HTAP实时分析能力
- 测试Serverless版本自动扩缩容
- 探索AI索引推荐功能
这个项目让我深刻体会到,好的架构设计必须建立在对业务特点的深入理解上。比如我们发现90%的库存查询都带有region条件,这个洞察直接影响了分片策略的设计。技术选型时也不要盲目追求新技术,而是要找到最适合当前业务规模和特点的方案。