单体拆分微服务

2023-02-03T14:11:02+08:00 | 14分钟阅读 | 更新于 2026-02-03T14:11:02+08:00

@

学习目标

学完本章,你应该能够:

  1. 说清微服务架构的动机,并讲明单体应用、模块化单体、微服务三者的差异。
  2. 讲清 RPC、gRPC、Protobuf 的基本概念,能独立写出一个 gRPC 服务端与客户端。
  3. 解释 DDD 中的限界上下文、聚合体、Repository 等核心概念,并说明它们和微服务边界的关系。
  4. 复述单体应用拆分微服务的完整路线图:混沌一片 → 模块化 → 模块依赖化 → 微服务化。
  5. 设计一套上线灰度方案:本地调用与 gRPC 调用并行 + 流量阈值控制 + 回滚。

前置知识(如果下面任意一点生疏,先回看对应章):

  • 第02章 Gin + GORM:知道一个服务的 Web/Service/Repository 分层。
  • 第07章 Kafka:知道消息是怎么从生产者到消费者的(本章灰度切换会用到「配置中心动态下发」的思维)。
  • 基本的 Go 接口、泛型与依赖注入(wire)写法。
  • 一点 HTTP/gRPC 调试经验(如用 Postman 发请求)。

本章你会动手做的事

  • 写一个 interactive.proto,用 protoc 编译出 .pb.go_grpc.pb.go
  • 起一个 gRPC 服务端(监听某个端口),再用客户端连上它调一次 Like
  • 给你的客户端加一个「阈值 + 随机数」开关,把 5% 流量切到远程、其余走本地,体会灰度。

一、核心概念讲解

1.1 什么是微服务架构

微服务是一种架构风格,它把一个大型单体应用拆分为数个、数十个独立的服务,每个服务:

  • 可独立开发、测试、部署、扩容;
  • 自己拥有数据存储(理论上不应共享数据库);
  • 通过轻量通信机制(HTTP / RPC / MQ)交互;
  • 松耦合、自治、去中心化。

注意:架构有很多种,微服务只是一种。它不是银弹,而是"分而治之"思想在系统设计上的体现。

类比:单体应用像一个超大的厨房,洗菜、切菜、炒菜、装盘全在一个屋子里,厨师之间随时能喊话、随手拿对方的工具。微服务像是把厨房拆成「洗菜间」「切菜间」「炒菜间」几个独立小店,各干各的、通过传菜窗口(API)交接。好处是每个小店可以单独扩人手、单独关门维修;坏处是原来喊一嗓子就能办的事,现在要走流程窗口。

1.2 为什么要使用微服务——分而治之

直观解释:应用太复杂了,要想办法降低复杂度,降低到一个人能理解的地步。

假设两个系统的复杂度为 Sa 和 Sb,合并成一个系统后复杂度 S > Sa + Sb(多出来的部分来自模块间的相互交互)。拆分带来三方面好处:

  • 总体复杂度降低;
  • 单个模块复杂度变得可理解;
  • 模块间仅通过 API 耦合,无需了解其他模块的内部细节。
flowchart LR
    subgraph 单体[单体: 复杂度 S 很高]
        M1[模块A] --- M2[模块B]
        M2 --- M3[模块C]
        M1 --- M3
    end
    subgraph 微服务[微服务: 各自可理解]
        S1[服务A] -. API .- S2[服务B]
        S2 -. API .- S3[服务C]
    end

1.3 模块化 vs 微服务化(高频面试题)

模块化本身也是分而治之,那为什么不能只做模块化?答案是不行,原因有四点:

维度模块化单体微服务化
部署整个应用一起部署每个服务独立部署
资源整个应用需要的资源(如 16G 内存)单服务所需资源(如 4G)
演进强耦合,难以独立迭代在 API 向后兼容下可独立演进
组织一个团队维护一个庞大应用一个团队对应一组服务(康威定律)

运维视角的关键差异:实例才是最基本的运维单位。模块化的资源粒度是"整个单体",微服务的资源粒度是"单个服务"。

1.4 RPC、HTTP、RESTful、微服务——理清关系

