一、主题树膨胀的真实痛点:从电商路由说起

很多做后端开发的朋友,大概率都遇到过这么个头疼事:自己负责的接口路由,越写越长、越写越杂,最后找个接口得翻半天,新增接口还容易和旧的撞名字,连上线的时候都得提心吊胆怕影响别人的功能。我前阵子帮一个做社区电商的朋友排查性能问题,就碰到了典型的「主题树膨胀」坑,今天就拿这个真实场景来掰扯清楚。

这个电商平台的核心功能分几大块:用户、商品、订单、营销,最开始的路由设计很简单,比如用户相关的都放在/user下面,商品的都在/goods下面,结果做了半年多,光用户模块就加了几十个子功能:什么新人专属福利、积分兑换、会员等级、地址管理、消息推送、售后申请……最后路由变成了什么样?给你们看个简化版的例子:

// 旧版用户模块路由(简化)
const routes = [
  { path: '/user/profile', name: '用户资料' },
  { path: '/user/address', name: '用户地址' },
  { path: '/user/member/level', name: '会员等级' },
  { path: '/user/member/points/query', name: '积分查询' },
  { path: '/user/member/points/record', name: '积分记录' },
  { path: '/user/member/points/exchange', name: '积分兑换' },
  { path: '/user/member/welfare/newcomer', name: '新人福利' },
  { path: '/user/message/system', name: '系统消息' },
  { path: '/user/message/marketing', name: '营销消息' },
  { path: '/user/aftersale/apply', name: '售后申请' },
  { path: '/user/aftersale/record', name: '售后记录' },
  // 后面还有几十条类似的路由
];

光看这个列表就知道,找个接口得翻半天,更要命的是,平台做了一个「路由匹配缓存」来加速接口响应——因为每次请求都要从这么多路由里找对应接口,太费时间,所以会把请求路径和对应接口的映射存起来。结果这个缓存慢慢变得特别大,缓存命中率越来越低,接口响应速度直接掉了20%,这就是典型的主题树膨胀拖慢了匹配效率。

二、两种结构的核心差异:别被名字骗了

碰到这个问题,大部分人第一反应是「拆」,但怎么拆?就分成了两种思路:一种是「层级浅宽结构」,一种是「层级深窄结构」,很多人光听名字就觉得「深窄好,层级多肯定更清晰」,其实完全不是那么回事,得结合场景看。

2.1 什么是层级浅宽结构?

简单说就是「主分类少,每个主分类下面的子分类多」,比如刚才的电商场景,浅宽结构会把所有和用户相关的功能都放在/user这个主分类下面,子分类尽量少,每个子分类下面放一堆具体接口。比如刚才的路由,浅宽结构会这么改:

// 浅宽结构路由(简化)
const routes = [
  // 主分类1:用户基础信息,下面放所有和用户基础相关的接口
  { path: '/user/profile', name: '用户资料' },
  { path: '/user/address', name: '用户地址' },
  // 主分类2:用户会员相关,下面放所有会员相关的接口
  { path: '/user/member-level', name: '会员等级' },
  { path: '/user/member-points-query', name: '积分查询' },
  { path: '/user/member-points-record', name: '积分记录' },
  { path: '/user/member-points-exchange', name: '积分兑换' },
  { path: '/user/member-welfare-newcomer', name: '新人福利' },
  // 主分类3:用户消息相关,下面放所有消息相关的接口
  { path: '/user/message-system', name: '系统消息' },
  { path: '/user/message-marketing', name: '营销消息' },
  // 主分类4:用户售后相关,下面放所有售后相关的接口
  { path: '/user/aftersale-apply', name: '售后申请' },
  { path: '/user/aftersale-record', name: '售后记录' },
];

你看,原来的/user/member/points这种三层的,改成了/user/member-points-query这种两层,主分类(就是第一层的/user)没有变,只是把原来的中间层给去掉了,每个主分类下面的接口变多了,这就是浅宽结构——层级浅(最多两层),每个层级的分类宽(子分类多)。

2.2 什么是层级深窄结构?

反过来就是「层级多,每个层级的子分类少」,比如刚才的路由,深窄结构会这么改:

// 深窄结构路由(简化)
const routes = [
  // 第一层:用户模块
  // 第二层:用户模块下的子模块:基础信息
  { path: '/user/base/profile', name: '用户资料' },
  { path: '/user/base/address', name: '用户地址' },
  // 第二层:用户模块下的子模块:会员
  // 第三层:会员子模块下的子模块:等级
  { path: '/user/member/level', name: '会员等级' },
  // 第三层:会员子模块下的子模块:积分
  { path: '/user/member/points/query', name: '积分查询' },
  { path: '/user/member/points/record', name: '积分记录' },
  { path: '/user/member/points/exchange', name: '积分兑换' },
  // 第三层:会员子模块下的子模块:福利
  { path: '/user/member/welfare/newcomer', name: '新人福利' },
  // 第二层:用户模块下的子模块:消息
  { path: '/user/message/system', name: '系统消息' },
  { path: '/user/message/marketing', name: '营销消息' },
  // 第二层:用户模块下的子模块:售后
  { path: '/user/aftersale/apply', name: '售后申请' },
  { path: '/user/aftersale/record', name: '售后记录' },
];

你看,层级变多了,最多到了三层,但是每个层级下面的子分类很少,比如/user下面只有basemembermessageaftersale四个子分类,/user/member下面只有levelpointswelfare三个子分类,这就是深窄结构——层级深,每个层级的分类窄。

三、用真实路由场景推演:两种结构的适配性

回到那个电商平台的问题,当时朋友纠结到底用哪种结构,我们就拿路由匹配的真实场景来推演,先搞清楚路由匹配的本质:每次请求过来,比如是GET /user/member/points/query,系统要做的就是从所有路由里找到和这个路径完全匹配的那个,然后调用对应的接口。

3.1 推演浅宽结构的表现

首先说浅宽结构,刚才的浅宽结构路由,所有接口都在/user这个主分类下面,也就是第一层只有一个主分类,下面有十几个子分类。那匹配的时候,系统会先看请求路径的第一层是不是/user,如果是,再从/user下面的十几个子分类里找对应的那个。

这么做的好处是什么?首先,匹配的层级少,系统不用一层一层往下找,直接在第一层的子分类里找,速度快;其次,缓存的时候,因为第一层只有一个主分类,缓存的内容不会因为中间层的变化而失效,比如你新增一个/user/member-points-consume的接口,不会影响原来的缓存,缓存命中率能稳定在90%以上。

但缺点也很明显,就是主分类下面的子分类太多了,找接口的时候很麻烦,比如你要找积分相关的接口,得从十几个子分类里翻,容易找错;而且如果主分类下面的接口太多,比如超过50个,那第一层的子分类列表就会特别长,维护的时候容易乱。

3.2 推演深窄结构的表现

再看深窄结构,刚才的深窄结构路由,第一层是/user,下面有四个子分类,每个子分类下面又有几个子分类。匹配的时候,系统会先看第一层是不是/user,再看第二层是不是/member,再看第三层是不是/points,最后找/query

这么做的好处是什么?分类清晰,找接口的时候很方便,比如你要找积分相关的接口,就去/user/member/points下面找,一目了然;而且每个层级的子分类少,维护的时候不容易乱,比如新增一个/user/member/points/consume的接口,直接放在/points下面就行,不会影响其他分类。

但缺点也很致命,就是匹配的层级多,系统要一层一层往下找,速度慢;而且缓存的时候,因为层级多,缓存的键是整个路径,比如你新增一个/user/member/points/consume的接口,可能会导致原来的/user/member/points/query的缓存失效,缓存命中率会降到60%以下,接口响应速度会变慢。

3.3 结合电商场景的选择

当时那个电商平台的情况是:用户模块的接口有30多个,不算特别多;而且平台对接口响应速度的要求很高,因为是做社区电商,用户打开页面慢了就会流失;另外,他们有专门的前端团队,前端可以通过文档工具来管理接口,不用后端自己找。

所以最后我们选了浅宽结构,原因有两个:第一,接口数量不多,主分类下面的子分类不会太多,找接口的麻烦可以通过文档工具解决;第二,浅宽结构的匹配速度快,缓存命中率高,能满足平台对响应速度的要求。

那如果换个场景呢?比如一个大型的电商平台,用户模块的接口有100多个,而且平台有很多个后端团队,每个团队负责一个子模块,比如会员团队负责会员相关的接口,消息团队负责消息相关的接口,那这个时候就适合用深窄结构,因为分类清晰,每个团队只需要维护自己的子模块,不会和其他团队冲突;而且大型平台的路由匹配一般会用专门的中间件来优化速度,层级多的问题可以通过中间件解决。

