Skip to content

三、核心技术点拆解(通俗解释)

这一章把后端的核心技术概念逐个拆开讲清楚。 每个概念都会用前端类比帮你快速理解。


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.envconfig.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) => { /* 处理 */ });                // 消费者

后端的消息队列多了什么?

特性EventBusRocketMQ
消息持久化❌ 内存里,刷新就没了✅ 存到磁盘,服务重启消息不丢
消费确认✅ 消费成功才删除,失败自动重试
负载均衡✅ 多个消费者自动分配
削峰填谷✅ 高峰时缓冲请求,匀速消费

在项目中的使用

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 加锁:只有一个能拿到锁去扣减,另一个等待
  → 防止超卖(和前端的防抖/节流思路相似,但更严格)