很多面试者搞混这几个概念,要捋清楚:

  • RPC(Remote Procedure Call):远程过程调用,是一种通信协议,让你像调用本地方法一样调用远端方法。RPC 可以基于 TCP(如 Dubbo)、HTTP(如 gRPC)、UDP、甚至消息队列。
  • HTTP:一种应用层协议。可以用 HTTP 实现微服务通信,也可以在 HTTP 之上封装出 RPC 协议。
  • RESTful:一种软件架构风格,符合 REST 风格的 Web 应用叫 RESTful Web。它和微服务没有直接关系,只是可以把它作为微服务的通信机制。
  • 微服务:一种架构,对通信协议没有强制定义。

结论:微服务 ≠ 必须 RPC;可以 HTTP 通信,也可以 RPC 通信,还可以 MQ 通信。

基于 HTTP vs 基于 RPC 的微服务

方案优点缺点
基于 HTTP运维简单,组件少,对研发要求低,兼容异构系统性能略低
基于 RPC(如 gRPC)性能高,跨语言,生态完善运维复杂,对研发要求高

1.5 gRPC 与 Protobuf

gRPC 是 Google 开源的高性能 RPC 框架,“遇事不决选 gRPC"基本不会出错。

特点:

  • 高性能:基于 HTTP/2 双向流,支持流控和压缩;
  • 跨语言:主流语言都有实现,异构系统首选;
  • 开源:社区强大,生态丰富。

gRPC 使用 IDL(接口描述语言) 来定义客户端和服务端之间的通信格式。Protobuf 就是 gRPC 选用的 IDL 落地语言。

Protobuf 的优势

  • 高效:二进制格式,序列化/反序列化速度极快,压缩率高;
  • 跨平台语言无关:支持 C/C++/Java/Python/Go 等;
  • 扩展性强:可以灵活修改数据结构而不破坏兼容性;
  • 工具链完善:编译器、代码生成器、调试工具齐全。

1.6 DDD 核心概念(拆分微服务的理论标准)

DDD(Domain Driven Design,领域驱动设计)是微服务拆分的理论依据。核心概念:

概念说明
限界上下文 Bounded Context描述问题的上下文边界,对应微服务边界。同一个"产品"在销售和售后眼里特征不同。
实体 Entity有唯一标识符(ID),属性可变。如用户、订单。数据库的表不一定是实体。
值对象 Value Object没有唯一标识,由属性定义。如价格、金额。一个领域中的实体到另一个领域可能变成值对象。
聚合体 Aggregate实体 + N 个值对象的集合,实体是聚合根。聚合体之间只能通过 ID 引用。聚合根是修改聚合体的唯一入口。
工厂 Factory分离构造过程与业务逻辑。也可以用 Builder 替代。
仓库 Repository数据存储的抽象,屏蔽缓存/数据库差异,主要操作聚合体的增删改查。
领域事件 Domain Event系统中发生的事情,如订单状态变更。可发布到 MQ。
领域服务 Domain Service跨聚合体的业务逻辑。普通 CRUD 项目里通常没多少代码。

判定限界上下文的难点:有些业务确实处于黑白地带,放哪里都合适,需要业务理解。

flowchart TD
    BC[限界上下文 = 一个微服务] --> AG[聚合体]
    AG --> AR[聚合根 Entity]
    AG --> VO1[值对象]
    AG --> VO2[值对象]
    AR -. 只通过 ID 引用 .-> AR2[另一个聚合根]
    AR --> REP[(Repository 持久化)]

二、代码实战

2.1 Protobuf 定义示例

文件 webook/api/proto/interactive/v1/interactive.proto

// 声明使用 proto3 语法(除非维护老系统,否则都用 proto3)
syntax = "proto3";

// 生成 Go 代码时的包路径配置(必须包含 . 或 /)
option go_package = "gitee.com/curriculum/webook/api/proto/interactive/v1;interactivev1";

// 包名(可选,没什么实际作用)
package interactive.v1;

// 服务定义:相当于 Go 中的 interface
service InteractiveService {
  // 每个方法:一个请求 message,一个响应 message
  rpc Like(LikeRequest) returns (LikeResponse);
  rpc CancelLike(CancelLikeRequest) returns (CancelLikeResponse);
  rpc Collect(CollectRequest) returns (CollectResponse);
  rpc Get(GetRequest) returns (GetResponse);
}

// 点赞请求
message LikeRequest {
  // biz 标识业务类型,如 "article" 或 "comment"
  string biz = 1;
  int64 biz_id = 2;
  int64 uid = 3;
}

