去年接了个项目,一个电商系统的核心链路改造。当时的情况很典型:单体应用扛不住大促流量,一到整点秒杀,订单服务超时、库存超卖、数据库连接池被打满,监控面板一片红。老板给的目标很明确——把系统拆成微服务,高峰期扛住平时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秒,leaseTime -1走看门狗自动续期
boolean locked = lock.tryLock(2, -1, TimeUnit.SECONDS);
if (!locked) {
return false;
}
// 业务操作
return stockService.deduct(skuId, count);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
这里解释几个参数:
waitTime:获取锁的等待时间,设置2秒意味着拿不到锁时最多阻塞2秒,避免线程无限等待leaseTime:设置-1时启用看门狗,默认每10秒续期一次,保证业务没执行完锁不会自动过期isHeldByCurrentThread():解锁前判断当前线程是否持有锁,防止误删
2.4 分布式锁实战中的几个大坑
锁超时导致的业务重复。有次运营搞促销,有个商品被秒杀接口连续扣减了两次库存。排查后发现,问题出在锁的持有时间超过了过期时间,第二个请求在第一个请求释放前就拿到了新锁。虽然Redisson看门狗会续期,但如果业务里有外部HTTP调用,比如通知第三方仓储系统,网络超时时间设得很长,就可能超过Redis的锁最大持有时间。最后方案是给外部调用单独设置超时,并且把重试逻辑改成异步补偿。
Redis主从切换导致锁丢失。默认模式下Redis主节点写入锁成功后,异步复制到从节点。如果主节点宕机,从节点升级为主节点,锁数据还没同步过去,其他线程就能拿到锁。生产环境一定要用Redisson的RedLock或者引入Redis主从强同步方案。但RedLock本身也有争议,实际业务如果允许极少概率的重复,可以在数据库层加唯一约束兜底。
库存扣减的最终防线应该放在数据库。分布式锁能挡住99.9%的并发问题,但分布式系统里没有任何机制能保证100%不出现重复。所以库存表里一定要加version字段,或者用update stock set count = count - #{n} where count >= #{n}这样的条件更新兜底,保证极端情况下也不会超卖。
2.5 分布式锁面试题高频内容速览
这块也是面试重灾区,把常见的几个问题整理一下,供大家参考:
- 为什么synchronized不能用?JVM锁只能锁单机,多实例部署后锁不互斥
- 分布式锁需要满足哪些特性?互斥性、可重入、死锁规避(过期时间)、高可用(主从/集群)
- 为什么SETNX要配合Lua?因为检查value和删除是两个操作,非原子会导致误删锁
- 看门狗原理是什么?Redisson默认给锁续期,每
lockWatchdogTimeout/3时间间隔执行续期,默认锁超时时间30秒 - 如果Redis是集群模式,锁的可靠性如何保证?可以用RedLock红锁,多个独立Redis节点同时加锁,超过半数成功才算加锁成功
3. 消息队列:三大作用与落地实现
3.1 消息队列到底解决了什么问题
很多新手把消息队列当成一个简单的“中间层”,觉得就是把一个接口调用改成往队列里发数据。其实消息队列在微服务架构里有三个不可替代的作用,面试和架构设计都会反复考。
| 作用 | 说明 | 典型场景 |
|---|---|---|
| 异步化 | 把不需要立即返回结果的操作放到后台执行 | 下单后发短信、送优惠券 |
| 流量削峰 | 瞬时高流量先写入队列,消费者按能力消费 | 秒杀、整点抢购 |
| 系统解耦 | 生产者不依赖消费者的可用性 | 订单系统对接仓储、物流、财务 |
以秒杀下单为例:用户点击下单后,如果同步调用库存、优惠券、积分、短信、物流等5个服务,接口耗时可能超过800毫秒。改成消息队列后,只同步扣减库存(用分布式锁保证),后续操作全部异步化,用户体验是下单立即成功,接口耗时控制在100毫秒内。
3.2 为什么选择Redis Stream
我们消息队列的选型上纠结了一段时间。最初用的是RabbitMQ,功能全面、延迟队列支持好。但后来有个业务场景需要轻量级的消息通道,不想再引入一套中间件,正好Redis本身已经有Stream数据结构,就试着用它做了一套消息通道。
Redis Stream从5.0版本开始支持,有点像Kafka的简化版,有消费者组、消息ACK、Pending List机制,足够覆盖大部分异步场景。本身依赖的Redis组件已经是基础设施,不需要额外运维。
如果项目里已经有RabbitMQ或者Kafka,不建议纯粹为了新技术切到Redis Stream。我当时选择它的原因是团队运维成本低、代码接入简单,同时消息量级不高,不需要Kafka那种高吞吐能力。
3.3 Spring Boot拉取Redis Stream消息的完整落地
Redis Stream最核心的是消息的可靠消费机制。消费者从Stream里读取消息后,消息会进入Pending List,消费者处理完成后发送XACK,消息才会被确认删除。如果消费者崩溃,没有ACK的消息会一直躺在Pending List里,另一个消费者读取时通过XAUTOCLAIM进行消息转移,实现至少一次投递。
我封装了一套基于Spring Boot的Redis Stream消费组件:
java复制@Component
public class OrderCreateConsumer {
private static final String STREAM_KEY = "stream:order:created";
private static final String CONSUMER_GROUP = "group:order-service";
@PostConstruct
public void init() {
// 创建消费者组,如果已存在则忽略异常
try {
redisTemplate.opsForStream()
.createGroup(STREAM_KEY, CONSUMER_GROUP);
} catch (Exception e) {
// 组已存在,忽略
}
// 启动消费线程
new Thread(this::consume, "order-stream-consumer").start();
}
public void consume() {
while (true) {
try {
List<MapRecord<String, Object, Object>> records = redisTemplate
.opsForStream()
.read(Consumer.from(CONSUMER_GROUP, "consumer-1"),
StreamReadOptions.empty().count(10).block(Duration.ofSeconds(3)),
StreamOffset.create(STREAM_KEY, ReadOffset.lastConsumed()));
for (MapRecord<String, Object, Object> record : records) {
String recordId = record.getId().getValue();
try {
OrderMessage message = objectMapper.convertValue(record.getValue(), OrderMessage.class);
handleOrderCreated(message);
// 确认消费成功
redisTemplate.opsForStream().acknowledge(STREAM_KEY, CONSUMER_GROUP, record.getId());
} catch (Exception e) {
// 处理失败的消息不要ACK,让Pending List保留
log.error("处理订单消息失败, recordId={}", recordId, e);
// 消息转移到死信队列,避免阻塞正常消费
redisTemplate.opsForStream().add("stream:order:dead",
Map.of("recordId", recordId, "reason", e.getMessage()));
}
}
} catch (Exception e) {
log.error("拉取Redis Stream消息异常", e);
try {
Thread.sleep(1000);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
break;
}
}
}
}
}
有几个细节说一下:
ReadOffset.lastConsumed()表示从当前消费者组消费的最新位置开始读取,而不是从头读block(Duration.ofSeconds(3))让消费者阻塞等待新消息,避免空轮询打满CPU- 处理失败的消息不能ACK,否则就丢了;记录到死信队列,方便之后手工排查
- 消费者线程用
while(true)循环,但必须处理异常,否则线程一挂就没消息消费了
3.4 消息重复消费问题怎么处理
Redis Stream和RabbitMQ默认都是至少一次投递,消费者处理成功后,ACK消息如果因为网络原因丢失,消息会再次被投递。所以业务侧必须要做幂等处理。
幂等方案的核心是唯一业务标识。比如订单创建消息里带上orderId,消费者处理前先查表判断是否已经处理过:
java复制public void handleOrderCreated(OrderMessage message) {
String orderId = message.getOrderId();
// 通过唯一键插入,如果冲突说明已经处理过
try {
eventDao.insert(EventRecord.builder()
.businessId(orderId)
.eventType("ORDER_CREATED")
.status(1)
.build());
} catch (DuplicateKeyException e) {
log.info("重复消费消息, orderId={}", orderId);
return;
}
// 执行真正的业务逻辑
doBusiness(message);
}
这里的关键是businessId字段必须在数据库上有唯一索引。两个线程同时插入时,只有一个能成功,另一个会收到DuplicateKeyException,这样就算消息队列重复投递,也不会导致重复发优惠券或重复通知仓储。
3.5 延迟消息队列的两种实现
业务里经常有“超时未支付自动取消订单”“下单10分钟后发提醒”之类的需求,这就要用到延迟消息。我们项目中同时用到了两种方案:
RabbitMQ死信队列实现延迟消息。给消息设置TTL,消息过期后没有被消费,就会转发到死信交换机,再路由到对应的延迟消费队列。这个方案成熟可靠,缺点是延迟时间不精确,消息可能早于或晚于设定时间到达。
Redisson延迟队列实现定时任务。实际项目中更适合基于时间轮的延迟队列,Redisson封装了RDelayedQueue:
java复制RBlockingQueue<OrderMessage> blockingQueue = redissonClient.getBlockingQueue("delay:order:timeout");
RDelayedQueue<OrderMessage> delayedQueue = redissonClient.getDelayedQueue(blockingQueue);
// 发送延迟消息,5分钟后生效
delayedQueue.offer(orderMessage, 5, TimeUnit.MINUTES);
// 消费者线程从阻塞队列里取,到时间才能取到
new Thread(() -> {
while (true) {
try {
OrderMessage message = blockingQueue.take();
cancelTimeoutOrder(message);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}).start();
这个方案简单直接,适合延迟时间跨度长、不需要精确到秒的场景。我们用它处理了未支付订单超时关闭功能,效果不错。
4. 限流熔断:系统自我保护的双保险
4.1 服务雪崩是怎么发生的
微服务链路长,任何一个下游服务出问题都可能拖垮整个系统。最典型的场景:订单服务依赖库存服务,库存服务又依赖数据库,数据库突然变慢,库存服务的线程全部阻塞等待,订单服务发过来的大量请求也在等待响应,线程池被占满,订单服务也开始超时。然后依赖订单服务的服务也跟着出问题,像多米诺骨牌一样全部倒下,这就是服务雪崩。
限流是保护自己,熔断是保护上游,降级是保护用户体验。这三件事要配合起来做。
4.2 限流算法对比与选型
面试常问限流算法,但实际项目中真正需要手写算法的场景不多,市面上的框架都帮你实现了。不过理解原理有助于正确配置参数。
| 算法 | 核心思想 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 固定窗口 | 每单位时间窗口内允许固定请求数 | 实现简单 | 窗口边界流量突刺,比如前59秒没请求,最后1秒来1000个 | 一般接口粗粒度限流 |
| 滑动窗口 | 把时间窗口细分成多格,滑动统计 | 解决固定窗口突刺 | 内存占用稍高 | 需要平稳流量的场景 |
| 漏桶 | 请求先进桶,以恒定速率流出 | 流量绝对平滑 | 无法应对突发流量,瞬间大流量直接丢弃 | 保护下游系统 |
| 令牌桶 | 以恒定速率生成令牌,请求需拿令牌 | 支持一定突发流量 | 突发流量可能打爆下游 | 大多数业务接口 |
我们实际用的最多的是令牌桶,Sentinel默认就支持。原因很简单:业务系统要有一定的突发处理能力,完全平滑的漏桶会让正常促销流量被误杀。
4.3 Sentinel接入与核心配置
Sentinel是阿里开源的高可用防护组件,相比Hystrix,最大的优势是细粒度的实时统计和丰富的流控模式。我集成到Spring Cloud Alibaba项目里的方式如下:
xml复制<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
yaml复制spring:
application:
name: order-service
cloud:
sentinel:
transport:
port: 8719
dashboard: localhost:8080
datasource:
- nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
dataId: order-service-sentinel-rules
groupId: DEFAULT_GROUP
rule-type: flow
我习惯把限流规则放在Nacos配置中心,这样调整限流阈值不用重启服务。在代码里通过注解方式配置限流点:
java复制@SentinelResource(value = "createOrder",
blockHandler = "createOrderBlockHandler",
fallback = "createOrderFallback")
public OrderVO createOrder(OrderCreateRequest request) {
// 核心下单逻辑
}
blockHandler是限流或熔断触发后的处理方法,fallback是业务异常时的兜底方法。注意两者参数定义有区别,blockHandler的最后一个参数必须是BlockException,fallback的参数需要和原方法保持一致。
4.4 熔断策略的三种触发模式
Sentinel熔断有三种触发模式,要根据下游服务的实际表现来选择:
慢调用比例熔断。当接口在指定时间内的调用响应时间超过阈值的比例达到设定值时,触发熔断。举例:maxRt=500(超过500毫秒算慢调用)、proportion=0.5(慢调用占比50%)、count=100(1秒内请求数超过100才会触发统计)。适合数据库慢查询或外部接口偶发变慢的场景。
异常比例熔断。当接口异常数量占总调用数的比例超过阈值时熔断。适合下游服务稳定抛异常,比如第三方接口返回错误码。
异常数熔断。当单位时间内的异常数量超过阈值时熔断。适合异常绝对值明显的场景。
我实际用下来,慢调用比例最常用,因为很多故障的表现就是响应变慢而不是直接抛异常。熔断打开后,后续请求会快速失败,不再打到下游瘫痪的服务,同时开启定时探测,等下游恢复后自动关闭熔断。
4.5 热点参数限流和系统保护
除了普通接口限流,还有两个容易被忽略但很有用的功能。
热点参数限流可以精确到某个参数值。比如商品详情页,大部分商品流量正常,但某个爆款商品的流量可能是其他商品的100倍,这时候可以给商品ID配置单独的限流阈值。Sentinel通过@SentinelResource配合热点规则实现:
java复制@SentinelResource(value = "getSkuDetail",
blockHandler = "getSkuDetailBlockHandler")
public SkuDetailVO getSkuDetail(@RequestParam("skuId") Long skuId) {
// 查询逻辑
}
系统自适应保护是全局层面的兜底。可以在Dashboard里开启系统规则,当系统负载超过阈值、CPU使用率超过设定值、或者进入系统的请求总量过多时,Sentinel会进行全局流量控制,优先保护核心业务。
5. 工程落地中遇到的常见问题与排查实录
5.1 若依微服务框架起步的几个坑
有一定基础的同学喜欢拿若依微服务版当脚手架,毕竟它把Spring Cloud Alibaba的整套都集成好了。但这个框架上手有一些细节问题,新手容易卡住。
启动顺序:若依微服务版有几个必须按顺序启动的模块。首先是Nacos(服务注册中心),然后是网关gateway,再是各个业务服务。我第一次拉下来项目,直接启动system服务,结果一直报连接Nacos失败,后来才发现Nacos还没启动。
默认账号问题:若依里的初始账号是admin,但数据库脚本执行后密码会加密存储。如果登录不上,去sys_user表把密码字段值改成空字符串,再走一遍重置密码逻辑。
服务间鉴权:若依默认开启了所有服务间的认证校验,自定义的接口如果不加@Anonymous注解会被401拦截。很多初学者写了个新接口,从网关调到system服务直接报错,就是这个原因。
5.2 MinIO对象存储的国产替代实践
项目中原本用的图片存储方案是云上的对象存储,后来因为成本和合规要求,换成了开源的MinIO。MinIO是国产友好的开源对象存储方案,兼容S3协议,支持分布式部署。
集成到Spring Boot非常简单:
java复制@Configuration
public class MinIOConfig {
@Bean
public MinioClient minioClient(MinIOProperties properties) {
return MinioClient.builder()
.endpoint(properties.getEndpoint())
.credentials(properties.getAccessKey(), properties.getSecretKey())
.build();
}
}
public String uploadFile(MultipartFile file) {
String objectName = UUID.randomUUID().toString() + "-" + file.getOriginalFilename();
minioClient.putObject(PutObjectArgs.builder()
.bucket("product-images")
.object(objectName)
.stream(file.getInputStream(), file.getSize(), -1)
.contentType(file.getContentType())
.build());
return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
.bucket("product-images")
.object(objectName)
.expiry(7, TimeUnit.DAYS)
.build());
}
需要注意,MinIO上传大文件时,不能一次性读出全部字节到内存,应该用stream(file.getInputStream(), file.getSize(), -1)分片上传,避免造成JVM内存溢出。另外,MinIO自己使用纠删码存储,分布式部署至少需要4个数据盘,生产环境不要把数据盘和应用盘放在同一个节点上,否则磁盘故障会导致数据不可恢复。
5.3 掉单问题排查实录
大促期间发现一个比较隐蔽的问题:用户支付成功后,订单状态偶尔会停留在"待支付"。排查链路后发现问题出在支付回调的异步消息处理上。支付服务发送消息给订单服务,订单服务消费消息后更新订单状态。但订单服务当时正在做一次大版本升级,消息消费后还没更新完数据库,服务被强制重启,导致消息已经ACK了但数据没写进去。
这就是典型的"消息丢失"问题,来得很隐蔽。最终方案做了两层修复:
第一层,订单状态更新不依赖消息的最终一致性。在订单表增加payment_status字段,支付回调时直接通过Feign调用订单服务,如果调用失败就标记为"回调失败"状态,由定时任务扫描补偿。
第二层,消息消费端增加本地消息表的双重保障。消费者在处理消息前,先把消息记录到本地事务表,业务处理成功后标记完成。定时任务扫描未完成的消息,重新触发业务处理。
5.4 避坑清单与排查思路速查
| 问题 | 可能原因 | 排查方向 |
|---|---|---|
| 服务注册不上 | Nacos地址配置错误、防火墙端口没开 | 检查nacos.core.auth.enabled是否和配置一致,服务端8848/9848端口都要通 |
| Feign调用超时 | 下游服务线程池满、网络抖动 | 查看Sentinel监控和调用链,确认下游耗时 |
| 分布式锁不生效 | 多实例部署但锁key没有区分环境 | 锁key统一加环境前缀,如prod:stock:1001 |
| 消息消费非常慢 | 消费者数量太少、处理逻辑耗时长 | 检查消费者线程数、消息积压数,考虑增加分区和消费者实例 |
| 限流失效 | 规则没加载、Sentinel依赖没引入 | 打开Dubbo控制台查看实时监控,确认规则是否推送成功 |
这套排查思路的核心是先看监控,再看日志,最后才动代码。很多线上问题不需要猜,SkyWalking链路追踪里已经把每次调用的耗时和异常都记录下来,直接顺着链路看就能定位到具体是哪个服务、哪个方法慢。
6. 关于这套技术栈的一些个人体会
做完整套改造,我自己最大的感受是:微服务架构里的这些组件,每一个单独拿出来都不算复杂,但组合在一起的联动效应才是真正的难点。分布式锁保证了库存扣减的互斥,消息队列承接了流量的削峰,Sentinel在突发流量下保护了下游服务,三者环环相扣,缺一个整套系统都不稳。
另外有两点想提醒正在做类似项目的同学:
第一,新技术一定要先做小流量验证再上全量。我们的Redis Stream消息组件,先在积分服务上跑了两周,确认消费稳定、重复消费频率低于万分之一,才推广到订单链路。
第二,不要过度设计。如果业务量还没到日均百万级,单体加缓存加数据库优化可能比微服务更合适。微服务拆分增加了部署、监控、排查的复杂度,中小团队可能得不偿失。
这套架构后来在两次大促中分别扛住了平时8倍和10倍的流量,除了个别慢SQL触发过限流,核心链路没再出过线上事故。希望大家在实际项目中也能一步步走通这条路,遇到具体问题随时可以对照文章里的方案来排查。
