Java 项目实战: 交友项目 - 技术架构与选型剖析

概述

交友项目的后端不是"一个 Spring Boot 项目写完所有功能",而是按"接入层 / 应用层 / 域服务层 / 数据层"分层的 Dubbo 微服务;本文把这条分层链路、每一层为什么这么分、以及十几个技术组件各自的取舍理由讲清楚,并用一次真实的验证码登录请求把整条链路串起来。

纲要

  • 前后端分离:架构的起点
    • 传统模板渲染模式的三宗罪
    • 分离模式的接口契约协作
    • 两种模式对比表 + 数据流 Mermaid 对比图
    • 本项目的三段式形态:移动端 App + 后端 API + 后台管理
  • 后端分层架构
    • 接入层 / 应用层 / 域服务层 / 数据层 / 中间件 / 外部服务
    • 分层职责表 + 分层 Mermaid 图
    • 为什么应用层与域服务层必须分开
  • 技术选型理由
    • Dubbo、MongoDB、Redis、RabbitMQ、FastDFS/OSS、环信的取舍表
    • 每个选型的"如果不用它会怎样"
  • 一次请求的完整链路(Mermaid 时序图,以验证码登录为例)
  • 工程结构实读
    • mf 聚合工程模块树(标注所属层)
    • 父 pom 与模块 pom 的真实版本组合
  • 技术栈与后续章节的对应关系
  • 常见误解澄清
  • 官方文档
  • 总结

从"前后端分离"说起

讲架构绕不开第一个决定:页面由谁渲染。这个决定直接决定了后面要不要拆服务、拆几层。

传统模式的问题

早期 Java Web 项目是后端用 JSP、Thymeleaf、FreeMarker 这类模板引擎把 HTML 拼好再吐给浏览器。它的问题不是"技术落后",而是三条硬伤:

  • 前后端强耦合 。页面结构写在后端的 .jsp / .ftl 里,改个按钮位置得动后端代码、重新发版。前端工程师没有后端环境跑不起来页面。
  • 无法多端复用。模板渲染出的是 HTML,只服务于浏览器。同一个业务要给 App 用,只能再写一套返回 JSON 的接口,业务逻辑复制一份。
  • 调试链路长。前端调样式必须等后端把同一个 " 路由/参数不匹配 → 前端手改 JS 里的 URL 常量。

分离模式

前后端分离把"渲染"这件事从后端拿走:后端只提供 RESTful 接口,统一返回 JSON;前端独立开发、独立部署,两端靠接口契约协作。

接口契约是核心,不是"前端用不用 Vue"。契约包括 URL 路径、请求方法、请求参数结构、返回体结构、错误码含义。契约一旦定下,前端可以用 Mock 数据先跑起来,后端也可以先用 Postman 把接口调通,两边并行。

对比项 传统模板渲染 前后端分离
渲染位置 后端(JSP/Thymeleaf 拼 HTML) 前端(App/浏览器拼 UI)
后端产物 HTML 字符串 JSON 数据
开发方式 前端依赖后端写页面 前后端并行,靠接口契约
部署方式 打在一个 war 包里 前端静态资源与后端服务分开部署
多端支持 基本只服务浏览器 同一批接口可被 App、后台、小程序复用
接口契约 隐式(写在 JSP 里) 显式(接口文档,如 YApi 定义)
调试方式 改页面要重启后端 Postman 调接口 + 前端 Mock

