一件商品的三条数据路径,MySQL、Redis与Elasticsearch如何分工协作

商品从上架到下单涉及修改、详情查看与搜索等不同操作,单一数据库难以兼顾一致性、性能与查询能力。实际系统中,MySQL 承担核心业务记录,Redis 缓存组装后的详情结果,Elasticsearch 支...

互联网/IT 阅读量: 1692

在电商系统中,一件商品的生命周期看似简单:上架、展示、搜索、下单。但深入技术实现层面,同一商品在不同环节会触发截然不同的数据访问模式——商家修改标题与 SKU、用户搜索关键词并筛选价格、打开详情页浏览信息、最终提交订单预占库存。这些动作虽围绕同一实体,却对数据系统的事务性、响应速度与查询灵活性提出差异化要求。

图1清晰呈现了这种分工逻辑:商品修改走 MySQL,查看详情依赖 Redis 缓存,商品检索则交由 Elasticsearch 处理。这不是技术堆砌,而是基于业务场景的权衡选择。

核心业务记录:MySQL 的不可替代性

商品并非仅由标题与主图构成。一个商品下挂多个 SKU,每个 SKU 对应规格、价格与库存;此外还有审核状态、展示策略、开售时间等配置项。这些数据彼此关联,修改时需保证原子性——例如更新商品主信息的同时同步调整 SKU 库存,必须整体成功或整体回滚。

项目采用 MySQL 存储此类结构化数据,通过 Repository 层封装读写操作,Service 层划定数据库事务边界。这里需澄清一个常见误解:MySQL 的价值不仅在于“数据落盘”,更在于其提供的关系模型、外键约束、二级索引以及 InnoDB 引擎支持的本地事务与行级锁机制。

以库存操作为例,系统既使用 UPDATE stock = stock - 1 这类增量更新表达“减一”,也采用 SELECT FOR UPDATE 实现强一致的串行协调。值得注意的是,事务的存在不等于并发问题自动解决——具体行为仍取决于 SQL 语句类型、隔离级别与索引设计。

若商品规模小、查询模式固定,仅用 MySQL 完全可行。优化方向应优先聚焦于 SQL 效率、索引覆盖、批量处理与字段裁剪,而非盲目引入新组件。新增存储前,必须明确回答两个问题:当前瓶颈具体是什么?新组件将承接哪一类负载?

详情页加速:Redis 缓存的是“组装结果”而非原始数据

用户打开商品详情时,期望获得的是“可直接渲染的完整对象”,而非分散在商品表、SKU 表、类目表中的多条记录。为此,系统采用典型的缓存穿透防护策略:先批量读取 Redis;对未命中的 ID,调用共享服务从数据库组装详情,并写回缓存。

文章配图

该流程节省的不仅是数据库 I/O,更是重复的 JOIN 查询与对象映射成本。批量读取还降低了网络往返次数。但需注意,当前实现中即使缓存命中,后续仍会调用展示状态服务读取数据库,以确保关键状态(如是否可售)不被缓存陈旧值误导。

代价同样明显:缓存数据存在过期风险,需维护序列化、失效、重建等逻辑。尤其当商品变更后,若缓存未及时清除,用户可能看到旧标题甚至错误的售卖状态。

项目中设置了双重保障机制:数据库提交后,同步尝试删除对应详情缓存(最多三次重试),同时异步触发详情与 SKU 数据重建。同步删除是为了堵住“写已提交、读仍命中旧缓存”的时间窗口;异步重建则为后续请求提前准备新鲜数据。

值得警惕的是,这类机制无法覆盖所有并发异常。例如,一个读请求在写提交前已读取旧值,写提交后删缓存,该读请求最终又将旧值写回 Redis——即“缓存回填污染”。此外,七天过期策略仅是兜底手段,绝不能替代业务关键状态变更时的主动失效。

选型时应评估旧数据的实际影响:标题错几秒可接受,但错误放行已下架商品则可能导致资损。缓存策略必须与业务风险等级匹配。

图2揭示了搜索链路的另一层约束:Elasticsearch 虽擅长全文检索与多条件组合筛选,但深度分页(如跳过 9,980 条取第 20 条)会导致内存与排序开销剧增。因此项目设定了保护阈值:from + size ≤ 10,000,单页结果不超过 100 条,并采用 search_after 替代传统分页,以维持查询稳定性。

这说明,即便使用专用搜索引擎,也不能忽视成本控制。ES 的优势在于灵活查询能力,而非无限制的数据遍历。

下单与库存:Lua 原子脚本与事务回滚的边界

Lua 原子性,有清楚的边界

最后是订单提交环节,涉及库存预占。系统在 Redis 中使用 Lua 脚本实现原子检查与扣减:先读取当前预占数与总库存,判断是否满足 reserved < total,再执行 total -= 1 与 reserved += 1。

图3展示了这一过程的边界意识:假设预占总量为 100,当前已占 1,脚本内完成检查与更新后,total 变为 99,reserved 变为 0(因本次操作释放原预占并新建)。若后续 MySQL 更新 SKU 库存失败,数据库事务回滚,但 Redis 中的变更不会自动撤销——因为 Redis 与 MySQL 并非同一事务上下文。

为此,系统额外部署了防护机制:订单维度日志、重复操作校验、消费侧商品锁。这些措施各司其职,共同降低资损概率,但无法保证“Redis 与 MySQL 绝对一致”。技术上,跨存储的强一致性需依赖分布式事务框架(如 Seata),而多数场景选择最终一致性+补偿机制更为务实。

多系统协同的本质:职责分离与边界清晰

  • MySQL 负责业务事实的可靠记录,强调 ACID 与关系完整性;
  • Redis 承担高频读取路径的加速,以空间换时间,接受有限时效性;
  • Elasticsearch 专精面向查询的数据组织,牺牲部分写入效率换取检索灵活性。

三者之间通过事件驱动(如商品变更触发缓存清理)、定时任务(如 ES 字段同步)与补偿机制(如订单超时释放)维持松耦合协同。这种架构不是银弹,而是对“不可能三角”——一致性、可用性、分区容错性——在具体业务场景下的务实取舍。

现实中,许多团队早期仅用 MySQL 即可支撑业务增长;随着流量与功能复杂度上升,才逐步拆分出缓存与搜索层。关键不在于技术栈是否“先进”,而在于每一步扩展是否回应了真实瓶颈:是慢?是错?还是不可扩展?

数据系统选型没有标准答案,只有与业务节奏匹配的演进路径。