一、用微服务重构电商系统
很多中小团队做电商,起步阶段都是单体架构------一个 Go 服务包揽用户、商品、订单、支付所有逻辑,数据库也是单库单表。业务初期这样没问题,但随着流量增长,问题开始集中爆发:
- 多端数据割裂:H5 商城、管理后台、服务商端各自对接同一套接口,数据一致性和迭代效率越来越差。
- 高并发扛不住:秒杀场景下,单体应用的数据库连接池和内存直接打满,服务雪崩。
- 问题定位困难:一个请求跨了五六个服务调用,没有链路追踪,线上出了 Bug 只能靠猜。
- 安全防护缺失:SQL 注入、XSS 攻击、接口重放,生产环境裸奔。
我们团队经历了这个完整的痛苦过程,最终决定用 GoFrame + gRPC + ETCD 做微服务重构,把单体拆成独立的服务域,配合双网关、全链路追踪、分布式事务等企业级方案,支撑百万级并发。
这篇文章,就是我们整个重构过程的架构复盘。
二、整体架构设计
2.1 技术选型
| 模块 | 技术选型 | 选型理由 |
|---|---|---|
| 业务框架 | GoFrame v2.9+ | Go 生态最全的工程化框架,ORM/缓存/配置/日志开箱即用 |
| 通信协议 | gRPC + Protobuf | 服务间高性能通信,强类型接口定义,自动生成客户端代码 |
| 服务注册/发现 | ETCD | 轻量级分布式 KV,支持集群部署,Go 原生集成 |
| API 网关 | Kong | 成熟的开源网关,支持限流、鉴权、灰度发布等插件 |
| 配置中心 | Nacos | 支持多环境配置管理和热更新,与 GoFrame 生态集成方便 |
| 链路追踪 | Jaeger | CNCF 毕业项目,支持 gRPC/HTTP 全链路透传 |
| 搜索引擎 | Elasticsearch | 商品搜索多字段加权排序、高亮显示 |
| 消息队列 | RabbitMQ | 死信队列处理订单超时,消息可靠投递 |
| 监控告警 | Prometheus + Grafana | 自定义业务指标采集,实时告警 |
| 日志 | Loki | 轻量级日志聚合,与 Grafana 无缝集成 |
| 数据库中间件 | ShardingSphere-Proxy | 分库分表透明代理,业务代码零侵入 |
| 容器编排 | Kubernetes | HPA 自动扩缩容,灰度发布,云原生部署 |
2.2 双网关架构:为什么不是一个网关打天下
这是我们架构中最特别的设计之一。很多团队会只用一个网关统一承接所有流量,但在电商场景中,H5 商城和 Admin 管理台对网关的需求完全不同:
- H5 网关:面向公网用户,核心诉求是高并发限流、防刷、JWT 鉴权、灰度发布。
- Admin 网关:面向内部运营,核心诉求是细粒度 RBAC 权限控制、操作审计、IP 白名单。
两个网关共享底层的 Kong 实例,但各自配置独立的插件策略和路由规则。这样 H5 侧的限流策略不会影响管理台操作,管理台的权限变更也不会波及用户侧。
2.3 服务拆分策略
我们按照业务域拆分了以下核心服务:
sql
├── user-service 用户服务(注册/登录/OAuth2.0)
├── product-service 商品服务(SPU/SKU/ES搜索)
├── order-service 订单服务(下单/支付/超时取消)
├── payment-service 支付服务(微信/支付宝双通道)
├── inventory-service 库存服务(防超卖/分布式锁)
├── seckill-service 秒杀服务(独立部署,流量隔离)
└── admin-service 管理服务(RBAC/审计/运营工具)
每个服务独立数据库、独立部署、独立扩缩容。服务间通过 gRPC 同步调用,跨服务数据一致性通过分布式事务保障。
三、核心技术实现
3.1 秒杀系统:如何扛住峰值流量
秒杀是电商系统最极端的并发场景。我们采用三层削峰策略:
第一层:Kong 网关限流
在网关层配置令牌桶限流策略,将超出承载能力的请求直接拦截,避免打到后端服务。
nginx
# Kong 限流配置示意
rate-limiting:
plugin: rate-limiting
config:
minute: 1000
policy: redis
fault_tolerant: true
第二层:Redis Lua 原子扣库存
库存扣减是秒杀的核心,必须保证原子性。我们用 Redis + Lua 脚本实现,避免超卖:
lua
-- 秒杀扣库存 Lua 脚本(简化示意)
local stock = redis.call('GET', KEYS[1])
if tonumber(stock) <= 0 then
return -1
end
redis.call('DECR', KEYS[1])
return 1
第三层:Sentinel 熔断 + 令牌桶削峰
即使网关和 Redis 扛住了,下游服务也可能因为数据库压力过载而崩溃。我们引入 Sentinel 做服务级熔断,同时用令牌桶控制进入下单流程的请求速率,把瞬间流量摊平。
3.2 防超卖:我们试了 4 种方案
防超卖是电商的经典难题。我们实际对比了 4 种方案:
| 方案 | 优点 | 缺点 | 最终选择 |
|---|---|---|---|
| 数据库行锁(SELECT FOR UPDATE) | 实现简单 | 并发性能差,锁等待严重 | 否 |
| Redis 分布式锁(SETNX) | 性能好 | 锁续期复杂,存在死锁风险 | 否 |
| Redis Lua 原子操作 | 原子性好,性能高 | 需要保证 Redis 与 DB 最终一致 | 是(秒杀场景) |
| 数据库乐观锁(版本号) | 无锁竞争 | 高并发下 CAS 重试多,性能下降 | 是(常规下单场景) |
最终方案:秒杀场景用 Redis Lua 原子扣减,异步同步到数据库;常规下单用数据库乐观锁。两种场景各取所长。
3.3 分库分表:ShardingSphere-Proxy 实战
单库单表在订单量达到千万级后,查询性能断崖式下降。我们用 ShardingSphere-Proxy 做透明分库分表:
- 分片策略:按用户 ID 取模分库,按订单时间范围分表
- 分布式主键:Snowflake 算法生成全局唯一 ID
- 跨分片查询:通过 ShardingSphere 的合并引擎处理,业务代码零侵入
深度分页的坑:
分库分表后,LIMIT offset, size 的性能会随 offset 增大急剧恶化。ShardingSphere 需要将所有分片的 offset + size 条数据都拉到 Proxy 层做合并,当 offset=100000 时,实际扫描的数据量是 分片数 × 100010。
我们用两种方式解决:
- 游标翻页(Scroll API):用上一页最后一条记录的排序值作为游标,避免大 offset
- 禁止深翻页:业务层面限制最多翻 50 页,超过的引导用户缩小搜索范围
3.4 商品搜索:ES 多字段加权 + 高亮
商品搜索不能只靠数据库 LIKE,我们引入 Elasticsearch 做全文检索:
- 多字段加权:商品名称权重 10,分类权重 5,描述权重 2,保证名称匹配的排在前面
- 高亮显示:搜索关键词在结果中高亮标记
- 搜索建议:基于用户搜索历史的热门搜索词推荐
json
// ES 多字段加权查询(简化示意)
{
"query": {
"multi_match": {
"query": "手机壳",
"fields": ["name^10", "category^5", "description^2"],
"type": "best_fields"
}
},
"highlight": {
"fields": { "name": {} }
}
}
3.5 分布式事务:订单超时取消
下单后 30 分钟未支付需要自动取消订单并释放库存。我们用 RabbitMQ 死信队列实现:
- 下单成功后,消息投递到延迟队列(TTL=30min)
- 消息过期后进入死信队列
- 死信消费者执行取消逻辑:关订单 + 释放库存 + 退款
这比定时任务轮询更精准,也不会产生数据库压力。
3.6 全链路追踪:Jaeger 透传
微服务架构下,一个用户请求可能跨 5-6 个服务。没有链路追踪,排查线上问题就是噩梦。
我们通过 Jaeger 实现 gRPC/HTTP 全链路透传:
- 网关层生成 TraceID,通过 HTTP Header 传递
- gRPC 调用通过 Metadata 透传 TraceID
- 每个服务自动上报 Span 到 Jaeger Collector
- Grafana 集成 Jaeger 数据源,可以直接从告警跳转到链路详情
3.7 认证与安全体系
JWT 令牌轮换:
Access Token 短有效期(15min),Refresh Token 长有效期(7天)。Token 过期后用 Refresh Token 静默续期,用户无感知。Refresh Token 存入 Redis,支持主动失效(踢人下线)。
OAuth2.0 三方登录:
支持微信、支付宝快捷登录,授权码模式,State 参数防 CSRF。
防注入:
所有数据库操作通过 GoFrame ORM 预编译,参数化查询,杜绝 SQL 注入。前端输入做 XSS 过滤。
3.8 监控与运维
Prometheus + Grafana:
自定义业务指标采集(QPS、订单成功率、支付成功率、库存告警),Grafana 看板实时展示。
Loki 日志告警:
轻量级日志聚合,按 TraceID 关联日志和链路,慢调用自动告警。
K8s HPA 自动扩缩容:
秒杀期间,seckill-service 根据 CPU 使用率自动扩容到 10 副本,流量回落后自动缩容。
以上核心模块的完整源码已整理好,获取方式见文末。
四、踩坑与经验总结
1. 分库分表后 JOIN 查询几乎不可用
拆库之后,跨库 JOIN 直接报错。我们通过两种方式应对:冗余字段(把常用关联字段冗余到主表)和业务层组装(先查主表拿 ID 列表,再批量查从表)。前者牺牲存储换性能,后者牺牲性能换灵活,需要根据场景取舍。
2. 分布式锁的续期问题
Redis 分布式锁(SETNX)在高并发场景下,如果业务执行时间超过锁的 TTL,锁会自动释放,导致并发问题。我们引入了看门狗机制(Watchdog),后台协程定期续期,直到业务主动释放。
3. ES 与 MySQL 数据一致性
商品数据写入 MySQL 后需要同步到 ES,中间有延迟。我们用 Canal 监听 MySQL Binlog,异步写入 ES。但 Canal 也有延迟(通常 100ms-1s),对实时性要求极高的场景(如刚上架的商品必须立即可搜),需要额外做一次主动同步。
4. 秒杀服务必须独立部署
最初我们把秒杀逻辑放在 order-service 里,结果秒杀流量把整个订单服务打挂了,连带正常下单也受影响。后来把秒杀独立成 seckill-service,单独部署、单独限流、单独数据库,彻底隔离。
五、写在最后
从单体到微服务,这个重构过程远比想象中复杂。不只是技术选型和代码实现,更难的是服务边界的划分、分布式一致性的权衡、以及运维体系的搭建。
Go 在微服务领域的生态已经足够成熟------GoFrame、gRPC、ETCD、Kong、Jaeger,每一个组件都经过大厂验证。关键是把这些组件串起来,形成一套可落地的完整架构。
如果你对我们的项目感兴趣,或者想拿到完整源码和内部辅导,可以关注并私信我。包括全栈源码、部署手册、以及我们重构过程中积累的所有架构决策文档,省去你大量摸索的时间。
希望这篇文章能给正在做微服务重构的同学一些参考。如果有问题或者想法,欢迎评论区讨论。