用 Mermaid 对比两种模式的数据流。
#mermaid-svg-AGIsqJvSKP6bIseO{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-AGIsqJvSKP6bIseO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AGIsqJvSKP6bIseO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AGIsqJvSKP6bIseO .error-icon{fill:#552222;}#mermaid-svg-AGIsqJvSKP6bIseO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AGIsqJvSKP6bIseO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AGIsqJvSKP6bIseO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AGIsqJvSKP6bIseO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AGIsqJvSKP6bIseO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AGIsqJvSKP6bIseO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AGIsqJvSKP6bIseO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AGIsqJvSKP6bIseO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AGIsqJvSKP6bIseO .marker.cross{stroke:#333333;}#mermaid-svg-AGIsqJvSKP6bIseO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AGIsqJvSKP6bIseO p{margin:0;}#mermaid-svg-AGIsqJvSKP6bIseO .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-AGIsqJvSKP6bIseO .cluster-label text{fill:#333;}#mermaid-svg-AGIsqJvSKP6bIseO .cluster-label span{color:#333;}#mermaid-svg-AGIsqJvSKP6bIseO .cluster-label span p{background-color:transparent;}#mermaid-svg-AGIsqJvSKP6bIseO .label text,#mermaid-svg-AGIsqJvSKP6bIseO span{fill:#333;color:#333;}#mermaid-svg-AGIsqJvSKP6bIseO .node rect,#mermaid-svg-AGIsqJvSKP6bIseO .node circle,#mermaid-svg-AGIsqJvSKP6bIseO .node ellipse,#mermaid-svg-AGIsqJvSKP6bIseO .node polygon,#mermaid-svg-AGIsqJvSKP6bIseO .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-AGIsqJvSKP6bIseO .rough-node .label text,#mermaid-svg-AGIsqJvSKP6bIseO .node .label text,#mermaid-svg-AGIsqJvSKP6bIseO .image-shape .label,#mermaid-svg-AGIsqJvSKP6bIseO .icon-shape .label{text-anchor:middle;}#mermaid-svg-AGIsqJvSKP6bIseO .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-AGIsqJvSKP6bIseO .rough-node .label,#mermaid-svg-AGIsqJvSKP6bIseO .node .label,#mermaid-svg-AGIsqJvSKP6bIseO .image-shape .label,#mermaid-svg-AGIsqJvSKP6bIseO .icon-shape .label{text-align:center;}#mermaid-svg-AGIsqJvSKP6bIseO .node.clickable{cursor:pointer;}#mermaid-svg-AGIsqJvSKP6bIseO .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-AGIsqJvSKP6bIseO .arrowheadPath{fill:#333333;}#mermaid-svg-AGIsqJvSKP6bIseO .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-AGIsqJvSKP6bIseO .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-AGIsqJvSKP6bIseO .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AGIsqJvSKP6bIseO .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-AGIsqJvSKP6bIseO .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AGIsqJvSKP6bIseO .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-AGIsqJvSKP6bIseO .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-AGIsqJvSKP6bIseO .cluster text{fill:#333;}#mermaid-svg-AGIsqJvSKP6bIseO .cluster span{color:#333;}#mermaid-svg-AGIsqJvSKP6bIseO div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-AGIsqJvSKP6bIseO .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-AGIsqJvSKP6bIseO rect.text{fill:none;stroke-width:0;}#mermaid-svg-AGIsqJvSKP6bIseO .icon-shape,#mermaid-svg-AGIsqJvSKP6bIseO .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AGIsqJvSKP6bIseO .icon-shape p,#mermaid-svg-AGIsqJvSKP6bIseO .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-AGIsqJvSKP6bIseO .icon-shape .label rect,#mermaid-svg-AGIsqJvSKP6bIseO .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AGIsqJvSKP6bIseO .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-AGIsqJvSKP6bIseO .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-AGIsqJvSKP6bIseO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 传统模式:渲染在后端
请求页面
查库
拼好的 HTML
浏览器
后端服务
数据库
#mermaid-svg-dlvCFZJtuMfX7JNg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dlvCFZJtuMfX7JNg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dlvCFZJtuMfX7JNg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dlvCFZJtuMfX7JNg .error-icon{fill:#552222;}#mermaid-svg-dlvCFZJtuMfX7JNg .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dlvCFZJtuMfX7JNg .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dlvCFZJtuMfX7JNg .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dlvCFZJtuMfX7JNg .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dlvCFZJtuMfX7JNg .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dlvCFZJtuMfX7JNg .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dlvCFZJtuMfX7JNg .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dlvCFZJtuMfX7JNg .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dlvCFZJtuMfX7JNg .marker.cross{stroke:#333333;}#mermaid-svg-dlvCFZJtuMfX7JNg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dlvCFZJtuMfX7JNg p{margin:0;}#mermaid-svg-dlvCFZJtuMfX7JNg .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-dlvCFZJtuMfX7JNg .cluster-label text{fill:#333;}#mermaid-svg-dlvCFZJtuMfX7JNg .cluster-label span{color:#333;}#mermaid-svg-dlvCFZJtuMfX7JNg .cluster-label span p{background-color:transparent;}#mermaid-svg-dlvCFZJtuMfX7JNg .label text,#mermaid-svg-dlvCFZJtuMfX7JNg span{fill:#333;color:#333;}#mermaid-svg-dlvCFZJtuMfX7JNg .node rect,#mermaid-svg-dlvCFZJtuMfX7JNg .node circle,#mermaid-svg-dlvCFZJtuMfX7JNg .node ellipse,#mermaid-svg-dlvCFZJtuMfX7JNg .node polygon,#mermaid-svg-dlvCFZJtuMfX7JNg .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-dlvCFZJtuMfX7JNg .rough-node .label text,#mermaid-svg-dlvCFZJtuMfX7JNg .node .label text,#mermaid-svg-dlvCFZJtuMfX7JNg .image-shape .label,#mermaid-svg-dlvCFZJtuMfX7JNg .icon-shape .label{text-anchor:middle;}#mermaid-svg-dlvCFZJtuMfX7JNg .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-dlvCFZJtuMfX7JNg .rough-node .label,#mermaid-svg-dlvCFZJtuMfX7JNg .node .label,#mermaid-svg-dlvCFZJtuMfX7JNg .image-shape .label,#mermaid-svg-dlvCFZJtuMfX7JNg .icon-shape .label{text-align:center;}#mermaid-svg-dlvCFZJtuMfX7JNg .node.clickable{cursor:pointer;}#mermaid-svg-dlvCFZJtuMfX7JNg .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-dlvCFZJtuMfX7JNg .arrowheadPath{fill:#333333;}#mermaid-svg-dlvCFZJtuMfX7JNg .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-dlvCFZJtuMfX7JNg .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-dlvCFZJtuMfX7JNg .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dlvCFZJtuMfX7JNg .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-dlvCFZJtuMfX7JNg .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dlvCFZJtuMfX7JNg .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-dlvCFZJtuMfX7JNg .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-dlvCFZJtuMfX7JNg .cluster text{fill:#333;}#mermaid-svg-dlvCFZJtuMfX7JNg .cluster span{color:#333;}#mermaid-svg-dlvCFZJtuMfX7JNg div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-dlvCFZJtuMfX7JNg .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-dlvCFZJtuMfX7JNg rect.text{fill:none;stroke-width:0;}#mermaid-svg-dlvCFZJtuMfX7JNg .icon-shape,#mermaid-svg-dlvCFZJtuMfX7JNg .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dlvCFZJtuMfX7JNg .icon-shape p,#mermaid-svg-dlvCFZJtuMfX7JNg .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-dlvCFZJtuMfX7JNg .icon-shape .label rect,#mermaid-svg-dlvCFZJtuMfX7JNg .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dlvCFZJtuMfX7JNg .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-dlvCFZJtuMfX7JNg .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-dlvCFZJtuMfX7JNg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 前后端分离:渲染在前端
HTTP + JSON 请求
查库
JSON 数据
前端本地渲染 UI
App / 后台前端
后端服务
数据库
用户界面

