从单体到微服务:某电商平台200+服务拆分实战,从架构设计到运维落地的全流程复盘
我们是怎么踩进这个坑的
说实话,写这篇文章的时候我还在反思当年那个决定。
2019年,我们公司的电商平台单体应用已经跑到了6000+个接口,代码量超过800万行。那时候的技术债就像雪球,越滚越大——每次发布都要全量部署,一个订单服务的bug能把整个商城拖挂掉,测试覆盖率连30%都不到。
最致命的是业务增速太快,每次大促前都要预留两周做性能优化,但优化完的瓶颈没几天又被新需求打破。技术团队连续加班半年,人均发际线后移两厘米,问题还是解决不了。
老板看着竞品天天上线新功能,我们连个秒杀活动都要搞三个月,终于拍了桌子:拆!
第一步:评估与规划——别急着动手
很多团队拆微服务最大的问题就是急着动手,结果拆完发现比单体还难维护。
我们花了整整两个月做前期评估,主要看了四个维度:
1. 业务领域划分
直接用DDD(领域驱动设计)来梳理。我们开了三天工作坊,把核心业务拆成:
电商平台整体架构领域划分
├── 用户域(User Domain)
│ ├── 用户服务
│ ├── 会员等级服务
│ └── 用户认证授权服务
├── 商品域(Product Domain)
│ ├── 商品信息服务
│ ├── SKU管理服务
│ ├── 类目管理服务
│ └── 商品评价服务
├── 交易域(Trade Domain)
│ ├── 订单服务
│ ├── 购物车服务
│ ├── 促销服务
│ └── 秒杀服务
├── 支付域(Payment Domain)
│ ├── 支付网关服务
│ ├── 退款服务
│ └── 支付对账服务
├── 库存域(Inventory Domain)
│ ├── 库存管理服务
│ └── 库存锁定服务
├── 物流域(Logistics Domain)
│ ├── 物流跟踪服务
│ └── 仓储管理服务
└── 支撑域(Support Domain)
├── 消息通知服务
├── 搜索服务
└── 数据报表服务
这个划分的核心原则是:高内聚、低耦合。每个域内部的业务紧密相关,域与域之间通过明确定义的接口交互。
2. 系统现状评估
我们用了一个简单的矩阵来评估每个模块的拆分难度和收益:
| 模块 | 复杂度 | 耦合度 | 变更频率 | 拆分优先级 |
|---|---|---|---|---|
| 订单服务 | 高 | 高 | 高 | P0 |
| 商品服务 | 中 | 中 | 高 | P0 |
| 支付服务 | 高 | 高 | 中 | P0 |
| 用户服务 | 中 | 低 | 中 | P1 |
| 库存服务 | 中 | 高 | 高 | P1 |
3. 技术栈选型
这里我们踩了不少坑,最终确定的技术栈:
技术选型清单
├── 服务框架
│ ├── Java: Spring Cloud Alibaba (2022.x)
│ └── Go: 部分高并发服务使用Go
├── 注册中心
│ └── Nacos (同时承担配置中心角色)
├── 消息队列
│ ├── Kafka: 订单、支付等高吞吐场景
│ └── RocketMQ: 事务消息场景
├── 数据库
│ ├── MySQL: 主从架构,分库分表
│ ├── Redis: 缓存集群
│ └── MongoDB: 商品评论等非结构化数据
├── 链路追踪
│ └── SkyWalking
├── 日志系统
│ ├── ELK: 日志收集与分析
│ └── Loki: 轻量级日志方案
└── 容器化
├── Kubernetes: 容器编排
└── Docker: 容器打包
4. 迁移策略
我们选择了绞杀者模式(Strangler Fig Pattern),这是最稳妥的方案:
想象一棵绞杀榕树,它从寄主树外部开始生长,慢慢包裹并取代寄主。微服务迁移也是这样——在旧系统旁边逐步建立新系统,每次迁移一部分功能,直到旧系统完全被取代。
迁移阶段规划
阶段一(Month 1-3): 基础设施搭建
├── Kubernetes集群部署
├── CI/CD流水线建设
├── 服务注册发现搭建
└── 基础监控告警
阶段二(Month 4-6): 非核心服务迁移
├── 用户服务
├── 商品信息服务
├── 搜索服务
└── 消息通知服务
阶段三(Month 7-9): 核心交易链路迁移
├── 购物车服务
├── 订单服务
├── 库存服务
└── 支付服务
阶段四(Month 10-12): 收尾与优化
├── 流量切换
├── 旧系统下线
└── 性能调优
第二步:基础设施——地基不牢地动山摇
容器化部署
我们选择Kubernetes作为容器编排平台,原因在于它丰富的生态系统和对云原生应用的天然支持。
# 一个简单的订单服务Kubernetes部署配置
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
namespace: ecom-trade
labels:
app: order-service
version: v1.2.0
spec:
replicas: 3
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
version: v1.2.0
spec:
containers:
- name: order-service
image: registry.example.com/order-service:1.2.0
ports:
- containerPort: 8080
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "2000m"
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 20
periodSeconds: 5
env:
- name: SPRING_PROFILES_ACTIVE
value: "k8s"
- name: JAVA_OPTS
value: "-Xms512m -Xmx1024m"
---
apiVersion: v1
kind: Service
metadata:
name: order-service
namespace: ecom-trade
spec:
selector:
app: order-service
ports:
- port: 80
targetPort: 8080
type: ClusterIP
# Ingress配置 - 流量入口
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ecom-ingress
namespace: ecom-trade
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/rate-limit: "100"
spec:
ingressClassName: nginx
tls:
- hosts:
- www.example.com
secretName: ecom-tls
rules:
- host: www.example.com
http:
paths:
- path: /api/order
pathType: Prefix
backend:
service:
name: order-service
port:
number: 80
- path: /api/product
pathType: Prefix
backend:
service:
name: product-service
port:
number: 80
CI/CD流水线
流水线的设计我们参考了Google DORA的四大指标,目标是实现每天多次部署:
# Jenkinsfile - 订单服务流水线
pipeline {
agent none
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build') {
steps {
container('maven') {
sh 'mvn clean package -DskipTests'
}
}
}
stage('Security Scan') {
parallel {
stage('SAST') {
steps {
OWASP()
}
}
stage('Container Scan') {
steps {
trivy image --exit-code 0 --severity HIGH,CRITICAL \
registry.example.com/order-service:${BUILD_NUMBER}
}
}
}
}
stage('Test') {
steps {
container('maven') {
sh 'mvn test'
}
}
}
stage('Deploy to Staging') {
when {
branch 'develop'
}
steps {
script {
def imageTag = "${BUILD_NUMBER}"
sh """
docker build -t registry.example.com/order-service:${imageTag} .
docker push registry.example.com/order-service:${imageTag}
kubectl set image deployment/order-service \
order-service=registry.example.com/order-service:${imageTag} \
-n ecom-trade-staging
"""
}
}
}
stage('Smoke Test') {
when {
branch 'develop'
}
steps {
sh 'python tests/smoke_test.py --service order-service --env staging'
}
}
stage('Deploy to Production') {
when {
branch 'main'
}
input '确认部署到生产环境?'
steps {
script {
def imageTag = "${BUILD_NUMBER}"
sh """
kubectl set image deployment/order-service \
order-service=registry.example.com/order-service:${imageTag} \
-n ecom-trade
kubectl rollout status deployment/order-service \
-n ecom-trade --timeout=300s
"""
}
}
}
}
post {
always {
cleanWs()
}
success {
slackSend(channel: '#deployments',
message: "✅ 订单服务 ${env.BUILD_NUMBER} 部署成功")
}
failure {
slackSend(channel: '#deployments',
message: "❌ 订单服务 ${env.BUILD_NUMBER} 部署失败")
}
}
}
第三步:服务拆分——最艰难的抉择
服务拆分原则
这里要说一个很多人容易犯的错误:不要为了微服务而微服务。
我们总结出了几个拆分原则:
服务拆分决策树
┌─────────────┐
│ 开始评估 │
└──────┬──────┘
│
┌────────────┼────────────┐
│ │ │
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│变化频率高│ │变化频率低│ │变化频率低│
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
│ ┌────────┴────────┐ │
│ │ 业务边界清晰? │ │
│ └────────┬────────┘ │
│ │ │
┌────┴────┐ ┌────┴────┐ ┌────┴────┐
│ 拆分 │ │ 拆分 │ │ 不拆分 │
│(高收益) │ │(高收益) │ │(低风险) │
└─────────┘ └─────────┘ └─────────┘
具体来说:
- 业务边界清晰:订单服务处理订单生命周期,商品服务处理商品信息,两者业务边界明确
- 独立部署:订单服务上线不影响商品服务
- 独立扩展:秒杀期间可以单独扩容订单服务
- 数据隔离:每个服务有自己的数据库,不共享表
服务拆分详细清单
我们的200+服务最终分成以下几大类:
服务拆分清单(节选核心服务)
┌─────────────────────────────────────────────────────────────────┐
│ 用户域 (8个服务) │
├─────────────────────────────────────────────────────────────────┤
│ 1. 用户中心服务 - user-service - 用户基础信息管理 │
│ 2. 认证授权服务 - auth-service - JWT令牌、OAuth2.0 │
│ 3. 会员等级服务 - member-service - 会员体系、积分 │
│ 4. 地址管理服务 - address-service - 收货地址管理 │
│ 5. 客服系统 - cs-service - 在线客服 │
│ 6. 用户画像服务 - userprofile-service - 用户标签、推荐 │
│ 7. 隐私合规服务 - privacy-service - GDPR、数据脱敏 │
│ 8. 用户行为服务 - userbehavior-service - 点击流、行为分析 │
├─────────────────────────────────────────────────────────────────┤
│ 商品域 (15个服务) │
├─────────────────────────────────────────────────────────────────┤
│ 1. 商品信息服务 - product-service - 商品详情、规格 │
│ 2. SKU管理服务 - sku-service - SKU管理 │
│ 3. 类目管理服务 - category-service - 类目体系 │
│ 4. 商品评价服务 - review-service - 评价、晒单 │
│ 5. 商品图片服务 - image-service - 图片处理、CDN │
│ 6. 商品搜索服务 - search-service - Elasticsearch │
│ 7. 商品推荐服务 - recommendation-service - 个性化推荐 │
│ 8. 商品定价服务 - pricing-service - 价格计算、优惠 │
│ 9. 商品库存查询 - product-inventory - 库存查询 │
│ 10. 商品品牌服务 - brand-service - 品牌管理 │
│ 11. 商品规格服务 - specification-service - 规格参数 │
│ 12. 商品审核服务 - audit-service - 内容审核 │
│ 13. 商品下架服务 - delist-service - 下架管理 │
│ 14. 商品拼团服务 - groupbuy-service - 拼团活动 │
│ 15. 商品预售服务 - presale-service - 预售管理 │
├─────────────────────────────────────────────────────────────────┤
│ 交易域 (25个服务) │
├─────────────────────────────────────────────────────────────────┤
│ 1. 订单服务 - order-service - 订单创建、查询 │
│ 2. 购物车服务 - cart-service - 购物车管理 │
│ 3. 促销服务 - promotion-service - 优惠活动 │
│ 4. 秒杀服务 - flashsale-service - 秒杀活动 │
│ 5. 预售服务 - presale-service - 预售订单 │
│ 6. 团购服务 - grouppurchase-service - 团购订单 │
│ 7. 优惠券服务 - coupon-service - 优惠券发放、使用 │
│ 8. 积分服务 - points-service - 积分兑换 │
│ 9. 钱包服务 - wallet-service - 用户余额 │
│ 10. 售后服务中心 - aftersales-service - 退货、换货 │
│ ... (还有15个交易相关服务) │
├─────────────────────────────────────────────────────────────────┤
│ 支付域 (12个服务) │
├─────────────────────────────────────────────────────────────────┤
│ 1. 支付网关 - payment-gateway - 统一支付入口 │
│ 2. 微信支付服务 - wxpay-service - 微信支付对接 │
│ 3. 支付宝服务 - alipay-service - 支付宝对接 │
│ 4. 银联服务 - unionpay-service - 银联支付 │
│ 5. 苹果支付服务 - appstorepay-service - App Store支付 │
│ 6. 退款服务 - refund-service - 退款处理 │
│ 7. 支付对账服务 - reconciliation-service - 对账 │
│ 8. 风控服务 - risk-control-service - 支付风控 │
│ 9. 发票服务 - invoice-service - 电子发票 │
│ 10. 钱包充值服务 - wallet-recharge-service - 充值 │
│ 11. 分期服务 - installment-service - 分期付款 │
│ 12. 支付回调服务 - payment-callback-service - 支付结果回调 │
├─────────────────────────────────────────────────────────────────┤
│ 库存域 (10个服务) │
├─────────────────────────────────────────────────────────────────┤
│ 1. 库存服务 - inventory-service - 库存管理 │
│ 2. 库存锁定服务 - lock-service - 库存预占 │
│ 3. 仓储管理服务 - warehouse-service - 仓库管理 │
│ 4. 库存调拨服务 - transfer-service - 库存调拨 │
│ 5. 库存盘点服务 - stocktake-service - 库存盘点 │
│ 6. 供应商服务 - supplier-service - 供应商管理 │
│ 7. 采购服务 - purchase-service - 采购订单 │
│ 8. 库存预警服务 - alert-service - 库存预警 │
│ 9. 智能补货服务 - autoreplenish-service - 智能补货建议 │
│ 10. 库存同步服务 - sync-service - 多渠道库存同步 │
└─────────────────────────────────────────────────────────────────┘
接口设计——契约先行
微服务拆分最大的挑战之一是接口设计。我们采用了契约优先(Contract-First) 的设计方法:
// 订单服务接口定义 - 使用OpenAPI 3.0规范
// order-service/src/main/resources/openapi.yaml
openapi: 3.0.3
info:
title: 订单服务 API
description: 电商平台订单核心接口
version: 1.0.0
contact:
name: 电商平台技术团队
paths:
/api/v1/orders:
post:
summary: 创建订单
operationId: createOrder
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/OrderCreateRequest'
responses:
'200':
description: 创建成功
content:
application/json:
schema:
$ref: '#/components/schemas/OrderCreateResponse'
'400':
description: 参数错误
'429':
description: 请求过于频繁
/api/v1/orders/{orderId}:
get:
summary: 查询订单详情
operationId: getOrder
parameters:
- name: orderId
in: path
required: true
schema:
type: string
format: uuid
responses:
'200':
description: 查询成功
content:
application/json:
schema:
$ref: '#/components/schemas/OrderDetailResponse'
'404':
description: 订单不存在
components:
schemas:
OrderCreateRequest:
type: object
required: [userId, items]
properties:
userId:
type: string
format: uuid
description: 用户ID
items:
type: array
minItems: 1
items:
$ref: '#/components/schemas/OrderItem'
addressId:
type: string
format: uuid
description: 收货地址ID
couponId:
type: string
description: 优惠券ID
remark:
type: string
maxLength: 200
description: 订单备注
OrderItem:
type: object
required: [skuId, quantity]
properties:
skuId:
type: string
format: uuid
quantity:
type: integer
minimum: 1
maximum: 999
OrderCreateResponse:
type: object
properties:
orderId:
type: string
format: uuid
orderNo:
type: string
description: 订单号
totalAmount:
type: number
format: double
status:
type: string
enum: [PENDING_PAYMENT, PAID, SHIPPED, COMPLETED, CANCELLED]
// Java端使用Spring Cloud Contract进行契约测试
// 消费者驱动契约(CDC)测试
@SpringBootTest
@AutoConfigureStubRunner(stubsMode = StubsMode.LOCAL)
class OrderServiceConsumerTest {
@Autowired
private OrderClient orderClient;
@StubRunnerId(8888)
void shouldCreateOrderSuccessfully() {
// 基于契约定义的测试
OrderCreateRequest request = OrderCreateRequest.builder()
.userId("550e8400-e29b-41d4-a716-446655440000")
.items(List.of(
OrderItem.builder()
.skuId("6ba7b810-9dad-11d1-80b4-00c04fd430c8")
.quantity(2)
.build()
))
.addressId("6ba7b812-9dad-11d1-80b4-00c04fd430c8")
.build();
OrderCreateResponse response = orderClient.createOrder(request);
assertThat(response).isNotNull();
assertThat(response.getOrderId()).isNotNull();
assertThat(response.getStatus()).isEqualTo(OrderStatus.PENDING_PAYMENT);
}
}
第四步:数据拆分——最痛苦的环节
数据拆分是我们遇到最大困难的地方,没有之一。
数据库拆分策略
单体数据库的表结构经过多年积累,耦合严重。我们的拆分策略:
数据库拆分策略
单体数据库 (MySQL)
│
├── 用户相关表 → 用户数据库
│ ├── user_info
│ ├── user_profile
│ └── user_address
│
├── 商品相关表 → 商品数据库
│ ├── product_info
│ ├── product_sku
│ └── product_category
│
├── 订单相关表 → 订单数据库
│ ├── order_master
│ ├── order_item
│ └── order_log
│
└── 支付相关表 → 支付数据库
├── payment_order
└── payment_record
每个数据库独立,有独立的账号权限,独立的备份策略
分库分表方案
对于订单这种数据量特别大的表,我们使用了ShardingSphere进行分库分表:
# ShardingSphere配置 - 订单库分片策略
spring:
shardingsphere:
datasource:
names: ds0,ds1
ds0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://ds0:3306/order_db_0
username: ${DB_USER}
password: ${DB_PASS}
ds1:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://ds1:3306/order_db_1
username: ${DB_USER}
password: ${DB_PASS}
rules:
sharding:
tables:
order_master:
actual-data-nodes: ds$->{0..1}.order_master_$->{0..3}
table-strategy:
standard:
sharding-column: user_id
sharding-algorithm-name: user-id-sharding
key-generate-strategy:
column: order_id
key-generator-name: snowflake
sharding-algorithms:
user-id-sharding:
type: INLINE
props:
algorithm-expression: order_master_$->{user_id % 4}
key-generators:
snowflake:
type: SNOWFLAKE
props:
worker-id: 12
props:
sql-show: false
// 自定义分片算法
@Component
public class HybridShardingAlgorithm implements ShardingAlgorithm<Long> {
private static final int DATABASE_COUNT = 2;
private static final int TABLE_COUNT_PER_DB = 4;
private static final int TOTAL_TABLES = DATABASE_COUNT * TABLE_COUNT_PER_DB;
@Override
public String doSharding(Collection<String> availableTargetNames,
ShardingValue<Long> shardingValue) {
Long userId = shardingValue.getValue();
// 先按用户ID分库
int dbIndex = (int) (userId % DATABASE_COUNT);
// 再按订单创建时间分表(月度分表)
LocalDate now = LocalDate.now();
int tableIndex = (now.getYear() * 12 + now.getMonthValue()) % TABLE_COUNT_PER_DB;
String tableName = "order_master_" + (dbIndex * TABLE_COUNT_PER_DB + tableIndex);
return Collections.singleton(tableName);
}
}
数据迁移方案
迁移过程中我们采用了双写+校验的方案:
# 数据迁移脚本(Python示例)
import pymysql
from datetime import datetime
import logging
logger = logging.getLogger(__name__)
class DataMigrationTool:
def __init__(self, source_config, target_config):
self.source = pymysql.connect(**source_config)
self.target = pymysql.connect(**target_config)
def migrate_orders(self, batch_size=10000):
"""
订单数据迁移
策略:全量迁移 + 增量同步
"""
source_cursor = self.source.cursor()
target_cursor = self.target.cursor()
# 全量迁移
last_id = 0
total_count = 0
while True:
# 分批查询
source_cursor.execute("""
SELECT order_id, user_id, order_no, total_amount,
status, create_time, update_time
FROM order_master
WHERE order_id > %s
ORDER BY order_id
LIMIT %s
""", (last_id, batch_size))
rows = source_cursor.fetchall()
if not rows:
break
# 批量插入目标库
for row in rows:
target_cursor.execute("""
INSERT INTO order_master
(order_id, user_id, order_no, total_amount,
status, create_time, update_time)
VALUES (%s, %s, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE
total_amount = VALUES(total_amount),
status = VALUES(status),
update_time = VALUES(update_time)
""", row)
total_count += 1
last_id = rows[-1][0]
if total_count % 100000 == 0:
logger.info(f"已迁移 {total_count} 条记录,当前ID: {last_id}")
self.target.commit()
logger.info(f"全量迁移完成,共 {total_count} 条记录")
return total_count
def start_incremental_sync(self):
"""
增量同步启动
使用Canal监听MySQL binlog,同步到新库
"""
from canal.client import Client
from canal.protocol import EntryProtocol
client = Client()
client.connect('canal-server', 11111)
client.subscribe('ecom_order')
while True:
entries = client.get(100)
for entry in entries:
if entry.entry_type == EntryProtocol.EntryType.TRANSACTIONBEGIN:
continue
if entry.entry_type == EntryProtocol.EntryType.TRANSACTIONEND:
continue
# 解析binlog事件
row_changes = entry.entry_type.rowchange
table_name = row_changes.table_name
for row in row_changes.row_data:
if row_changes.change_type == EntryProtocol.RowChange.Type.INSERT:
self._insert_record(table_name, row.after_columns)
elif row_changes.change_type == EntryProtocol.RowChange.Type.UPDATE:
self._update_record(table_name, row.after_columns)
elif row_changes.change_type == EntryProtocol.RowChange.Type.DELETE:
self._delete_record(table_name, row.before_columns)
client.rollback()
第五步:服务治理——让200+服务有序协作
服务注册与发现
我们使用Nacos作为注册中心和配置中心,因为它同时支持AP和CP模式,而且国内社区活跃。
# Nacos配置中心 - 订单服务配置
# nacos-config/order-service.yaml
spring:
datasource:
url: jdbc:mysql://order-db:3306/order_db?useSSL=false&serverTimezone=Asia/Shanghai
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
cloud:
nacos:
discovery:
server-addr: ${NACOS_ADDR}
config:
server-addr: ${NACOS_ADDR}
file-extension: yaml
# 业务配置
order:
max-items-per-order: 100
auto-cancel-time: 30 # 未支付订单自动取消时间(分钟)
timeout: 5000 # 订单服务超时时间(ms)
# 限流配置
order:
rate-limit:
create-order: 100 # 每秒创建订单上限
query-order: 500 # 每秒查询订单上限
# 降级配置
order:
circuit-breaker:
failure-rate-threshold: 50 # 失败率阈值
wait-time-in-ms: 30000 # 熔断等待时间
sliding-window-size: 10 # 滑动窗口大小
熔断与限流
// 订单服务熔断配置
@Configuration
public class OrderCircuitBreakerConfig {
@Bean
public Resilience4jConfigCustomizer resilience4jConfigCustomizer() {
return config -> {
// 创建订单熔断器
config.forId("createOrder")
.circuitBreakerConfig(CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.waitDurationInOpenState(Duration.ofSeconds(30))
.slidingWindowSize(10)
.slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
.slowCallRateThreshold(80)
.build())
.rateLimiterConfig(RateLimiterConfig.custom()
.timeoutDuration(Duration.ofSeconds(3))
.limitForPeriod(100)
.limitRefreshPeriod(Duration.ofSeconds(1))
.build());
};
}
}
// 使用注解实现熔断和限流
@Service
public class OrderService {
private final OrderMapper orderMapper;
private final ProductFeignClient productClient;
private final InventoryFeignClient inventoryClient;
@CircuitBreaker(name = "createOrder", fallbackMethod = "createOrderFallback")
@RateLimiter(name = "createOrder")
public OrderVO createOrder(OrderCreateRequest request) {
// 1. 校验商品信息
List<ProductItemVO> items = productClient.batchGetProducts(request.getItemIds());
// 2. 校验库存
boolean stockEnough = inventoryClient.checkStock(request.getItemIds());
if (!stockEnough) {
throw new BusinessException("库存不足");
}
// 3. 计算订单金额
BigDecimal totalAmount = calculateAmount(items, request.getCouponId());
// 4. 创建订单
Order order = new Order();
order.setOrderNo(generateOrderNo());
order.setUserId(request.getUserId());
order.setTotalAmount(totalAmount);
order.setStatus(OrderStatus.PENDING_PAYMENT);
order.setCreateTime(LocalDateTime.now());
orderMapper.insert(order);
// 5. 创建订单明细
for (ProductItemVO item : items) {
OrderItem orderItem = new OrderItem();
orderItem.setOrderId(order.getOrderId());
orderItem.setSkuId(item.getSkuId());
orderItem.setQuantity(item.getQuantity());
orderItem.setPrice(item.getPrice());
orderMapper.insertOrderItem(orderItem);
}
return convertToVO(order);
}
// 熔断降级方法
public OrderVO createOrderFallback(OrderCreateRequest request, Exception e) {
log.error("订单创建服务熔断,请求: {}", request, e);
// 降级:返回一个延迟创建的订单
Order order = new Order();
order.setOrderNo(generateOrderNo());
order.setStatus(OrderStatus.PENDING_REVIEW);
order.setTotalAmount(BigDecimal.ZERO);
// 发送异步创建任务
orderAsyncService.submitCreateTask(request);
return convertToVO(order);
}
}
API网关设计
// 网关配置 - Spring Cloud Gateway
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
// 订单服务路由
.route("order-service", r -> r
.path("/api/order/**")
.filters(f -> f
.stripPrefix(1)
.circuitBreaker(c -> c
.setName("orderCircuitBreaker")
.setFallbackUri("forward:/fallback/order"))
.requestRateLimiter(r -> r
.setRateLimiter(redisRateLimiter())
.setKeyResolver(ipKeyResolver()))
.addResponseHeader("X-Service-Name", "order-service")
)
.uri("lb://order-service"))
// 商品服务路由
.route("product-service", r -> r
.path("/api/product/**")
.filters(f -> f
.stripPrefix(1)
.requestRateLimiter(r -> r
.setRateLimiter(redisRateLimiter())
.setKeyResolver(ipKeyResolver()))
)
.uri("lb://product-service"))
// 支付服务路由
.route("payment-service", r -> r
.path("/api/payment/**")
.filters(f -> f
.stripPrefix(1)
.circuitBreaker(c -> c
.setName("paymentCircuitBreaker")
.setFallbackUri("forward:/fallback/payment"))
)
.uri("lb://payment-service"))
// 秒杀服务路由(独立限流)
.route("flashsale-service", r -> r
.path("/api/flashsale/**")
.filters(f -> f
.stripPrefix(1)
.requestRateLimiter(r -> r
.setRateLimiter(flashsaleRateLimiter())
.setKeyResolver(ipKeyResolver()))
)
.uri("lb://flashsale-service"))
.build();
}
@Bean
public RedisRateLimiter redisRateLimiter() {
return new RedisRateLimiter(100, 200, 1);
}
@Bean
public RedisRateLimiter flashsaleRateLimiter() {
return new RedisRateLimiter(50, 100, 1); // 秒杀接口更严格的限流
}
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> {
String ip = exchange.getRequest().getHeaders()
.getFirst("X-Forwarded-For");
return Optional.ofNullable(ip)
.orElse(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
};
}
}
// 全局过滤器 - 统一鉴权与日志
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getURI().getPath();
// 白名单路径跳过鉴权
if (isWhitelist(path)) {
return chain.filter(exchange);
}
// 获取Token
String token = exchange.getRequest().getHeaders()
.getFirst("Authorization");
if (StringUtils.isBlank(token)) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
byte[] bytes = "{\"code\":401,\"msg\":\"未登录\"}".getBytes(StandardCharsets.UTF_8);
return response.writeWith(Mono.fromSupplier(() ->
new DefaultDataBufferFactory().wrap(bytes)));
}
// 验证Token
String userId = verifyToken(token);
if (userId == null) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.writeWith(Mono.fromSupplier(() ->
new DefaultDataBufferFactory().wrap("{\"code\":401,\"msg\":\"Token无效\"}".getBytes())));
}
// 将用户信息传递到下游服务
ServerHttpRequest request = exchange.getRequest()
.mutate()
.header("X-User-Id", userId)
.header("X-Request-Id", UUID.randomUUID().toString())
.build();
return chain.filter(exchange.mutate().request(request).build());
}
private boolean isWhitelist(String path) {
return path.startsWith("/api/auth/")
|| path.startsWith("/api/product/category")
|| path.startsWith("/api/health");
}
private String verifyToken(String token) {
String userId = redisTemplate.opsForValue().get("token:" + token);
return userId;
}
@Override
public int getOrder() {
return -100;
}
}
第六步:可观测性——让系统透明可见
链路追踪
// SkyWalking链路追踪配置
@Configuration
public class SkyWalkingConfig {
@Bean
public TracingInterceptor tracingInterceptor() {
return new TracingInterceptor();
}
}
// 自定义Trace注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TraceMethod {
String value() default "";
}
// 使用示例
@Service
public class OrderService {
@TraceMethod("创建订单")
public OrderVO createOrder(OrderCreateRequest request) {
// 业务逻辑
return order;
}
@TraceMethod("支付回调处理")
public void handlePaymentCallback(PaymentCallbackEvent event) {
// 支付回调处理
}
}
// 链路追踪信息传递 - 跨服务传播
@Component
public class TraceContextInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
// 从请求头获取Trace ID
String traceId = request.getHeader("X-Trace-Id");
String spanId = request.getHeader("X-Span-Id");
if (StringUtils.isBlank(traceId)) {
traceId = UUID.randomUUID().toString().replace("-", "");
}
// 设置到ThreadLocal
TraceContext.set(traceId, spanId);
// 传递到响应头
response.setHeader("X-Trace-Id", traceId);
response.setHeader("X-Span-Id", UUID.randomUUID().toString().replace("-", ""));
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
TraceContext.clear();
}
}
日志规范
# 日志配置 - logback-spring.xml
<configuration>
<!-- 日志输出格式 -->
<property name="LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] [%level] [%logger{36}] - %msg%n"/>
<!-- 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<!-- 文件输出 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/order-service/order.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>/var/log/order-service/order.%d{yyyy-MM-dd}.log.gz</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<!-- 业务日志 -->
<appender name="BUSINESS_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/order-service/business.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>/var/log/order-service/business.%d{yyyy-MM-dd}.log.gz</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
<encoder>
<pattern>{"timestamp":"%d{yyyy-MM-dd HH:mm:ss.SSS}","traceId":"%X{traceId}","level":"%level","logger":"%logger","msg":"%msg"}</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
<logger name="business" level="INFO" additivity="false">
<appender-ref ref="BUSINESS_LOG"/>
</logger>
</configuration>
// 结构化业务日志
@Slf4j
@Service
public class OrderService {
public OrderVO createOrder(OrderCreateRequest request) {
// 业务操作日志
log.info("business | 创建订单 | userId={} | items={} | amount={}",
request.getUserId(),
request.getItems().size(),
request.getTotalAmount());
// 异常日志
try {
// 业务逻辑...
} catch (Exception e) {
log.error("business | 创建订单失败 | userId={} | error={}",
request.getUserId(),
e.getMessage(), e);
throw e;
}
}
}
监控告警
# Prometheus监控配置
# prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'order-service'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['order-service:8080']
labels:
service: 'order-service'
env: 'production'
- job_name: 'product-service'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['product-service:8080']
labels:
service: 'product-service'
env: 'production'
- job_name: 'payment-service'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['payment-service:8080']
labels:
service: 'payment-service'
env: 'production'
# Grafana告警规则
# alert_rules.yml
groups:
- name: order-service-alerts
rules:
- alert: OrderServiceHighErrorRate
expr: rate(http_server_requests_seconds_count{status=~"5.."}[5m]) /
rate(http_server_requests_seconds_count[5m]) > 0.05
for: 5m
labels:
severity: critical
annotations:
summary: "订单服务错误率过高"
description: "订单服务错误率为 {{ $value | humanizePercentage }},超过5%阈值"
- alert: OrderServiceHighLatency
expr: histogram_quantile(0.95,
rate(http_server_requests_seconds_bucket[5m])) > 2
for: 5m
labels:
severity: warning
annotations:
summary: "订单服务P95延迟过高"
description: "订单服务P95延迟为 {{ $value }}s,超过2s阈值"
- alert: OrderServiceCircuitBreakerOpen
expr: resilience4j_circuitbreaker_state{service_name="order-service"} == 1
for: 1m
labels:
severity: critical
annotations:
summary: "订单服务熔断器打开"
description: "订单服务熔断器处于打开状态"
第七步:性能优化——实战中的数据
缓存策略
// Redis缓存配置
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.serializeValuesWith(
RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues();
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
}
}
// 缓存使用示例 - 商品信息缓存
@Service
public class ProductCacheService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String PRODUCT_KEY_PREFIX = "product:";
private static final String PRODUCT_SKU_KEY_PREFIX = "product:sku:";
/**
* 获取商品信息,带缓存
*/
public ProductVO getProduct(Long productId) {
String cacheKey = PRODUCT_KEY_PREFIX + productId;
// 1. 先查缓存
ProductVO cached = (ProductVO) redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
return cached;
}
// 2. 缓存未命中,查数据库
Product product = productMapper.selectById(productId);
if (product == null) {
throw new BusinessException("商品不存在");
}
// 3. 写入缓存,TTL 10分钟
ProductVO vo = convertToVO(product);
redisTemplate.opsForValue().set(cacheKey, vo, 10, TimeUnit.MINUTES);
// 4. 异步预热SKU缓存
warmUpSkuCache(productId);
return vo;
}
/**
* 缓存预热 - SKU信息
*/
private void warmUpSkuCache(Long productId) {
List<Sku> skus = skuMapper.selectByProductId(productId);
for (Sku sku : skus) {
String skuKey = PRODUCT_SKU_KEY_PREFIX + sku.getSkuId();
redisTemplate.opsForValue().set(skuKey, sku, 30, TimeUnit.MINUTES);
}
}
/**
* 缓存更新
*/
public void updateProduct(Long productId) {
String cacheKey = PRODUCT_KEY_PREFIX + productId;
redisTemplate.delete(cacheKey);
// 异步删除SKU缓存
List<Sku> skus = skuMapper.selectByProductId(productId);
for (Sku sku : skus) {
redisTemplate.delete(PRODUCT_SKU_KEY_PREFIX + sku.getSkuId());
}
}
/**
* 缓存击穿防护 - 互斥锁
*/
@Cacheable(value = "product", key = "#productId")
public ProductVO getProductWithLock(Long productId) {
String lockKey = "product:lock:" + productId;
try {
// 尝试获取分布式锁
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (locked != null && locked) {
// 获取到锁,查数据库
Product product = productMapper.selectById(productId);
return convertToVO(product);
} else {
// 未获取到锁,等待后重试
Thread.sleep(100);
return getProductWithLock(productId);
}
} catch (Exception e) {
throw new BusinessException("查询商品失败", e);
} finally {
redisTemplate.delete(lockKey);
}
}
}
异步化处理
// 异步消息处理 - 使用RocketMQ
@Configuration
public class RocketMQConfig {
@Bean
public RocketMQTemplate rocketMQTemplate(RocketMQConnectionFactory connectionFactory) {
return new RocketMQTemplate(connectionFactory);
}
}
@Service
public class OrderMessageService {
@Autowired
private RocketMQTemplate rocketMQTemplate;
/**
* 发送订单创建消息
*/
public void sendOrderCreatedMessage(Long orderId) {
OrderMessage message = new OrderMessage();
message.setOrderId(orderId);
message.setEventType("ORDER_CREATED");
message.setTimestamp(System.currentTimeMillis());
// 发送异步消息
rocketMQTemplate.syncSend("order-topic", MessageBuilder
.withPayload(message)
.setHeader("ORDER_ID", orderId)
.build());
}
/**
* 发送订单超时取消消息(延迟消息)
*/
public void sendOrderTimeoutMessage(Long orderId, int delaySeconds) {
rocketMQTemplate.syncSendDelayed("order-timeout-topic",
MessageBuilder.withPayload(orderId).build(),
delaySeconds * 1000L);
}
}
// 消息消费者
@Component
@RocketMQMessageListener(
topic = "order-topic",
consumerGroup = "order-consumer-group"
)
public class OrderMessageConsumer implements RocketMQListener<OrderMessage> {
@Autowired
private InventoryService inventoryService;
@Autowired
private NotificationService notificationService;
@Override
public void onMessage(OrderMessage message) {
try {
switch (message.getEventType()) {
case "ORDER_CREATED":
// 扣减库存
inventoryService.deductStock(message.getOrderId());
// 发送通知
notificationService.sendOrderCreatedNotification(message.getOrderId());
break;
case "ORDER_PAID":
// 确认库存扣减
inventoryService.confirmDeductStock(message.getOrderId());
break;
case "ORDER_CANCELLED":
// 释放库存
inventoryService.releaseStock(message.getOrderId());
break;
}
} catch (Exception e) {
log.error("处理订单消息失败: {}", message, e);
// 发送到死信队列
throw e;
}
}
}
第八步:团队组织——技术之外的关键
架构重构不仅是技术活,更是组织变革。
团队结构调整
拆分前后团队对比
拆分前(功能团队):
┌─────────────────────────────────────┐
│ 后端开发组(30人) │
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │用户 │ │商品 │ │订单 │ │支付 │ │
│ │模块 │ │模块 │ │模块 │ │模块 │ │
│ └──────┘ └──────┘ └──────┘ └──────┘ │
│ 共享代码库,统一发布 │
└─────────────────────────────────────┘
拆分后(特性团队):
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ 用户团队 │ │商品团队 │ │交易团队 │ │支付团队 │
│ (5人) │ │ (6人) │ │ (8人) │ │ (5人) │
│ 全栈能力 │ │ 全栈能力 │ │ 全栈能力 │ │ 全栈能力 │
│ 独立迭代 │ │ 独立迭代 │ │ 独立迭代 │ │ 独立迭代 │
└────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │ │
└────────────┴────────────┴────────────┘
│
┌─────────┴─────────┐
│ 平台支撑团队 │
│ (基础设施/运维) │
└───────────────────┘
协作机制
跨团队协作流程
1. 接口契约
├── 服务提供方定义OpenAPI规范
├── 服务消费方基于契约进行开发
└── 契约测试确保双方对齐
2. 版本管理
├── API版本控制:/api/v1/, /api/v2/
├── 向前兼容:新接口不破坏旧接口
└── 废弃通知:废弃接口提前3个月通知
3. 沟通机制
├── 周会:各团队同步进展
├── 架构评审:重大变更需要评审
└── 故障复盘:每次故障后复盘改进
我们踩过的坑
坑1:分布式事务的诱惑
刚开始我们想用Seata做分布式事务,结果测试环境一压测,性能直接崩了。
// 反面教材 - 不推荐的分布式事务
@GlobalTransactional
public void createOrderWithTransaction(OrderCreateRequest request) {
// 创建订单
Long orderId = orderService.createOrder(request);
// 扣减库存(跨服务事务)
inventoryService.deductStock(orderId, request.getItems());
// 创建支付单(跨服务事务)
paymentService.createPayment(orderId, request.getTotalAmount());
}
后来我们改成了最终一致性方案:
// 正面教材 - 基于消息队列的最终一致性
@Service
public class OrderCreateService {
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Transactional
public OrderVO createOrder(OrderCreateRequest request) {
// 1. 创建订单(本地事务)
Order order = orderMapper.insertAndGetOrder(request);
// 2. 发送准备消息(本地事务内保证消息发送)
rocketMQTemplate.syncSend("order-prepared-topic",
new OrderPreparedMessage(order.getOrderId()));
return convertToVO(order);
}
}
@Component
@RocketMQMessageListener(topic = "order-prepared-topic",
consumerGroup = "order-handler")
public class OrderHandler implements RocketMQListener<OrderPreparedMessage> {
@Override
public void onMessage(OrderPreparedMessage message) {
Long orderId = message.getOrderId();
// 3. 扣减库存(异步,失败重试)
inventoryService.deductStockAsync(orderId);
// 4. 创建支付单(异步,失败重试)
paymentService.createPaymentAsync(orderId);
}
}
坑2:服务间调用链路过长
某个订单创建请求,调用链如下:
用户请求 → API网关 → 订单服务 → 商品服务 → 库存服务 → 促销服务
↓
用户服务(获取用户信息)
↓
优惠券服务(获取可用优惠)
一个请求经过7个服务,延迟高达800ms。
解决方案是接口聚合:
// 创建订单聚合接口
@RestController
@RequestMapping("/api/order")
public class OrderController {
@Autowired
private OrderAggregateService orderAggregateService;
@PostMapping("/create")
public Result<OrderVO> createOrder(@RequestBody OrderCreateRequest request) {
// 聚合服务内部编排多个子服务调用
OrderVO order = orderAggregateService.createOrder(request);
return Result.success(order);
}
}
@Service
public class OrderAggregateService {
@Autowired
private OrderService orderService;
@Autowired
private ProductBatchQueryService productBatchQueryService; // 批量查询
@Autowired
private InventoryCheckService inventoryCheckService; // 批量校验
@Autowired
private PromotionCalculateService promotionCalculateService; // 促销计算
public OrderVO createOrder(OrderCreateRequest request) {
// 1. 批量查询商品信息
List<ProductVO> products = productBatchQueryService.batchGetProducts(request.getItemIds());
// 2. 批量校验库存
List<StockCheckResult> stockResults = inventoryCheckService.batchCheckStock(request.getItemIds());
// 3. 计算优惠
PromotionResult promotionResult = promotionCalculateService.calculatePromotion(
request.getUserId(), request.getItemIds(), request.getCouponId());
// 4. 创建订单
return orderService.createOrder(request, products, stockResults, promotionResult);
}
}
坑3:数据一致性难题
商品库存和订单库存不一致的问题,我们花了很长时间解决:
// 库存预扣 + 确认机制
@Service
public class InventoryService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 库存预扣(下单时调用)
*/
public boolean preDeductStock(Long skuId, int quantity) {
String key = "stock:pre:" + skuId;
Long count = redisTemplate.opsForValue().increment(key, quantity);
if (count > 0) {
// 预扣成功,设置过期时间(订单超时取消时自动释放)
redisTemplate.expire(key, 30, TimeUnit.MINUTES);
return true;
} else {
// 预扣失败,回滚
redisTemplate.opsForValue().decrement(key, quantity);
return false;
}
}
/**
* 确认扣减(支付成功后调用)
*/
public void confirmDeduct(Long skuId, int quantity) {
// 从Redis预扣转移到DB正式扣减
String preKey = "stock:pre:" + skuId;
Long preCount = redisTemplate.opsForValue().get(preKey);
if (preCount != null && preCount >= quantity) {
// DB扣减
inventoryMapper.deductStock(skuId, quantity);
// 删除预扣记录
redisTemplate.delete(preKey);
}
}
/**
* 释放预扣(订单取消时调用)
*/
public void releasePreDeduct(Long skuId, int quantity) {
String key = "stock:pre:" + skuId;
redisTemplate.opsForValue().decrement(key, quantity);
redisTemplate.delete(key);
}
}
最终效果
拆分完成半年后,我们统计了一些关键指标:
迁移前后对比
迁移前 迁移后
───── ─────
发布时间 2周/次 每天多次
平均发布成功率 75% 98%
平均接口响应时间 350ms 120ms
系统可用性 99.5% 99.95%
故障恢复时间 30分钟 5分钟
团队交付效率 1x 3x
更重要的是团队的士气明显提升了——以前每次上线都像打仗,现在各团队可以独立迭代,互不影响。
给想做的团队几点建议
- 不要追求一步到位:我们花了12个月才完成,不要指望3个月搞定
- 基础设施先行:K8s、CI/CD、监控这些先搭好,不然后面会非常痛苦
- 契约先行:服务间接口一定要先定契约,否则后期改接口能改死人
- 组织要同步调整:技术架构和团队架构要匹配,不然会有新的墙
- 要有耐心:微服务不是银弹,它解决的是复杂度和交付速度的问题,但会引入新的复杂度
这篇复盘就到这里。如果你正在考虑做类似的迁移,欢迎交流——每个坑都是踩过的,希望能帮你少踩几个。
