一、主题树膨胀的真实痛点:从电商路由说起
很多做后端开发的朋友,大概率都遇到过这么个头疼事:自己负责的接口路由,越写越长、越写越杂,最后找个接口得翻半天,新增接口还容易和旧的撞名字,连上线的时候都得提心吊胆怕影响别人的功能。我前阵子帮一个做社区电商的朋友排查性能问题,就碰到了典型的「主题树膨胀」坑,今天就拿这个真实场景来掰扯清楚。
这个电商平台的核心功能分几大块:用户、商品、订单、营销,最开始的路由设计很简单,比如用户相关的都放在/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下面只有base、member、message、aftersale四个子分类,/user/member下面只有level、points、welfare三个子分类,这就是深窄结构——层级深,每个层级的分类窄。
三、用真实路由场景推演:两种结构的适配性
回到那个电商平台的问题,当时朋友纠结到底用哪种结构,我们就拿路由匹配的真实场景来推演,先搞清楚路由匹配的本质:每次请求过来,比如是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 层级浅宽结构的优缺点
优点:
- 匹配速度快:层级少,系统不用一层一层往下找,直接在主分类下面找子分类,速度快;
- 缓存命中率高:主分类少,缓存的内容不会因为中间层的变化而失效,缓存命中率高;
- 适合小团队:团队小,接口数量不多,找接口的麻烦可以通过文档工具解决。
缺点:
- 分类不清晰:主分类下面的子分类多,找接口的时候麻烦,容易找错;
- 维护难度大:主分类下面的接口太多,维护的时候容易乱,容易出现接口命名冲突;
- 不适合多团队:多个团队负责同一个主分类,容易出现冲突,比如两个团队都想给主分类下面加一个接口,容易撞名字。
4.2 层级深窄结构的优缺点
优点:
- 分类清晰:层级多,每个层级的子分类少,找接口的时候方便,一目了然;
- 维护难度小:每个层级的子分类少,维护的时候不容易乱,不容易出现接口命名冲突;
- 适合多团队:多个团队负责不同的子分类,不会出现冲突,比如会员团队负责
/user/member下面的接口,消息团队负责/user/message下面的接口,不会撞名字。
缺点:
- 匹配速度慢:层级多,系统要一层一层往下找,速度慢;
- 缓存命中率低:层级多,缓存的键是整个路径,新增接口容易导致缓存失效,缓存命中率低;
- 适合大团队:团队大,接口数量多,需要专门的中间件来优化匹配速度。
4.3 选择结构的核心原则
其实选哪种结构,核心就是两个原则:
- 接口数量:如果接口数量少(比如少于50个),适合用浅宽结构;如果接口数量多(比如超过100个),适合用深窄结构;
- 团队规模:如果团队小(比如少于10个人),适合用浅宽结构;如果团队大(比如超过20个人),适合用深窄结构;
- 性能要求:如果对性能要求高,适合用浅宽结构;如果对性能要求不高,适合用深窄结构。
五、主题设计的分寸与取舍:避坑指南
最后,给大家总结一下主题设计的分寸和取舍,也就是设计主题树的时候要注意的几个点,避免踩坑。
5.1 不要盲目追求层级多
很多人觉得层级多就是好,其实不是,层级多会带来匹配速度慢、缓存命中率低的问题,只有在接口数量多、团队大的情况下,层级多的好处才会超过坏处,否则不要盲目追求层级多。
5.2 不要盲目追求层级少
反过来,也不要盲目追求层级少,层级少会带来分类不清晰、维护难度大的问题,如果接口数量多、团队大,层级少的坏处会超过好处,所以也不要盲目追求层级少。
5.3 分类要符合业务逻辑
不管选哪种结构,分类都要符合业务逻辑,比如和用户相关的接口都放在/user下面,和商品相关的接口都放在/goods下面,不要把和用户相关的接口放在/goods下面,这样找接口的时候会很麻烦。
5.4 命名要规范
不管选哪种结构,命名都要规范,比如用小写字母加连字符,不要用大写字母、下划线或者其他特殊字符,这样找接口的时候会很方便,不容易找错。
5.5 用工具辅助维护
不管选哪种结构,都要用工具辅助维护,比如用接口文档工具(比如Swagger、Postman)来管理接口,这样找接口的时候会很方便,不用自己翻代码;如果选深窄结构,还要用专门的中间件来优化匹配速度,比如用Nginx来做路由匹配,这样层级多的问题就可以解决。
六、总结
主题树膨胀是很多后端开发都会遇到的问题,解决这个问题的核心就是选对结构,要么选层级浅宽结构,要么选层级深窄结构,没有绝对的好坏,只有适合不适合。选结构的时候,要结合接口数量、团队规模、性能要求这三个原则,不要盲目跟风。同时,设计主题树的时候要注意分类符合业务逻辑、命名规范、用工具辅助维护,这样才能避免踩坑。
评论
围绕“主题树膨胀拖慢匹配:采用层级浅宽结构还是深窄结构?用真实路由场景推演主题设计的分寸与取舍。”参与讨论