本项目的形态

交友项目不是"一个前端 + 一个后端",而是三段式:

  • 移动端 App :Flutter + Android,集成环信 SDK 做即时通讯,用 shared_preferences 存本地数据、sqflite 存本地库,connectivity 监听网络、iconfont 做图标。交给前端团队开发,后端通过 APK 对接测试(讲义用网易 MuMu 模拟器)。
  • 后端服务:只提供 API,返回 JSON。这是课程的重心。
  • 后台管理系统:另一套前端页面,用于运营侧管理(用户、内容审核、数据统计),同样走后端 API。

注意一个容易搞混的点:三个前端共用后端,但不共用同一个入口模块 。App 走 mf-app-server,后台走后台对应的应用服务模块(后台模块在本章工程快照里还没出现,到"后台系统"一章才加入)。两端的展示需求不同,接口编排也就不同------这一点后面讲"应用层与域服务层为什么要分开"时会展开。

后端分层架构

讲义 1.8 节给了一张架构图加一份要点清单(Dubbo 做微服务、RabbitMQ 做消息中间件、Redis 做缓存、MySQL + MongoDB 存海量数据、集成第三方组件、Spark MLlib 做推荐)。图没法在文本里复现,但结合工程实读的模块结构,可以把这套后端归纳成清晰的分层。
#mermaid-svg-5HSgq9Fnf7jUrCXZ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-5HSgq9Fnf7jUrCXZ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5HSgq9Fnf7jUrCXZ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5HSgq9Fnf7jUrCXZ .error-icon{fill:#552222;}#mermaid-svg-5HSgq9Fnf7jUrCXZ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5HSgq9Fnf7jUrCXZ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5HSgq9Fnf7jUrCXZ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5HSgq9Fnf7jUrCXZ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5HSgq9Fnf7jUrCXZ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5HSgq9Fnf7jUrCXZ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5HSgq9Fnf7jUrCXZ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5HSgq9Fnf7jUrCXZ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5HSgq9Fnf7jUrCXZ .marker.cross{stroke:#333333;}#mermaid-svg-5HSgq9Fnf7jUrCXZ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5HSgq9Fnf7jUrCXZ p{margin:0;}#mermaid-svg-5HSgq9Fnf7jUrCXZ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-5HSgq9Fnf7jUrCXZ .cluster-label text{fill:#333;}#mermaid-svg-5HSgq9Fnf7jUrCXZ .cluster-label span{color:#333;}#mermaid-svg-5HSgq9Fnf7jUrCXZ .cluster-label span p{background-color:transparent;}#mermaid-svg-5HSgq9Fnf7jUrCXZ .label text,#mermaid-svg-5HSgq9Fnf7jUrCXZ span{fill:#333;color:#333;}#mermaid-svg-5HSgq9Fnf7jUrCXZ .node rect,#mermaid-svg-5HSgq9Fnf7jUrCXZ .node circle,#mermaid-svg-5HSgq9Fnf7jUrCXZ .node ellipse,#mermaid-svg-5HSgq9Fnf7jUrCXZ .node polygon,#mermaid-svg-5HSgq9Fnf7jUrCXZ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-5HSgq9Fnf7jUrCXZ .rough-node .label text,#mermaid-svg-5HSgq9Fnf7jUrCXZ .node .label text,#mermaid-svg-5HSgq9Fnf7jUrCXZ .image-shape .label,#mermaid-svg-5HSgq9Fnf7jUrCXZ .icon-shape .label{text-anchor:middle;}#mermaid-svg-5HSgq9Fnf7jUrCXZ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-5HSgq9Fnf7jUrCXZ .rough-node .label,#mermaid-svg-5HSgq9Fnf7jUrCXZ .node .label,#mermaid-svg-5HSgq9Fnf7jUrCXZ .image-shape .label,#mermaid-svg-5HSgq9Fnf7jUrCXZ .icon-shape .label{text-align:center;}#mermaid-svg-5HSgq9Fnf7jUrCXZ .node.clickable{cursor:pointer;}#mermaid-svg-5HSgq9Fnf7jUrCXZ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-5HSgq9Fnf7jUrCXZ .arrowheadPath{fill:#333333;}#mermaid-svg-5HSgq9Fnf7jUrCXZ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-5HSgq9Fnf7jUrCXZ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-5HSgq9Fnf7jUrCXZ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5HSgq9Fnf7jUrCXZ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-5HSgq9Fnf7jUrCXZ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5HSgq9Fnf7jUrCXZ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-5HSgq9Fnf7jUrCXZ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-5HSgq9Fnf7jUrCXZ .cluster text{fill:#333;}#mermaid-svg-5HSgq9Fnf7jUrCXZ .cluster span{color:#333;}#mermaid-svg-5HSgq9Fnf7jUrCXZ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-5HSgq9Fnf7jUrCXZ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-5HSgq9Fnf7jUrCXZ rect.text{fill:none;stroke-width:0;}#mermaid-svg-5HSgq9Fnf7jUrCXZ .icon-shape,#mermaid-svg-5HSgq9Fnf7jUrCXZ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5HSgq9Fnf7jUrCXZ .icon-shape p,#mermaid-svg-5HSgq9Fnf7jUrCXZ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-5HSgq9Fnf7jUrCXZ .icon-shape .label rect,#mermaid-svg-5HSgq9Fnf7jUrCXZ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5HSgq9Fnf7jUrCXZ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-5HSgq9Fnf7jUrCXZ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-5HSgq9Fnf7jUrCXZ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 外部服务
中间件
数据层
域服务层(Dubbo 服务提供者,承载核心业务能力)
应用层(按端拆分,只做接口编排)
接入层
客户端
Dubbo 调用
Dubbo 调用
Dubbo 调用
移动端 App
后台管理前端
Nginx / API 网关:统一入口、路由、静态资源、反向代理
mf-app-server:面向 App
mf-admin:面向后台
mf-dubbo-db:MySQL 域服务
mf-dubbo-mongo:MongoDB 域服务
MySQL:关系型业务数据
MongoDB:动态、位置等文档数据
Redis:验证码 / 热点缓存
OSS:图片对象存储
FastDFS:小视频分布式文件存储
RabbitMQ:异步解耦、日志采集、内容审核
阿里云短信
百度人脸识别
环信 IM
Spark + MLlib 推荐

