去年接了个项目,一个电商系统的核心链路改造。当时的情况很典型:单体应用扛不住大促流量,一到整点秒杀,订单服务超时、库存超卖、数据库连接池被打满,监控面板一片红。老板给的目标很明确——把系统拆成微服务,高峰期扛住平时10倍的流量,还不能丢单、不能超卖、不能把下游打挂。这半年踩了不少坑,也把微服务架构里最核心的几块硬骨头啃下来了:高并发场景下的分布式锁、消息队列、限流熔断、分布式事务。这篇文章就把这套实战经验完整拆开,从架构设计到具体实现,再到线上问题排查,希望能帮到正在做微服务改造、或者准备高并发面试的朋友。
1. 业务场景复盘:单体到微服务的拆分思路
1.1 单体应用为什么扛不住
老系统是典型的单体重应用,所有模块打在一个jar包里,部署在十几台机器上,前端用Nginx做负载均衡。表面上看起来没问题,但流量一上来,瓶颈非常明显:
- 所有请求都经过同一个应用,线程池被慢接口占满后,健康检查接口也会跟着超时
- 数据库连接是全局共享的,任何一个慢SQL都可能拖垮整个系统
- synchronized锁在集群环境下完全失效,库存扣减必须靠数据库乐观锁,性能极差
- 模块之间没有隔离,订单服务一抖动,支付、售后全部跟着遭殃
最痛的一次是库存扣减的并发问题。两个节点同时处理同一件商品的订单,各自都在内存里校验了库存充足,结果数据库更新时出现超卖。后来虽然加了version字段做乐观锁,但接口响应时间直接从20毫秒涨到了800毫秒,用户体验非常差。
1.2 微服务拆分的边界划分
这次改造没有一步到位,而是选了业务耦合最严重的交易链路先拆。拆分的原则就一条:按业务能力边界划分,保证每个服务有独立的数据库和独立的部署单元。
| 服务名称 | 核心职责 | 独立资源 |
|---|---|---|
| gateway | 统一入口、鉴权、路由转发 | 独立部署,无状态 |
| user-service | 用户信息、地址、积分 | user库 |
| order-service | 订单创建、订单状态流转 | order库 |
| stock-service | 库存扣减、库存回滚 | stock库 |
| payment-service | 支付流水、对账 | payment库 |
这里有个很关键的决策:订单和库存必须拆开,因为库存是超卖问题的核心,需要独立的伸缩能力。订单服务可以随便加实例,库存服务则需要保证数据一致性,不能盲目扩容。
1.3 技术选型组件清单
技术选型上,Spring Cloud Alibaba全家桶基本是当前国内微服务的主流答案,生态成熟、文档齐全,踩坑也容易搜到方案。
| 模块 | 选型 | 用途 |
|---|---|---|
| 注册配置中心 | Nacos | 服务注册发现、配置集中管理 |
| 远程调用 | OpenFeign + LoadBalancer | 服务间HTTP调用 |
| 分布式锁 | Redis + Redisson | 库存扣减、防重复提交 |
| 消息队列 | Redis Stream + RabbitMQ | 异步下单、削峰填谷 |
| 限流熔断 | Sentinel | 接口限流、服务熔断降级 |
| 对象存储 | MinIO | 商品图片、音视频文件存储 |
| 链路追踪 | SkyWalking | 全链路调用监控 |
这套组合覆盖了微服务治理的绝大部分场景。需要注意,不是说每个项目都要上全套,很多中小团队用Nacos + OpenFeign + Sentinel就够了,消息队列要看业务复杂度再决定。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 高并发下的分布式锁实战
2.1 单体锁在集群环境下失效的原因
没接触过分布式系统的同学可能觉得加锁很简单,直接synchronized或者ReentrantLock就完了。但在微服务架构里,所有服务都是多实例部署,比如order-service部署了5个节点,同一时刻可能有5个线程同时执行库存扣减的代码。JVM层面的锁只能锁住本机的线程,管不了其他节点上的线程,这就是分布式锁要解决的核心问题。
分布式锁的本质,是找一个所有节点都能访问的第三方存储,在这个存储上创建一个互斥标记。Redis因为速度快、支持原子操作,成了最主流的实现选择。
2.2 Redis分布式锁的自研演进过程
第一次自己写分布式锁,不推荐直接上Redisson,建议先把原理走一遍,后面排查问题才有底气。
最基础的版本是SETNX加过期时间:
java复制Boolean locked = redisTemplate.opsForValue()
.setIfAbsent("lock:stock:1001", "node-1", 30, TimeUnit.SECONDS);
if (locked) {
try {
// 执行业务逻辑
doDeductStock();
} finally {
redisTemplate.delete("lock:stock:1001");
}
}
这个版本有两个致命问题。第一,如果业务执行时间超过30秒,锁会自动过期,这时候另一个节点又拿到了锁,两个节点同时操作同一份数据,和没加锁一样。第二,删除锁的时候可能误删别人的锁——A线程的锁过期了,B线程拿到锁,A线程执行完finally里的delete,直接把B的锁删了。
第二个问题的解决方式是使用Lua脚本,比较value再删除,保证原子性:
lua复制if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
但锁自动过期的问题,自己写太难处理了。你需要一个后台任务不断给锁续期,还要考虑续期线程本身挂掉的情况。这其实就是Redisson看门狗机制做的事情,自己写十个有九个会在极端场景翻车。
2.3 Redisson的正确使用方式
实际项目中直接用Redisson封装好的锁,比自己造轮子要可靠得多。
xml复制<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.22.1</version>
</dependency>
java复制@Autowired
private RedissonClient redissonClient;
public boolean deductStock(Long skuId, Integer count) {
RLock lock = redissonClient.getLock("stock:" + skuId);
try {
// waitTime 2秒
