尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

企业架构治理两类病:分布式单体与共享数据库

企业架构治理两类病:分布式单体与共享数据库 企业架构治理两类病分布式单体与共享数据库架构改造常见的误区不是技术选错而是没先定义领域边界服务按表拆、跨服务直接读库或把分布式事务当作默认方案都会把局部复杂度扩散到整条链路。本文讨论三个反模式及修正条件。事件驱动和 Outbox 也有适用成本只有在异步一致性可接受时才值得引入。flowchart TD subgraph AntiPattern [❌ 典型反模式分布式泥潭与共享 DB] ServiceA[订单微服务 A] --|网状 RPC 深度嵌套| ServiceB[用户微服务 B] ServiceB --|RPC 阻塞| ServiceC[库存微服务 C] ServiceA --|直接读写对方 DB| Database[(共享数据库 (Shared DB))] ServiceB -- Database end subgraph Refactored [✅ 修正范式事件驱动 (EDA) Outbox 边界防护] OrderSub[订单领域上下文 (Order Context)] --|1. 本地事务写入| OrderDB[(订单独立 DB Outbox 表)] OrderDB --|2. Debezium / Message Relay| EventBus[(RocketMQ / Kafka 事件总线)] EventBus --|3. 异步订阅 Domain Event| StockSub[库存领域上下文 (Stock Context)] end1. 反模式一过度微服务化导致的“分布式单体Distributed Monolith”某些团队将微服务拆分粒度切得过细甚至按照简单的数据库表结构进行拆分如用户服务、订单服务、地址服务、优惠券服务。在处理一个简单的创建订单业务时客户端请求先打到订单服务订单服务通过 RPC 依次同步调用用户服务、地址服务、优惠券服务和库存服务。这种设计将原本单体应用内的内存函数调用变成了 4 次跨网络 RPC。一旦其中任何一个微服务出现抖动或网络丢包整个链条就会发生级联崩溃Cascading Failure。使用arthas诊断在线服务追踪这个长链条的耗时与线程状态# 使用 Arthas 跟踪 Controller 的具体调用链耗时 trace com.example.trade.controller.OrderController createOrder #cost 200终端抓取到的诊断日志展示---ts2026-08-12 14:20:11;thread_namehttp-nio-8080-exec-12;ida4;is_daemontrue;priority5;TCCLorg.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader ---[312.45ms] com.example.trade.controller.OrderController:createOrder() ---[12.10ms] com.example.trade.service.UserService:getUser() #RPC Call 1 ---[45.30ms] com.example.trade.service.AddressService:getAddress() #RPC Call 2 ---[185.20ms] com.example.trade.service.CouponService:validateCoupon() #RPC Call 3 (Timeout!) ---[68.10ms] com.example.trade.service.StockService:deductStock() #RPC Call 4跟踪日志清晰地揭示因为链条太长单个下游服务的耗时拉长直接引发了上游 312ms 的延迟。正确修正方式划清界限上下文拥抱事件驱动架构EDA利用领域驱动设计DDD重新聚合业务概念。将高频同步交互的字段如地址、优惠券基础快照在下单时直接作为值对象Value Object打入订单领域不再进行实时 RPC 查询。同时将强同步调用重构为通过消息队列发起的“领域事件Domain Event”异步解耦。2. 反模式二共享数据库Shared Database另一个极为普遍的反模式是表面上独立部署了 5 个微服务但它们底层的 MyBatis 代码全部直接连接同一个 MySQL 数据库甚至互相交叉读写对方的数据库表。这种做法彻底击穿了微服务的隔离防线。一旦订单团队修改了user_info表的一个字段名称用户团队的服务在发布时就会因为 SQL 找不到列名而直接引发生产事故。正确修正方式数据私有化与 Outbox 模式每个微服务必须拥有完全私有的数据库实例或 Schema。禁止任何跨服务的 DB 直连所有跨域数据交换必须通过 API 或订阅领域事件完成。为了保证业务数据变更与领域事件发送的绝对一致性必须采用Transactional Outbox 模式在同一个本地数据库事务中既保存业务数据又向outbox表插入一条待发送事件由后台单独的 Relay 线程异步推送到 Kafka / RocketMQ。3. 生产级 Transactional Outbox 事件发布代码下面是在 Spring Boot 应用中落地的 Transactional Outbox 模式实现代码package com.example.architecture.outbox; import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.UUID; Service public class OrderDomainService { private static final Logger log LoggerFactory.getLogger(OrderDomainService.class); private final OrderRepository orderRepository; private final OutboxEventRepository outboxRepository; private final ObjectMapper objectMapper; public OrderDomainService(OrderRepository orderRepository, OutboxEventRepository outboxRepository, ObjectMapper objectMapper) { this.orderRepository orderRepository; this.outboxRepository outboxRepository; this.objectMapper objectMapper; } /** * 创建订单业务保证业务持久化与 Outbox 事件表保存在同一个本地 DB 事务中 */ Transactional public String createOrderTransactional(OrderEntity order) { // 1. 保存订单主业务数据 orderRepository.save(order); log.info(订单持久化成功, OrderID: {}, order.getOrderId()); // 2. 构建领域事件 payload OrderCreatedEvent event new OrderCreatedEvent( order.getOrderId(), order.getUserId(), order.getTotalAmount(), LocalDateTime.now().toString() ); try { // 3. 将事件写入同一数据库事务的 Outbox 表中 OutboxEntity outboxRecord new OutboxEntity(); outboxRecord.setEventId(UUID.randomUUID().toString()); outboxRecord.setAggregateType(ORDER); outboxRecord.setAggregateId(order.getOrderId()); outboxRecord.setEventType(ORDER_CREATED); outboxRecord.setPayload(objectMapper.writeValueAsString(event)); outboxRecord.setCreatedAt(LocalDateTime.now()); outboxRecord.setStatus(PENDING); outboxRepository.save(outboxRecord); log.info(Outbox 领域事件预埋成功 [EventID: {}], outboxRecord.getEventId()); } catch (Exception e) { throw new RuntimeException(序列化 Outbox 事件失败, e); } return order.getOrderId(); } }界限上下文先于服务拆分。若采用 Outbox需要同时设计事件幂等、失败重投和对账它解决的是本地事务与消息发布的一致性窗口不会自动解决所有跨服务问题。
返回列表