各层职责一句话说清:

分层 对应模块/组件 职责 不该做的事
客户端 App、后台前端 渲染 UI、收集用户输入 不该直接连数据库
接入层 Nginx / 网关 统一入口、路由转发、限流、鉴权前置 不该写业务逻辑
应用层 mf-app-server、后台应用模块 接口编排、参数校验、协议适配、组装 VO 不该写可复用的核心业务
域服务层 mf-dubbo-db、mf-dubbo-mongo 承载可复用的业务能力,通过 Dubbo 暴露 不该感知具体是哪个端在调
数据层 MySQL、MongoDB、Redis、OSS、FastDFS 持久化与缓存,按数据特征分工 不该被应用层直连
中间件 RabbitMQ 异步解耦、削峰、日志采集、审核事件 不该承担同步强一致流程
外部服务 阿里云短信、百度人脸、环信、推荐系统 提供专业能力(短信、识别、IM、推荐) 不该自研这些重资产能力

为什么应用层与域服务层要分开

这是整篇架构里最该想透的一处。前置课 Dubbo 里已经埋了伏笔:cloud-dubbo-demo 把工程拆成 dubbo-api(接口)、dubbo-domains(业务域)、user-service / order-service(对外应用)。项目把同一思路放大:接口放 mf-dubbo-interface,业务实现放 mf-dubbo-db / mf-dubbo-mongo,对外入口放 mf-app-server。

