在电商系统中,库存实时扣减与 Elasticsearch 搜索的联动 是一个典型的 高并发、强一致性挑战场景。用户在搜索或下单时,必须看到准确的库存状态(如“有货”、“仅剩 3 件”),否则可能导致:
- 超卖(库存扣成负数)
- 用户体验差(显示有货,下单时提示无库存)
本文将为你设计一套 Elasticsearch 与 库存服务联动的完整方案,确保搜索结果中的库存信息实时、准确、高效,同时兼顾性能与一致性。
一、核心挑战
| 问题 | 说明 |
|---|---|
| 数据延迟 | ES 是近实时(NRT),默认 1s 刷新,无法满足秒杀等场景 |
| 并发更新冲突 | 高并发扣减库存时,ES 文档版本冲突 |
| 一致性保障 | 如何保证 MySQL、Redis、ES 库存数据一致 |
| 性能瓶颈 | 频繁更新 ES 文档影响写入吞吐 |
二、系统架构设计
我们采用 “缓存 + 搜索分离” + “事件驱动” 的架构:
+----------------+ +------------------+ +-----------------------+
| 用户下单 | --> | 库存服务(Redis) | --> | Kafka / RabbitMQ |
+----------------+ +--------+---------+ +-----------+-----------+
| |
v v
+----------------------+ +--------------------------+
| MySQL(持久化库存) | | Elasticsearch(搜索展示)|
+----------------------+ +--------------------------+
核心组件职责:
| 组件 | 职责 |
|---|---|
| Redis | 库存扣减主库(高性能、原子操作) |
| MySQL | 持久化库存,用于对账和恢复 |
| Kafka | 异步通知库存变更事件 |
| Elasticsearch | 展示库存状态,支持搜索过滤 |
| 库存服务 | 控制扣减逻辑(如预扣、回滚) |
三、库存扣减流程(以“下单”为例)
四、Elasticsearch 索引设计(SKU 级库存字段)
在 sku-catalog-* 索引中增加库存相关字段:
PUT /sku-catalog-2024-10
{
"mappings": {
"properties": {
"id": { "type": "keyword" },
"title": { "type": "text" },
"price": { "type": "scaled_float", "scaling_factor": 100 },
"stock": {
"type": "integer",
"doc_values": true // 支持聚合与排序
},
"stock_status": {
"type": "keyword" // in_stock, low_stock, out_of_stock
},
"sales_count": { "type": "long" },
"version": { "type": "long" } // 与 Redis 版本同步,防并发冲突
}
}
}
✅
stock_status用于前端展示和搜索过滤(如“仅显示有货”)。
五、库存更新策略(如何同步到 ES)
方案一:事件驱动 + 消费者更新(推荐)
1. 库存服务发布事件
{
"event": "stock.updated",
"sku_id": "1001",
"stock": 2,
"version": 12345,
"timestamp": "2024-06-01T10:00:00Z"
}
2. Elasticsearch 消费者(Logstash / Flink / 自研服务)更新 ES
POST /sku-write/_update/sku_1001
{
"doc": {
"stock": 2,
"stock_status": "in_stock",
"version": 12345
}
}
3. 乐观锁防冲突(可选)
使用 version 字段避免旧事件覆盖新数据:
POST /sku-write/_update/sku_1001?if_seq_no=100&if_primary_term=2
或在脚本中判断:
"script": {
"source": """
if (ctx._source.version <= params.event_version) {
ctx._source.stock = params.stock;
ctx._source.version = params.event_version;
ctx._source.stock_status = params.stock > 0 ? 'in_stock' : 'out_of_stock';
}
""",
"params": {
"stock": 2,
"event_version": 12345
}
}
方案二:双写(不推荐)
应用层同时更新 Redis 和 ES:
redis.decr("stock:1001");
esClient.update(...);
❌ 缺点:ES 写入失败会导致数据不一致,且无法保证原子性。
方案三:定时补偿(兜底)
- 每日夜间全量同步 MySQL → ES 库存。
- 监控 ES 与 Redis 库存差异,告警或自动修复。
六、搜索查询中的库存过滤
1. 搜索时过滤无库存 SKU
GET /sku-read/_search
{
"query": {
"bool": {
"must": [
{ "match": { "title": "iPhone" } }
],
"filter": [
{ "term": { "stock_status": "in_stock" } }
]
}
}
}
2. 聚合中排除售罄 SKU
"aggs": {
"brands": {
"terms": {
"field": "brand",
"include": {
"partition": 0,
"num_partitions": 1
}
},
"filter": { "term": { "stock_status": "in_stock" } }
}
}
七、性能优化与高并发应对
1. 减少 ES 写入频率
- 批量更新:消费者每 100ms 批量提交
bulk请求。 - 限流降级:库存更新高峰期,允许短暂延迟(如 1s 内)。
2. 缓存库存状态(可选)
- 前端或网关缓存
stock_status,TTL=1s。 - 避免高频查询 ES。
3. 使用 source 过滤减少传输
GET /sku-read/sku_1001?_source=stock,stock_status
八、一致性保障策略
| 策略 | 说明 |
|---|---|
| Redis 为唯一 truth source | 所有扣减以 Redis 为准 |
| Kafka 持久化事件 | 即使消费者宕机,重启后可重放 |
| 幂等消费 | 消费者根据 event_id 或 (sku_id, version) 去重 |
| 监控告警 | 监控 Redis vs ES 库存差异 > 5% 时告警 |
| 对账服务 | 每日比对 MySQL、Redis、ES 库存,自动修复 |
九、特殊场景处理
1. 秒杀场景(极高并发)
- 预扣库存:下单前先调用
decr,成功再创建订单。 - 延迟同步 ES:允许 ES 库存延迟 1~2 秒更新,但下单页调用 Redis 实时查询。
- 前端降级:搜索页显示“有货”,点击后调用库存服务确认。
2. 库存回滚(订单取消)
- 订单取消时,库存服务发送
stock.incr事件。 - 消费者更新 ES
stock += 1。
3. 超卖保护
- Redis 扣减使用原子操作
DECR,负数则失败。 - 设置最大重试次数,避免死循环。
十、总结:库存联动架构核心原则
| 原则 | 说明 |
|---|---|
| ✅ Redis 为库存权威源 | 所有扣减在此执行 |
| ✅ ES 仅为展示层 | 数据可短暂延迟,但不能反向更新 Redis |
| ✅ 事件驱动异步同步 | 解耦库存服务与搜索系统 |
| ✅ 幂等 + 版本控制 | 防止数据错乱 |
| ✅ 监控 + 对账兜底 | 保障最终一致性 |
十一、扩展建议
| 场景 | 建议方案 |
|---|---|
| 库存分片(如区域仓) | 在 ES 中增加 warehouse_id 字段,支持就近配送搜索 |
| 预售库存 | 增加 pre_sale_stock 字段,与现货分离 |
| 虚拟库存 | 支持超卖,但需风控拦截 |
| 搜索结果标注“仅剩 X 件” | 在 script_fields 中动态计算并高亮 |
1103

被折叠的 条评论
为什么被折叠?



