# 125. 大型电商网站的商品详情页系统架构是如何一步一步演进的

商品详情页系统架构演进历程,这里分三个版本来讲解

为什么要演进?这个只有经历过才会深刻明白,只要想到成本和时间就明白了, 在一定的时间和成本下,一般都是从第一个版本开始的,后续根据业务发展, 用户越来越多,业务越来越复杂,支撑不了的时候,就需要开始改版了

# 第一个版本

# 架构设计

J2EE + Tomcat + MySQL

动态页面,每次请求都要调用多个依赖服务的接口,从数据库里查询数据, 然后通过类似 JSP 的技术渲染到 HTML 模板中,返回最终 HTML 页面

# 架构缺陷

  • 每次请求都是要访问数据库的,性能肯定很差
  • 每次请求都要调用大量的依赖服务,依赖服务不稳定导致商品详情页展示的性能经常抖动

# 第二个版本

# 架构设计

页面静态化技术

  • 通过 MQ 得到商品详情页涉及到的数据的变更消息

  • 通过 Java Worker 服务全量调用所有的依赖服务的接口

    查询数据库,获取到构成一个商品详情页的完整数据,并通过 velocity 等模板技术生成静态 HTML

    将静态 HTML 页面通过 rsync 工具直接推送到多台 nginx 服务器上, 每台 nginx 服务器上都有全量的HTML静态页面,nginx 对商品详情页的访问请求直接返回本地的静态 HTML 页面

# 架构缺陷

  • 全量更新问题

    如果某一个商品分类、商家等信息变更了

    那么那个分类、店铺、商家下面所有的商品详情页都需要重新生成静态 HTML 页面

  • 更新速度过慢问题

    分类、店铺、商家、商品越来越多,重新生成 HTML 的负载越来越高,rsync 全量同步所有 nginx 的负载也越来越高

    从数据变更到生成静态 HTML,再到全量同步到所有 nginx,时间越来越慢

  • 扩容问题

    因为每个商品详情页都要全量同步到所有的 nginx 上,导致系统无法扩容,无法增加系统容量

# 架构优化

  • 解决全量更新问题(维度化)

    每次 Java Worker 收到某个维度的变更消息,不是拉去全量维度并生成完整 HTML,而是按照维度拆分, 生成一个变化维度的 HTML 片段

    nginx 对多个 HTML 片段通过 SSI 合并 html 片段然后输出一个完整的 html

  • 解决扩容问题(集群化,每台机器负责其中一部分)

    每个商品详情页不是全量同步到所有的 nginx

    而是根据商品 id 路由到某一台 nginx 上,同时接入层 nginx 按照相同的逻辑路由请求

  • 解决更新速度过慢问题(增加更多机器资源)

    多机房部署,每个机房部署一套 Java Worker + 应用 Nginx, 所有机房用一套负载均衡设备,在每个机房内部完成全流程,不跨机房

# 架构优化后的缺陷

  • 更新速度还是不够快的问题

    商品的每个维度都有一个 HTML 片段,rsync 推送大量的 HTML 片段,负载太高,性能较差

    Nginx 基于机械硬盘进行 SSI 合并,性能太差

  • 还是存在全量更新的问题

    虽然解决了分类、商家、店铺维度的变更,只要增量重新生产较小的 HTML 片段即可, 不用全量重新生成关联的所有商品详情页的 HTML

    但是如果某个页面模板变更,或者新加入一个页面模板,还是会导致几亿个商品的 HTML 片段都要重新生成和 rsync, 要几天时间才能完成,无法响应需求

  • 还是存在容量问题

    nginx 存储有限,不能无限存储几亿,以及增长的商品详情页的 HTML 文件

    如果 nginx 存储达到极限,需要删除部分商品详情页的 HTML 文件,改成 nginx 找不到 HTML, 则调用后端接口,回到动态页面的架构

    动态页面架构在高并发访问的情况下,会对依赖系统造成过大的压力,几乎扛不住

# 第三个版本

# 需要支持的需求

  • 迅速响应各种页面模板的改版和个性化需求的新模板的加入
  • 页面模块化,页面中的某个区域变化,只要更新这个区域中的数据即可
  • 支持高性能访问
  • 支持水平扩容的伸缩性架构

# 系统架构设计

  1. 依赖服务有数据变更发送消息到 MQ

  2. 监听数据变更事件,写入缓存

    数据异构 Worker 服务监听 MQ 中的变更消息,调用依赖服务的接口,仅仅拉取有变更的数据即可,然后将数据存储到 redis 中

    数据异构 Worker 存储到 redis 中都是原子未加工数据,包括商品基本信息、商品扩展属性、 商品其他信息、商品规格参数、商品分类、商家信息

    数据异构 Worker 发送消息到 MQ,数据聚合 Worker 监听到 MQ 消息

    数据聚合 Worker 将原子数据从 redis 中取出,按照维度聚合后存储到 redis 中,包括三个维度

    • 基本信息维度:基本信息、扩展属性 - 商品介绍:PC 版、移动版 - 其他信息:商品分类、商家信息

    nginx+lua:lua 从 redis 读取商品各个维度的数据,通过 nginx 动态渲染到 html 模板中,然后输出最终的 html

简单说最大的变化就是:使用上了缓存、html 是通过模板动态渲染的

# 如何解决的问题

  • 更新问题:不再是生成和推送 html 片段了,不再需要合成 html,直接数据更新到 redis,然后走动态渲染,性能大大提升
  • 全量更新问题:数据和模板分离,数据更新呢就更新数据,模板更新直接推送模板到 nginx,不需要重新生成所有 html,直接走动态渲染
  • 容量问题:不需要依赖 nginx 所在机器的磁盘空间存储大量的 html,将数据放 redis,html 就存放模板,大大减少空间占用,而且 redis 集群可扩容

# 小结

从三个版本架构的演进来看,其实就是由一个从简到繁,不停的打补丁,最后打补丁都无法解决了, 才明白问题是出在根源上:没有使用缓存!

当第三个版本的时候,使用上了缓存,整个架构从根上就开始变化,后续的手段都是基于这个变化而变化的。