message LikeResponse {}

message CancelLikeRequest {
  string biz = 1;
  int64 biz_id = 2;
  int64 uid = 3;
}

message CancelLikeResponse {}

message CollectRequest {
  string biz = 1;
  int64 biz_id = 2;
  int64 uid = 3;
  int64 cid = 4; // 收藏夹 ID
}

message CollectResponse {}

// 互动数据
message Interactive {
  int64 biz_id = 1;
  int64 like_cnt = 2;
  int64 collect_cnt = 3;
  int64 view_cnt = 4;
  bool liked = 5;
  bool collected = 6;
}

message GetRequest {
  string biz = 1;
  int64 biz_id = 2;
  int64 uid = 3;
}

message GetResponse {
  Interactive intr = 1;
}

2.2 编译 Protobuf

直接用 protoc 命令:

# 安装 Go 和 gRPC 插件(一次性)
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
# 务必把 $GOPATH/bin 加入 PATH

# 编译
protoc \
  --go_out=. --go_opt=paths=source_relative \
  --go-grpc_out=. --go-grpc_opt=paths=source_relative \
  webook/api/proto/interactive/v1/interactive.proto

生成的产物:

  • interactive.pb.go:纯 Go 代码,主要是结构体定义;
  • interactive_grpc.pb.go:gRPC 服务端和客户端代码。

推荐使用 buf 工具替代 protoc,能解决插件难管理、命令难记、目录定位复杂等问题。

2.3 实现 gRPC 服务端

// webook/internal/interactive/grpc/server.go
package grpc

import (
    "context"
    "gitee.com/curriculum/webook/api/proto/interactive/v1/interactivev1"
    "gitee.com/curriculum/webook/internal/interactive/service"
)

// GRPCServer 是 gRPC 服务端实现
// 它组合了 protoc 生成的 UnimplementedInteractiveServiceServer
// 这是为了向前兼容:以后接口新增方法时不会编译失败
type GRPCServer struct {
    interactivev1.UnimplementedInteractiveServiceServer
    svc *service.InteractiveService
}

func NewGRPCServer(svc *service.InteractiveService) *GRPCServer {
    return &GRPCServer{svc: svc}
}

// Like 实现点赞接口
// 方法签名固定:context.Context + 请求 message + error 返回值
func (s *GRPCServer) Like(ctx context.Context, req *interactivev1.LikeRequest) (
    *interactivev1.LikeResponse, error) {
    // 步骤 1:从请求取出参数
    // 步骤 2:委托给领域服务处理业务逻辑
    err := s.svc.Like(ctx, req.GetBiz(), req.GetBizId(), req.GetUid())
    return &interactivev1.LikeResponse{}, err
}

func (s *GRPCServer) CancelLike(ctx context.Context, req *interactivev1.CancelLikeRequest) (
    *interactivev1.CancelLikeResponse, error) {
    err := s.svc.CancelLike(ctx, req.GetBiz(), req.GetBizId(), req.GetUid())
    return &interactivev1.CancelLikeResponse{}, err
}

func (s *GRPCServer) Collect(ctx context.Context, req *interactivev1.CollectRequest) (
    *interactivev1.CollectResponse, error) {
    err := s.svc.Collect(ctx, req.GetBiz(), req.GetBizId(), req.GetUid(), req.GetCid())
    return &interactivev1.CollectResponse{}, err
}

func (s *GRPCServer) Get(ctx context.Context, req *interactivev1.GetRequest) (
    *interactivev1.GetResponse, error) {
    intr, err := s.svc.Get(ctx, req.GetBiz(), req.GetBizId(), req.GetUid())
    if err != nil {
        return nil, err
    }
    return &interactivev1.GetResponse{
        Intr: &interactivev1.Interactive{
            BizId:       intr.BizId,
            LikeCnt:     intr.LikeCnt,
            CollectCnt:  intr.CollectCnt,
            ViewCnt:     intr.ViewCnt,
            Liked:       intr.Liked,
            Collected:   intr.Collected,
        },
    }, nil
}

⚠️ 新手必踩的坑:没嵌入 UnimplementedInteractiveServiceServer。protoc 生成的接口里有一组「未实现」的嵌入类型。如果你不把它嵌入到自己的 GRPCServer,将来 proto 里新增一个 RPC 方法,你的服务端结构体就满足不了新接口,编译直接失败。嵌入它,新增方法默认走「未实现」占位,编译照过——这就是向前兼容的关键。