分开的理由有三条,都很实在:

  1. 同一批业务能力被多个端复用 。App 要"根据手机号查用户",后台也要查同一个用户,只是展示字段和权限校验不同。如果把业务实现写在 mf-app-server 里,后台就得再写一遍。放进域服务层,两个应用层都通过 Dubbo 调同一个 UserApi,业务只有一份。
  2. 应用层按端做适配,域层保持稳定。App 端返回的 VO 要精简(省流量),后台端要返回完整字段(方便运营看),这种差异属于"适配",放在应用层;而"查用户"这个能力本身不变,放在域层。端多了也不影响域层。
  3. 独立扩容与团队分工 。App 端流量大就多起几个 mf-app-server,动态读写压力大多起几个 mf-dubbo-mongo,两者扩缩容互不牵连。团队也可以按域切分,各管一块。

一句话概括:应用层是"面向端的门面",域服务层是"可复用的业务资产"。门面可以有好几扇,资产只有一套。

技术选型理由

讲义 1.7 到 1.10 把技术栈列了四遍,但只有名字没有理由。下面的表补上"为什么用它 / 替代方案 / 不用会怎样"。

技术 解决的什么问题 替代方案 不用的后果
Dubbo(Spring Cloud Alibaba Dubbo) 应用层与域服务层之间的高性能 RPC 调用 Feign + HTTP、RestTemplate 用 HTTP 调内部服务,序列化开销大、契约松散,几十个服务时延迟叠加明显
MongoDB 存圈子动态、附近的人(地理位置) MySQL JSON 字段 动态字段结构多变,用 MySQL 要频繁改表;地理位置查询 MySQL 5.x 原生支持弱
Redis 验证码过期、热点数据缓存 本地缓存 Guava、数据库直查 验证码状态放数据库,过期要靠定时任务清理,多实例下还得考虑同步
RabbitMQ 发布动态后的异步处理、日志采集、内容审核解耦 线程池异步、Kafka 用线程池做异步,服务重启任务就丢;审核这种跨系统事件更没法可靠投递
FastDFS / 阿里云 OSS 图片与小视频的文件存储 直接写服务器本地磁盘 本地磁盘不共享,起第二个实例就读不到第一个实例上传的文件,扩容即失效
环信 IM 即时通讯(长连接、离线消息、多端同步) 自研 Netty 长连接 IM 的离线消息、顺序保证、多端同步、消息漫游是极大的工程量,自研投入产出比极低

几条值得单独展开:

为什么用 Dubbo 而不是纯 Feign。 Spring Cloud 默认的 Feign 走 HTTP + JSON,开发简单,但每次调用都要做一遍 HTTP 报文编解码,内部服务之间高频调用时开销明显。Dubbo 走二进制协议 + 接口级代理,性能更好,而且契约严格 ------服务提供方和消费方必须共享同一个 mf-dubbo-interface 模块,接口签名对不上编译就过不去。项目的域服务层被两个应用层复用,契约严格反而成了优点。Spring Cloud Alibaba 把 Dubbo 和 Nacos 串起来,注册中心用 Nacos,服务和消费方通过 spring-cloud://localhost 走注册发现。

为什么 MongoDB 存圈子动态,MySQL 存用户。 两者的数据特征完全不同:

  • 用户、粉丝关系这类数据字段固定、关系明确、需要事务,用 MySQL + MyBatis-Plus 天然合适。
  • 圈子动态(发布内容、点赞、评论)字段结构灵活、写入频繁、几乎不需要跨表强事务,用 MongoDB 的文档模型更顺。而且"附近的人"依赖地理位置查询,MongoDB 的 2dsphere 地理索引直接支持 $near,MySQL 5.x 做这个很别扭。

这不是"二选一",而是分工。同一个项目里 MySQL 管关系型业务、MongoDB 管文档型业务、Redis 管缓存,是常见的多存储组合,不是架构冗余。

RabbitMQ 的位置。 它解决的是"同步链路太长"的问题。用户发一条动态,主流程只需要写库并返回成功;而给粉丝推消息、写日志、送审核这些事可以异步丢给 MQ。这里有个坑:异步意味着最终一致,主流程不能依赖异步任务的结果。

