做后端开发到第五年,微服务该用的组件都用过了,但真正让我在线上吃过亏的,翻来覆去就是三个问题:分布式事务怎么收场、出了故障怎么快速定位、接口变更怎么不炸上游。
这篇文章不讲理论,直接给方案和代码。
线上出过一次事故:订单服务调库存服务扣减,库存服务超时回滚,但订单状态已经改成"已支付",两边数据对不上,最后手工修了半小时数据。
教训: 跨服务的强一致性在真实网络环境下是奢望。
我的方案是本地消息表 + 定时对账,核心逻辑就三块:
// 1. 本地事务:订单入库 + 消息落库(同库,保证原子性)
@Transactional
public void createOrder(OrderDTO order) {
orderMapper.insert(order);
messageMapper.insert(new PendingMessage(
"inventory",
"deduct",
order.getSkuId(),
order.getQuantity()
));
}
// 2. 定时任务:轮询未发送消息,调用库存服务(带指数退避重试)
@Scheduled(fixedDelay = 5000)
public void dispatchPendingMessages() {
List<PendingMessage> messages = messageMapper.selectUnsent(100);
for (PendingMessage msg : messages) {
try {
inventoryClient.deduct(msg.getSkuId(), msg.getQty());
messageMapper.markSent(msg.getId());
} catch (Exception e) {
msg.retryCount++;
msg.nextRetryTime = now + 2 ^ msg.retryCount * 1000;
messageMapper.update(msg);
}
}
}
// 3. 每日对账:拉取订单和库存流水,自动发现不一致并告警
@Scheduled(cron = "0 1 0 * * ?")
public void dailyReconciliation() {
List<Order> orders = orderMapper.selectYesterday();
List<InventoryLog> logs = inventoryLogMapper.selectYesterday();
// 比对两边数据,发现差异自动生成工单
List<Mismatch> mismatches = compare(orders, logs);
if (!mismatches.isEmpty()) {
alertService.send("对账异常", mismatches);
}
}这套方案上线后跑了半年,没有再出现过数据不一致的线上事故。代价是增加了5-10分钟的延迟(异步补偿),但业务可以接受。
以前排查故障的流程:用户报障 → 查Nginx日志找时间戳 → 跳转到应用服务器翻业务日志 → 发现日志不全 → 再查数据库慢查询 → 半小时过去了。
转折点是引入了OpenTelemetry + Jaeger,要求所有服务统一埋点。
日志改造前后对比:
# 改造前:纯文本日志,无法检索
logger.info("Order created, orderId: 12345")
# 改造后:结构化日志,自带trace_id
logger.info(
"Order created",
extra={
"trace_id": trace_id,
"order_id": "12345",
"user_id": "67890",
"cost_ms": 120,
"service": "order-service"
}
)Java服务端的拦截器配置(所有请求自动注入trace_id):
@Component
public class TraceInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
// 从上游提取或生成新的traceId
String traceId = request.getHeader("X-Trace-Id");
if (StringUtils.isEmpty(traceId)) {
traceId = UUID.randomUUID().toString();
}
// 放入MDC,日志框架自动打印
MDC.put("trace_id", traceId);
// 传递给下游服务
response.setHeader("X-Trace-Id", traceId);
return true;
}
}现在排查流程变成:输入trace_id → Jaeger展示完整调用链 → 点击每个span看结构化日志 → 定位到具体方法和SQL → 全过程不超过3分钟。
最痛的教训是某次重构,把订单服务的返回值从String orderId改成了Long orderId,结果上游支付服务直接报反序列化错误,回滚了两次才找到原因。
解决方案是引入消费者驱动契约(CDC),使用Spring Cloud Contract:
// 服务提供方定义契约(order-service)
Contract.make {
description "获取订单详情接口,必须返回订单ID为Long类型"
request {
method GET()
url "/order/10001"
headers {
header "Content-Type": "application/json"
}
}
response {
status 200
body([
orderId: 10001L, // Long类型,明确声明
amount: 99.50,
status: "PAID"
])
headers {
header "Content-Type": "application/json"
}
}
}<!-- Maven插件配置:自动生成契约测试 -->
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>4.0.0</version>
</plugin>每次代码提交时,CI流水线会自动执行契约测试。如果接口改动导致契约不兼容,构建直接失败,把问题拦截在开发阶段,而不是等到上线后才发现。
现在团队规定:所有对外接口的改动,必须同步更新契约文件,否则PR不予合并。
这三个问题解决之后,我最大的感受是:微服务的稳定性不是靠"不出错",而是靠"出错了能快速发现、快速恢复"。
分布式事务方案接受最终一致性,可观测性做到全链路可追踪,接口变更靠契约测试保底——这三件事做扎实了,系统就具备了基本的"抗造能力"。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。