2.4 启动 gRPC 服务端

// webook/internal/interactive/ioc/grpc.go
package ioc

import (
    "google.golang.org/grpc"
)

func InitGRPCServer(server *grpcinterative.GRPCServer) *grpc.Server {
    // 创建一个 gRPC Server
    s := grpc.NewServer()
    // 注册我们实现的 InteractiveServiceServer
    interactivev1.RegisterInteractiveServiceServer(s, server)
    return s
}
// webook/internal/interactive/main.go (节选)
package main

func main() {
    // 创建监听端口
    lis, err := net.Listen("tcp", ":8091")
    if err != nil {
        panic(err)
    }

    // 初始化依赖(用 wire 生成)
    app := wirex.NewApp()

    // 启动 gRPC Server
    server := app.GRPCServer
    log.Println("interactive gRPC listening on :8091")
    if err = server.Serve(lis); err != nil {
        panic(err)
    }
}

2.5 gRPC 客户端

// webook/internal/web/client/interactive.go
package client

import (
    "context"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
    "gitee.com/curriculum/webook/api/proto/interactive/v1/interactivev1"
)

type LocalInteractiveClientAdapter struct {
    // 适配本地 service,伪装成 gRPC 客户端
    // 微服务拆分完成后会删掉
    svc *service.InteractiveService
}

type InteractiveClient struct {
    remote interactivev1.InteractiveServiceClient // 真实 gRPC 客户端
    local  interactivev1.InteractiveServiceClient // 本地适配的 gRPC 客户端
    threshold int32 // 流量阈值,0~100,表示走远程调用的概率百分比
}

// Get 根据随机数 + 阈值来决定走本地还是远程
func (c *InteractiveClient) Get(ctx context.Context, biz string, bizId, uid int64) (
    *interactivev1.GetResponse, error) {
    randNum := rand.Int31n(100)
    if randNum < c.threshold {
        // 走 gRPC 远程调用
        return c.remote.Get(ctx, &interactivev1.GetRequest{
            Biz: biz, BizId: bizId, Uid: uid,
        })
    }
    // 走本地调用
    return c.local.Get(ctx, &interactivev1.GetRequest{
        Biz: biz, BizId: bizId, Uid: uid,
    })
}

2.6 客户端初始化 + 监听配置变更

// webook/internal/web/ioc/interactive.go
func InitInteractiveClient(client *grpc.ClientConn, configClient *config.Client) *client.InteractiveClient {
    // 真实 gRPC 客户端
    remote := interactivev1.NewInteractiveServiceClient(client)
    // 本地适配的 gRPC 客户端
    local := NewLocalAdapter(svc)

    res := &client.InteractiveClient{
        Remote: remote,
        Local:  local,
    }

    // 监听配置变更,实时调整阈值
    // 这是上线灰度的关键:通过配置中心动态调整流量比例
    configClient.Subscribe("interactive.threshold", func(val string) {
        threshold, _ := strconv.Atoi(val)
        res.UpdateThreshold(int32(threshold))
    })

    return res
}
sequenceDiagram
    participant C as Web 客户端
    participant T as 阈值开关
    participant R as 远程 gRPC
    participant L as 本地 Service
    C->>T: Get(biz,bizId,uid)
    alt randNum < threshold
        T->>R: 远程调用 interactive 服务
        R-->>C: 返回
    else 否则
        T->>L: 本地调用
        L-->>C: 返回
    end

⚠️ 新手必踩的坑:灰度开关只支持「随机」不支持「按业务 ID」。上面 rand.Int31n(100) 是纯随机,同一个 bizId 这次走远程、下次走本地,排查问题时要同时看两个地方。生产进阶做法是「阈值 + 业务 ID 哈希」:同一个 bizId 永远走同一条路径,出问题能稳定复现。


三、单体拆分微服务路线图

3.1 四个阶段