FastDFS/OSS 而不是本地磁盘。 只要服务会起多个实例,本地磁盘存储就必然出问题------A 实例上传的图片,B 实例的响应里给不出可访问的地址。对象存储天生带共享、扩容、CDN 能力。项目里图片走阿里云 OSS,小视频走 FastDFS。

为什么用环信而不是自研 IM。 很多人低估 IM 的复杂度:长连接保活、断线重连、消息顺序、离线消息拉取、多端同步、消息漫游、群聊、敏感词。环信这类服务把这些做成 SDK,后端只需通过 com.easemob.im:im-sdk-core 做服务端集成(如注册用户、发系统消息),业务侧专注社交逻辑。

一次请求的完整链路

分层图看的是"静态结构",时序图才能看出"动态链路"。用本章工程里真实存在的一次验证码登录 请求来走一遍------这个流程的代码在 mf-app-server 里已经有原型(LoginController + UserService)。
RabbitMQ MySQL mf-dubbo-db(域服务层) Redis mf-app-server(应用层) Nginx / 网关 移动端 App RabbitMQ MySQL mf-dubbo-db(域服务层) Redis mf-app-server(应用层) Nginx / 网关 移动端 App #mermaid-svg-GhvedmSwRwyBjQ8W{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-GhvedmSwRwyBjQ8W .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-GhvedmSwRwyBjQ8W .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-GhvedmSwRwyBjQ8W .error-icon{fill:#552222;}#mermaid-svg-GhvedmSwRwyBjQ8W .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-GhvedmSwRwyBjQ8W .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-GhvedmSwRwyBjQ8W .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-GhvedmSwRwyBjQ8W .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-GhvedmSwRwyBjQ8W .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-GhvedmSwRwyBjQ8W .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-GhvedmSwRwyBjQ8W .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-GhvedmSwRwyBjQ8W .marker{fill:#333333;stroke:#333333;}#mermaid-svg-GhvedmSwRwyBjQ8W .marker.cross{stroke:#333333;}#mermaid-svg-GhvedmSwRwyBjQ8W svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-GhvedmSwRwyBjQ8W p{margin:0;}#mermaid-svg-GhvedmSwRwyBjQ8W .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-GhvedmSwRwyBjQ8W text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-GhvedmSwRwyBjQ8W .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-GhvedmSwRwyBjQ8W .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-GhvedmSwRwyBjQ8W .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-GhvedmSwRwyBjQ8W .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-GhvedmSwRwyBjQ8W #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-GhvedmSwRwyBjQ8W .sequenceNumber{fill:white;}#mermaid-svg-GhvedmSwRwyBjQ8W #sequencenumber{fill:#333;}#mermaid-svg-GhvedmSwRwyBjQ8W #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-GhvedmSwRwyBjQ8W .messageText{fill:#333;stroke:none;}#mermaid-svg-GhvedmSwRwyBjQ8W .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-GhvedmSwRwyBjQ8W .labelText,#mermaid-svg-GhvedmSwRwyBjQ8W .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-GhvedmSwRwyBjQ8W .loopText,#mermaid-svg-GhvedmSwRwyBjQ8W .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-GhvedmSwRwyBjQ8W .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-GhvedmSwRwyBjQ8W .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-GhvedmSwRwyBjQ8W .noteText,#mermaid-svg-GhvedmSwRwyBjQ8W .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-GhvedmSwRwyBjQ8W .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-GhvedmSwRwyBjQ8W .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-GhvedmSwRwyBjQ8W .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-GhvedmSwRwyBjQ8W .actorPopupMenu{position:absolute;}#mermaid-svg-GhvedmSwRwyBjQ8W .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-GhvedmSwRwyBjQ8W .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-GhvedmSwRwyBjQ8W .actor-man circle,#mermaid-svg-GhvedmSwRwyBjQ8W line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-GhvedmSwRwyBjQ8W :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户收到短信,填入验证码再次提交 alt 用户不存在 opt 需要异步处理(如记录登录日志) POST /user/login {phone} 1 路由转发 2 参数校验(手机号格式) 3 生成 6 位验证码 4 SET CHECK_CODE_手机号,有效期 5 分钟 5 返回 200,不返回验证码本身 6 POST /user/loginVerification {phone, code} 7 路由转发 8 GET CHECK_CODE_手机号 9 返回已下发验证码 10 比对验证码,一致则删除 Redis 中的 key 11 Dubbo 调用 UserApi.findByMobile(phone) 12 SELECT * FROM tb_user WHERE mobile = ? 13 结果集(可能为空) 14 Dubbo 调用 UserApi.save(user) 15 INSERT INTO tb_user 16 返回 User 17 用 JWT 生成 token(写入 id 与手机号) 18 发送消息 19 返回 JSON {token, isNew} 20