四、两种结构的核心特性对比:别盲目跟风

为了让大家更清楚两种结构的差异,我把它们的核心特性做个对比,大家可以根据自己的场景来选。

4.1 层级浅宽结构的优缺点

优点:

  1. 匹配速度快:层级少,系统不用一层一层往下找,直接在主分类下面找子分类,速度快;
  2. 缓存命中率高:主分类少,缓存的内容不会因为中间层的变化而失效,缓存命中率高;
  3. 适合小团队:团队小,接口数量不多,找接口的麻烦可以通过文档工具解决。

缺点:

  1. 分类不清晰:主分类下面的子分类多,找接口的时候麻烦,容易找错;
  2. 维护难度大:主分类下面的接口太多,维护的时候容易乱,容易出现接口命名冲突;
  3. 不适合多团队:多个团队负责同一个主分类,容易出现冲突,比如两个团队都想给主分类下面加一个接口,容易撞名字。

4.2 层级深窄结构的优缺点

优点:

  1. 分类清晰:层级多,每个层级的子分类少,找接口的时候方便,一目了然;
  2. 维护难度小:每个层级的子分类少,维护的时候不容易乱,不容易出现接口命名冲突;
  3. 适合多团队:多个团队负责不同的子分类,不会出现冲突,比如会员团队负责/user/member下面的接口,消息团队负责/user/message下面的接口,不会撞名字。

缺点:

  1. 匹配速度慢:层级多,系统要一层一层往下找,速度慢;
  2. 缓存命中率低:层级多,缓存的键是整个路径,新增接口容易导致缓存失效,缓存命中率低;
  3. 适合大团队:团队大,接口数量多,需要专门的中间件来优化匹配速度。

4.3 选择结构的核心原则

其实选哪种结构,核心就是两个原则:

  1. 接口数量:如果接口数量少(比如少于50个),适合用浅宽结构;如果接口数量多(比如超过100个),适合用深窄结构;
  2. 团队规模:如果团队小(比如少于10个人),适合用浅宽结构;如果团队大(比如超过20个人),适合用深窄结构;
  3. 性能要求:如果对性能要求高,适合用浅宽结构;如果对性能要求不高,适合用深窄结构。

五、主题设计的分寸与取舍:避坑指南

最后,给大家总结一下主题设计的分寸和取舍,也就是设计主题树的时候要注意的几个点,避免踩坑。

5.1 不要盲目追求层级多

很多人觉得层级多就是好,其实不是,层级多会带来匹配速度慢、缓存命中率低的问题,只有在接口数量多、团队大的情况下,层级多的好处才会超过坏处,否则不要盲目追求层级多。

5.2 不要盲目追求层级少

反过来,也不要盲目追求层级少,层级少会带来分类不清晰、维护难度大的问题,如果接口数量多、团队大,层级少的坏处会超过好处,所以也不要盲目追求层级少。

5.3 分类要符合业务逻辑

不管选哪种结构,分类都要符合业务逻辑,比如和用户相关的接口都放在/user下面,和商品相关的接口都放在/goods下面,不要把和用户相关的接口放在/goods下面,这样找接口的时候会很麻烦。

5.4 命名要规范

不管选哪种结构,命名都要规范,比如用小写字母加连字符,不要用大写字母、下划线或者其他特殊字符,这样找接口的时候会很方便,不容易找错。

5.5 用工具辅助维护

不管选哪种结构,都要用工具辅助维护,比如用接口文档工具(比如Swagger、Postman)来管理接口,这样找接口的时候会很方便,不用自己翻代码;如果选深窄结构,还要用专门的中间件来优化匹配速度,比如用Nginx来做路由匹配,这样层级多的问题就可以解决。

六、总结

主题树膨胀是很多后端开发都会遇到的问题,解决这个问题的核心就是选对结构,要么选层级浅宽结构,要么选层级深窄结构,没有绝对的好坏,只有适合不适合。选结构的时候,要结合接口数量、团队规模、性能要求这三个原则,不要盲目跟风。同时,设计主题树的时候要注意分类符合业务逻辑、命名规范、用工具辅助维护,这样才能避免踩坑。