阶段关键动作
混沌一片完善单元测试;抽取公共 utils/helper;引入聚合层解除模块间循环依赖;按业务对象划分模块包
模块化创建不同代码仓库;准备微服务环境与框架选型;搭建 CI 和集成测试环境
模块依赖化业务模块逐个服务化;搭建自动部署和回滚平台;调研服务治理、网关、MQ、分布式事务、分布式任务调度、可观测性
服务化引入服务治理;引入网关;引入回归测试;按业务分库
flowchart LR
    S1[混沌一片] --> S2[模块化]
    S2 --> S3[模块依赖化]
    S3 --> S4[服务化]

3.2 拆分路线选型

有两条路线:

  1. 直接拆分某个模块为独立微服务:可提前验证全流程,但开弓没有回头箭。
  2. 全部模块按"模块化→模块依赖化→微服务化"分阶段推进:有后悔药,但无法提前验证。

本课程采用第一条路线,选择点赞/收藏模块(interactive)作为第一个拆分对象。

3.3 选择拆分模块的原则——先易后难

  • 优先选业务影响小的:崩了也没大影响;
  • 其次选最独立的:依赖少、被依赖少;
  • 最后考虑 QPS 低的;
  • 绝对不要选:用户、帖子、短信、热榜等核心服务。

重构心态:要考虑"出 BUG 怎么办”,而不是"怎么不出 BUG"。重构必然引入 BUG,再小心都会遇到。

3.4 详细迁移步骤

  1. 选定模块 A;
  2. 补充测试,覆盖率 ≥ 80%(业务覆盖比代码覆盖更重要);
  3. 代码拆分,命名为 webook-A
  4. 确定微服务框架技术选型;
  5. 将 A 改造为微服务,同时对外提供服务;
  6. 在原 webook 中同时使用本地调用和微服务调用,开关控制流量,允许回滚
  7. 逐步调整流量,直到 100% 走微服务;
  8. 移除原 webook 中的本地调用,纯依赖微服务。

3.5 模块化执行——重构点梳理

以 interactive 模块为例,重构前要先列出受影响的所有文件:

  • Web 层:article.go
  • Service 层:article.go、interactive.go
  • Domain 层:interactive.go
  • Repository 层:interactive.go
  • Cache 层:interactive.go
  • DAO 层:interactive.go、init.go
  • wire.go
  • ioc:db.go
  • events:article.go

最忌讳:没有分析重构点就直接动手,重构过程中才发现遗漏点,可能很难补救。

3.6 模块化步骤要点

  1. 把模块内的代码挪到独立目录(service/domain/repository 等);
  2. 解决数据库初始化(dao/init.go);
  3. 挪动 Kafka 消费者(注意:消息事件定义也要复制一份,不能再共享,否则模块化不彻底);
  4. 重新生成 wire 文件(main 启动 + 集成测试两处都要改);
  5. 运行集成测试验证。

模块化的目标:模块不再依赖 webook/internal 里的任何代码。

3.7 API 仓库管理方案

方案适用场景
统一 API 仓库小规模(≤30 人团队),所有微服务的 proto 放一处
各服务内部维护 API 目录中等规模
每个服务有实现仓库 + API 仓库巨型应用,团队组织合理

本课程采用方案一,目录结构:

webook/
├── api/
│   └── proto/
│       └── interactive/
│           └── v1/
│               ├── interactive.proto
│               └── gen/         # 编译产物
└── internal/

四、灰度上线方案(高频面试题)

4.1 为什么要"本地调用 + 微服务调用"并行

最大风险点在模块化和服务化这两个步骤。为避免出问题无法回滚:

  • 引入"同时使用本地调用和微服务调用"的中间状态;
  • 如果微服务调用出问题,立即把流量切回本地调用;
  • 没问题就逐步加大微服务流量。
flowchart TD
    Q[一次请求] --> T{randNum
小于阈值?} T -- 是 走远程 --> R[调用 interactive 微服务] T -- 否 走本地 --> L[调用本地 Service] R --> OK[返回] L --> OK R -- 出错且阈值>0 --> RB[调低阈值回滚到本地]

4.2 流量调度算法

最简单的方案:阈值 + 随机数

// 产生 0~99 的随机数
randNum := rand.Int31n(100)
if randNum < threshold {
    // 走远程 gRPC 调用
} else {
    // 走本地调用
}

进阶方案:阈值 + 业务 ID 哈希(保证同一业务数据走同一条路径,便于排查问题)。