这条链路把分层全部串起来了:接入层 只做转发,应用层 做参数校验、验证码校验、token 生成和结果组装,域服务层 通过 Dubbo 承接"查用户 / 存用户"这类可复用能力,数据层 里 Redis 管验证码、MySQL 管用户,中间件承接异步任务。一次请求穿过五层,各层各司其职。

注意几个工程细节:应用层返回给 App 的是 ResponseEntity,"查用户"这个动作则通过 @Reference 注入的 Dubbo 接口调用,域服务层用 @DubboService 暴露实现。这个对称关系(消费方 @Reference / 提供方 @DubboService)是 Dubbo 接入的标志。

工程结构实读

讲义 3.2 节给出了模块划分表,工程实读印证了这一点。这是本章快照下 mf 聚合工程的真实结构,每个模块标注它属于哪一层。

tree 复制代码
mf/                              聚合父工程(pom,只做版本管理与模块聚合)
├── mf-autoconfig/               公共层|自动装配组件(短信、OSS、人脸识别等 Template)
├── mf-model/                    公共层|实体类(User 等,被各层共享)
├── mf-commons/                  公共层|工具类(JWT 工具、通用返回对象等)
├── mf-dubbo/                    域服务层|Dubbo 子模块聚合(文件夹性质)
│   ├── mf-dubbo-interface/      域服务层|Dubbo 接口(UserApi 等契约)
│   ├── mf-dubbo-db/             域服务层|MySQL 域服务(UserApiImpl + Mapper)
│   └── mf-dubbo-mongo/          域服务层|MongoDB 域服务
└── mf-app-server/               应用层|面向 App 的入口,Controller + Service 编排

几点读法:

  • mf-dubbo 本身不写代码,它是个聚合子模块(可以理解为文件夹),把三个 Dubbo 相关模块收拢在一起统一管理。所以顶层父 pom 的 <modules> 里只写 mf-dubbo 一项,而 mf-dubbo 自己的 pom 再聚合 interface / db / mongo 三个子模块。
  • mf-dubbo-interface 是接口契约模块,被应用层(消费方)和域服务层(提供方)同时依赖------这就是 Dubbo 契约严格的原因:两边共享同一份接口定义。
  • 本层只做概览,各模块内部的包结构、类职责,见同系列《项目模块介绍》一篇。

父 pom 的关键片段(版本照实抄,mf/pom.xml)。

xml 复制代码
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.9.RELEASE</version>
</parent>

<properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
    <mysql.version>5.1.47</mysql.version>
    <mybatis.mybatis-plus>3.4.1</mybatis.mybatis-plus>
    <lombok.version>1.18.8</lombok.version>
    <mongo.version>4.0.5</mongo.version>
    <spring-cloud.version>Hoxton.SR10</spring-cloud.version>
    <spring-cloud-alibaba.version>2.2.5.RELEASE</spring-cloud-alibaba.version>
</properties>

mf-app-server 作为应用层,集合了"接 App 请求"所需的依赖(pom.xml 节选)。

xml 复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.1</version>
    </dependency>
    <dependency>
        <groupId>com.github.tobato</groupId>
        <artifactId>fastdfs-client</artifactId>
        <version>1.26.7</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-dubbo</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
</dependencies>

有一个讲义与工程的差异 要说明:讲义 3.2.4 节里模块间的依赖写的是 com.heima:mf-dubbo-interface,而工程实读的 groupId 是 com.mf:mf-dubbo-interface。两者只是组织标识不同,依赖关系一致,实际以工程 pom.xml 为准。另外父 pom 的 dependencyManagement 里引用了 ${mongodb.version},而 properties 里定义的是 mongo.version,这是原始工程的一处小笔误,不影响 Dubbo 与数据层这条主线。

各模块的端口与中间件地址(讲义 4.4、5.3.1 节明确给出,直接照实):

yml 复制代码
# mf-app-server(应用层)
server:
  port: 18080
spring:
  application:
    name: mf-app-server
  redis:
    host: 192.168.136.160
    port: 6379
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.136.160:8848
dubbo:
  registry:
    address: spring-cloud://localhost
  consumer:
    check: false      # 启动时先不校验提供者是否在线,便于本地分头启动
yml 复制代码
# mf-dubbo-db(域服务层)
server:
  port: 18081
spring:
  application:
    name: mf-dubbo-db
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/mf?useUnicode=true&characterEncoding=utf8&useSSL=false
    username: root
    password: root
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.136.160:8848
dubbo:
  protocol:
    name: dubbo
    port: 20881          # Dubbo 二进制协议端口,与 HTTP 的 18081 区分开
  registry:
    address: spring-cloud://localhost
  scan:
    base-packages: com.mf.dubbo.api   # Dubbo 服务实现所在包

