外观
三、核心技术点拆解(通俗解释)
这一章把后端的核心技术概念逐个拆开讲清楚。 每个概念都会用前端类比帮你快速理解。
3.1 什么是微服务
一句话:把一个大应用拆成多个小应用,每个小应用独立部署、独立运行。
前端类比:就像你把一个巨大的 App.tsx 拆成多个独立的微前端应用(Module Federation / qiankun),每个应用可以独立开发和部署。
text
❌ 单体应用(Monolith)
┌──────────────────────────────┐
│ 用户模块 │ AI 模块 │ 文件模块 │ ← 全部打包在一起
│ 一个挂了,全挂 │
└──────────────────────────────┘
✅ 微服务(Microservice)
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 用户服务 │ │ AI 服务 │ │ 文件服务 │ ← 独立部署
│ 挂了不影响 │ 挂了不影响 │ 挂了不影响 │
└──────────┘ └──────────┘ └──────────┘好处:
- 一个服务崩溃不会拖垮其他服务
- 不同服务可以用不同技术栈
- 可以单独扩容(AI 服务访问量大就多部署几个实例)
坏处:
- 运维复杂度上升
- 服务间通信需要处理网络问题
- 数据一致性更难保证
3.2 服务注册与发现
概念:微服务需要知道"其他服务在哪里"(IP 和端口)。
有两种常见方案:
方案 A:注册中心(如 Nacos、Eureka)
text
服务启动 → 向注册中心报到:"我是 user-service,我在 192.168.1.10:8080"
其他服务 → 问注册中心:"user-service 在哪?" → "在 192.168.1.10:8080"方案 B:DNS 直连(Docker / K8s 环境常用)
text
服务直接通过容器名/服务名访问:http://user-service:8080
容器平台自动解析这个名字到对应 IP前端类比:就像你在 .env 里写 VITE_API_BASE=http://api.example.com,区别是后端的这个地址可以动态发现。
代码示例:Feign Client 声明
java
// 声明一个调用 AI 服务的 Feign 客户端
// url 通常从配置文件 / 环境变量注入
@FeignClient(name = "ai-service", url = "${service.ai.url}")
public interface AiFeignClient {
@PostMapping("/internal/image/generate")
RtData<GenerateResult> generateImage(@RequestBody GenerateRequest request);
}你只需要声明一个接口,Feign 就自动帮你发 HTTP 请求。不需要手写 axios.post()!
3.3 配置管理
概念:每个服务需要配置信息(数据库地址、端口号、密钥等),就像前端的 .env 文件。
配置来源(优先级从低到高)
text
1. application.yml ← 默认配置,打包在代码里
2. application-{env}.yml ← 按环境区分(dev/test/prod)
3. 环境变量 ← Docker/K8s 通过环境变量覆盖
4. 配置中心(如 Nacos) ← 运行时动态修改前端对照
| 后端配置方式 | 前端类比 |
|---|---|
application.yml | .env 或 config.ts |
@Value("${key}") 注解 | import.meta.env.VITE_KEY |
| 环境变量覆盖 | CI/CD 中注入的 VITE_* 环境变量 |
| 配置中心 | Feature Flag 服务(如 LaunchDarkly) |
代码示例
java
// 在代码中读取配置(类似前端的 process.env.XXX)
@Value("${app.upload.max-size:10485760}") // 默认 10MB
private long maxUploadSize;3.4 数据库连接与操作
项目通常使用两种数据库,各有分工:
MongoDB — 文档数据库(主数据库)
什么是 MongoDB?
MongoDB 存储的是 JSON 风格的"文档",非常灵活,不需要像 MySQL 一样提前定义严格的表结构。
前端类比:就像你把数据存到一个个 JSON 文件里。
javascript
// 前端眼中的 MongoDB 数据
{
"_id": "abc123",
"uid": 10001,
"login": "13800138000",
"userType": "mobile",
"createdAt": "2024-01-01T00:00:00Z"
}后端代码怎么写?
java
// 第一步:定义数据模型(类似 TypeScript interface)
@Document(collection = "user_mst") // 对应 MongoDB 的集合(类似表)
public class UserMst {
@Id
private String id; // 主键,MongoDB 自动生成
private Long uid; // 用户 ID
private String login; // 登录名(手机号/邮箱)
private String userType; // 用户类型
private LocalDateTime createdAt; // 创建时间
}
// 第二步:定义 Repository(类似前端封装的 API service)
public interface UserMstRepository extends MongoRepository<UserMst, String> {
// 方法名即查询!Spring Data 自动把方法名翻译成数据库查询
UserMst findByLoginAndUserType(String login, String userType);
// 等价于: db.user_mst.findOne({ login: "xxx", userType: "yyy" })
List<UserMst> findByUidIn(Collection<Long> uids);
// 等价于: db.user_mst.find({ uid: { $in: [...] } })
}
// 第三步:在 Service 中使用
@Service
public class UserServiceImpl {
@Autowired // ← 依赖注入,框架自动创建实例(类似 React 的 useContext)
private UserMstRepository userMstRepository;
public UserMst getUser(String phone) {
return userMstRepository.findByLoginAndUserType(phone, "mobile");
}
}💡 关键理解:你只需要定义接口方法名,Spring Data 会根据方法名自动生成查询逻辑。这叫"约定优于配置"。
MySQL — 关系型数据库(用于统计)
MySQL 适合结构化数据和复杂统计查询(JOIN、GROUP BY 等)。
java
// Entity 定义
@TableName("daily_statistics")
public class DailyStatistics {
@TableId(type = IdType.AUTO)
private Long id;
private String dateStr;
private Integer totalUsers;
private Integer newUsers;
}
// Mapper 接口(类似 Repository,但用的是 MyBatis-Plus 框架)
@Mapper
public interface DailyStatisticsMapper extends BaseMapper<DailyStatistics> {
// BaseMapper 自动提供:insert / update / delete / select 方法
// 复杂查询可以用 XML 写 SQL
}3.5 消息队列 RocketMQ
一句话:消息队列就是一个"消息中转站"。A 服务发一条消息到队列,B 服务从队列取出来处理。
前端类比
javascript
// 前端的 EventBus(发布订阅模式)
eventBus.emit('image:generate', { taskId: '123', prompt: '一只猫' }); // 生产者
eventBus.on('image:generate', (data) => { /* 处理 */ }); // 消费者后端的消息队列多了什么?
| 特性 | EventBus | RocketMQ |
|---|---|---|
| 消息持久化 | ❌ 内存里,刷新就没了 | ✅ 存到磁盘,服务重启消息不丢 |
| 消费确认 | ❌ | ✅ 消费成功才删除,失败自动重试 |
| 负载均衡 | ❌ | ✅ 多个消费者自动分配 |
| 削峰填谷 | ❌ | ✅ 高峰时缓冲请求,匀速消费 |
在项目中的使用
text
画布服务 RocketMQ AI 服务
│ │ │
├── 发送任务消息 ──────────────▶│ │
│ ├── 任务消息 ──────────────────▶│
│ │ ├── 调 AI API 生图
│ │◀───────────────── 结果消息 ───┤
│◀── 结果消息 ──────────────────│ │
├── 上传图片到 OSS │ │
├── 更新任务状态 │ │消费模式
- 推送式(Push):消息来了就自动推给消费者(结果回调常用这种)
- 拉取式(Pull):消费者主动去拉消息(AI 任务消费常用这种,可以控制消费速率)
3.6 Redis 缓存
一句话:Redis 是一个超快的内存数据库,读写速度是 MySQL 的 100 倍以上。
前端类比
| Redis 用途 | 前端类比 |
|---|---|
| 缓存热点数据 | localStorage.getItem('user') |
| 限流计数 | 局部变量记录点击次数 |
| 分布式锁 | 不让两个标签页同时提交 |
| Session 存储 | sessionStorage |
典型使用场景
text
场景 1:网关限流
→ 用户 A 1 秒内请求了 100 次
→ Redis: INCR rate:user:A → "当前 100 次" → 超限,返回 429
场景 2:配额缓存
→ 查用户剩余生图次数
→ 先查 Redis(快),没有再查 MongoDB(慢),然后写回 Redis
场景 3:分布式锁
→ 两个请求同时要扣减配额
→ Redis 加锁:只有一个能拿到锁去扣减,另一个等待
→ 防止超卖(和前端的防抖/节流思路相似,但更严格)