4.3 流量调度完整流程

  1. 最开始阈值 = 0,所有流量走老路径;
  2. 调大阈值到 5%,放少量流量到新路径;
  3. 没问题就继续加大:10% → 30% → 50% → 100%;
  4. 有问题立即把阈值调回 0;
  5. 借助配置中心,动态修改阈值,立即同步到所有节点。

五、工程实践要点

  1. 重构必先补测试:覆盖率 ≥ 80%,且要覆盖核心业务场景和主要异常场景。代码全覆盖 ≠ 业务全覆盖。
  2. 小步前进:每完成一个关键步骤就跑测试,不要憋大招。
  3. 充分利用 IDE 重构功能:移包、改名、提取接口。
  4. wire 文件别漏:main 函数和集成测试各有自己的 wire 文件。
  5. API 设计向后兼容:gRPC 接口一旦上线就难以大改,新增字段用新编号,不要删除老字段。
  6. 配置中心是灰度方案的基础:阈值要能动态调整并实时生效。
  7. 重构是升职加薪机会:能加深对业务和架构的理解,重构成功的系统你就是专家,影响力扩大。

六、面试要点

本章涉及的常见面试题:

  • 什么是微服务架构?为什么使用微服务架构?
  • 模块化之后为什么要微服务化?
  • RESTful 和微服务架构是什么关系?
  • 可以用 HTTP 协议实现微服务架构吗?
  • 什么是 RPC?RPC 和 HTTP 是什么关系?
  • 什么是 DDD?介绍 DDD 的各个概念。
  • 微服务拆分怎么拆?具体步骤是什么?
  • 微服务拆分有哪些难点?
  • 怎么保证微服务拆分没有引入 BUG?(强调测试)
  • 怎么在线上做灰度方案?(阈值 + 随机数 / 业务哈希)

核心话术:在简历里写"主导微服务拆分",主动引导面试官问上述问题,强调"完善的测试可以显著减少引入 BUG 的可能性"。


自测题与动手练习

自测题(合上书能答出来,才算懂)

  1. 模块化单体已经「分而治之」了,为什么还非要微服务化?从「运维视角」说一个最关键差异。
  2. RPC、HTTP、RESTful、微服务四者是什么关系?微服务架构是否必须用 RPC?
  3. DDD 里「限界上下文」对应微服务的什么?聚合体之间为什么只能靠 ID 引用?
  4. 为什么 gRPC 服务端要嵌入 UnimplementedInteractiveServiceServer?不嵌入会怎样?
  5. 灰度上线为什么必须「本地调用 + 远程调用并行 + 阈值控制」?只做二选一(切过去就不管)有什么风险?

动手练习(建议真做一遍)

  1. 写 proto 并编译:按 2.1 写一份 interactive.proto,用 protoc 编译,确认生成 .pb.go_grpc.pb.go 两份文件。
  2. 起服务 + 调一次:用 2.3/2.4 起一个 gRPC 服务端监听 :8091,再用 2.5 的客户端连上它成功调一次 Like,观察返回。
  3. 加灰度开关:在客户端里加一个 threshold 字段,用 rand.Int31n(100) < threshold 决定走远程,先用 threshold=5 观察大多数请求仍走本地,再逐步调到 100 体会全量切换。

七、本章小结

本章内容是微服务工程师入门必修

  1. 微服务本质是"分而治之",目标是把复杂度降到一个人能理解的程度。
  2. 微服务 ≠ RPC,通信可以用 HTTP 也可以用 RPC;RESTful 只是风格,不是架构。
  3. gRPC + Protobuf 是当前 Go 生态最主流的微服务通信方案,“遇事不决选 gRPC”。
  4. DDD 是拆分微服务的理论标准,限界上下文 ≈ 微服务边界。
  5. 拆分四阶段:混沌一片 → 模块化 → 模块依赖化 → 服务化。
  6. 重构前先补测试,覆盖率 ≥ 80%;重构先易后难,从最独立的模块入手。
  7. 灰度上线方案:本地调用 + gRPC 调用并行 + 阈值控制流量 + 配置中心动态调整 + 随时可回滚。

下章将进入不停机数据迁移——微服务化之后必须把数据库也拆开,这是生产环境最高频的难题之一。

About Me

没什么想介绍的,一个很大众的码农…

喜欢代码,车,马,真的是 🐎

讨厌别人让我给自己的代码写注释 最厌烦别人的程序没有写注释

目标

学AI,加油!加油!