consumer.check: false 这个配置值得留意:不打开时,如果提供者还没启动,应用层启动就会报"找不到服务"而失败。本地分头启动时打开它,可以先把应用层拉起来,方便调试。

技术栈与后续章节的对应关系

这套架构是"横切"的,具体技术会分散到后面各章逐块落地。对照下表看学习顺序更清楚。

技术 用在哪个功能 对应章节
Maven 聚合工程 整体工程搭建与模块管理 本章(环境搭建)
阿里云短信 + 自动装配 登录验证码下发 01 章(短信验证码)
JWT + Dubbo 基础 用户登录、域服务调用 01 章(用户登录)
Redis 验证码过期、登录态、热点缓存 01 章起贯穿全程
MySQL + MyBatis-Plus 用户、粉丝关系等关系型数据 01 章起贯穿全程
MongoDB 圈子动态、附近的人(地理位置) 03-MongoDB 基础 / 04-圈子功能
RabbitMQ 动态异步处理、日志采集、内容审核 04-圈子功能 / 10-内容审核
环信 IM 即时通讯 06-即时通信
OSS / FastDFS 图片、小视频存储 08-小视频方案
后台应用服务模块 运营侧管理 09-后台系统
Spark + MLlib 好友、圈子、视频推荐 11-推荐系统

一句话读法:本章定下骨架,后面每一章往骨架里填一块肉。先理解分层与 Dubbo 的调用关系,再看每个技术落在哪一层,后面的章节就不会是一堆散点。

常见误解澄清

"前后端分离就是前端用 Vue。" 不是。Vue 只是可选的前端框架,本项目移动端用的是 Flutter + Android。分离的本质是接口契约 + 独立部署:后端只返回 JSON,前端拿到数据自己渲染,两边能独立发版。

"域服务层可有可无,业务直接写在应用层更省事。" 单端、短期看是省事,但只要出现第二个调用方(后台、小程序、其他服务),业务就得复制一份。域服务层的价值就在"一份业务能力被多个端复用",前面的用户查询例子已经说明。

"一个项目只能用一个数据库。" 相反,多存储各司其职是常态。MySQL 管关系型业务、MongoDB 管文档型业务与地理位置、Redis 管缓存,本身是分工,不是重复建设。

"用了微服务就一定很复杂。" 本项目是"够用的微服务",不是大厂级全量方案:服务按数据特征拆成 db 与 mongo 两组,注册中心用 Nacos 一个组件,没上配置中心的全套灰度、也没上链路追踪全家桶。该拆的拆(应用层 / 域服务层),该合的合(mf-commons 一个工具模块)。架构的复杂度要跟业务规模匹配。

官方文档

总结

交友项目的架构可以浓缩成三句话:

  • 横向分两端:前端(App + 后台)只负责渲染,后端只提供 RESTful JSON 接口,两端靠契约协作。
  • 纵向分四层:接入层(Nginx/网关)→ 应用层(按端拆分、只做编排)→ 域服务层(Dubbo 提供者、承载可复用业务)→ 数据层(MySQL / MongoDB / Redis / 对象存储),中间件与外部服务横向提供能力。
  • 选型看特征:关系型数据进 MySQL,文档型与地理位置进 MongoDB,状态与热点进 Redis,异步与解耦进 RabbitMQ,文件进对象存储,IM 交给环信。每个选择都能说出"不用它会怎样"。

把分层和调用关系理顺之后,下一章就进入分离思想本身------后端到底该怎么组织接口,前端又怎么和后端并行开发,见同系列《前后端分离》。

相关推荐
我是大猴子1 小时前
一些八股和限流措施
java
QAQmmttyyy1 小时前
ChatFly 开发实录 ①|五天,从零到一个能卖的 Mac 工具
架构
认真的小羽❅1 小时前
ThreadLocal 在虚拟线程间要小心
java·开发语言·jvm
海宇数据1 小时前
零信任架构实战:基于海宇企业综合风险报告构建自动化供应商资质审查网关
运维·人工智能·架构·自动化
美味蛋炒饭.1 小时前
Java八股面试(4)
java·开发语言·面试
SL_staff1 小时前
OKR落地难?技术团队如何用五层联动链打通执行断层
java·开源·全栈
省钱兄--zs1 小时前
无人自助拼豆系统架构设计与功能实现落地部署全流程实战指南
java·数据仓库·spring boot·系统架构·需求分析
Raas1001 小时前
AI网关能做调用审计吗?MAI Gateway(魔芋企业级AI网关)实战能力深度解读
java·服务器·网络·数据库·gateway·prompt·企业级产品
骇客野人1 小时前
企业4A架构(业务架构、应用架构、数据架构、安全架构)完整设计与搭建方案
安全·架构·安全架构