这两年做交易类的微服务项目最头疼的往往不是业务逻辑本身而是“下单成功之后积分没加”、“库存扣了但订单显示失败”这种数据对不上的问题。微服务一拆数据库跟着拆以前一个本地事务能搞定的事情现在得跨服务跨库去协调。一上来就上 TCC、Seata、消息中间件很多团队其实扛不住那个成本。所以我这次想聊聊一个很务实的方案SpringBoot 配合本地消息表在不依赖任何额外中间件的情况下把分布式事务的最终一致性做起来。这个方案非常适合那些还在用单库、服务拆了但不想立刻引入 MQ 的团队尤其是订单、支付、积分、账户这类核心交易链路。本地消息表不是什么新东西它的思想其实很朴素把“发消息”这一步和“业务操作”放在同一个本地数据库事务里业务成功了消息一定落库业务失败了消息也跟着回滚。之后由一个后台任务去扫这张表把消息挨个投递给下游服务下游消费成功再更新消息状态。没有 Kafka、没有 RocketMQ、没有 Seata只要一个关系型数据库加一个定时任务就能把“跨服务的最终一致性”给撑起来。这篇文章我会从问题拆解开始逐步讲清楚表结构怎么设计、事务怎么编排、定时任务怎么写、消费端幂等怎么做最后再把我实际踩过的坑和排查经验整理出来。适合正在做服务化拆分、又不想一开始就引入重载中间件的朋友参考。1. 分布式事务的痛点与本地消息表方案的定位1.1 微服务拆分后为什么一个简单操作会变得这么难先把场景具象化用户在商城下单订单服务创建一条订单记录同时需要告诉库存服务扣减库存告诉积分服务给用户加积分。在单体时代这三个操作都在一个数据库里一个Transactional就完事要么全成功要么全回滚。拆分成微服务之后订单库、库存库、积分库各管各的。下单这个动作变成了“订单服务本地写库 远程调用库存服务 远程调用积分服务”。如果调库存服务成功了调积分服务超时了怎么办订单已经建好了不可能回滚库存积分又没加上。用户看起来就是“钱扣了积分没到账”。很多团队第一反应是上分布式事务框架。但无论是 XA 两阶段提交还是 TCC都有不小的代价。XA 对数据库支持和锁时间要求高TCC 需要你为每个服务手写 confirm 和 cancel 逻辑开发和维护成本都不低。更现实的问题是业务量根本没大到需要那种级别的一致性保障杀鸡用牛刀还把自己绑死在特定中间件上。1.2 本地消息表的核心思想把“远程调用”降级为“异步投递”本地消息表这个方案核心思路是你不需要在本地事务执行过程中远程调用别人你只需要在这个事务里往自己库里写一条消息记录。整个分布式操作被拆成了两个阶段第一阶段本地事务里完成业务写操作同时把需要通知下游的消息写入本地消息表。这个事务只涉及本地库所以它是绝对可靠的。第二阶段后台定时任务扫描本地消息表找到状态为待发送的消息将这些消息投递给下游服务的接口。投递成功更新消息状态为已送达投递失败保留待发送状态并记录重试次数等下一次扫描再试。下游服务收到消息后执行自己的业务逻辑。因为消息可能被重复投递所以下游必须做幂等重复执行不会产生副作用。整个过程没有分布式锁、没有全局事务、没有消息中间件只有一个可靠的本地数据库事务加一个轮询任务。最终的效果是上游业务操作成功了消息一定会被投递出去下游因为各种原因失败了会不断重试直到成功或者进入死信。1.3 为什么说它是“无中间件依赖”的可靠方案拿它和引入消息中间件做对比。你用 RocketMQ 的事务消息需要部署 Broker需要保证 Broker 的高可用需要处理消息堆积、消费位点这些运维问题。你用 Kafka也一样而且 Kafka 在事务消息这块的支持并不算特别顺手。本地消息表方案依赖的东西只有三个业务数据库、定时任务、HTTP 调用或者 RPC 调用。这三个东西在绝大多数微服务项目里本来就有。数据库你不可能不用定时任务用一个Scheduled注解就能起远程调用用 Feign 或者普通的 HTTP Client 都行。所以从落地角度看它几乎零额外成本。当然它也是有代价的。最大的代价是数据一致性达成有时间窗口不是实时的。这个窗口取决于定时任务扫描的频率。第二个代价是消息表本身会无限增长需要定期清理。第三是投递的可靠性完全靠自己的重试机制保障如果重试逻辑写得不够健壮消息可能会丢。这些代价在中小规模场景下完全可以接受并且可以通过设计来规避后面我会展开讲。2. 方案整体设计表结构与消息状态机2.1 一个订单加积分的具体例子空谈概念不好理解我用一个具体的例子贯穿全文。假设有两个服务订单服务Order Service和积分服务Point Service。用户下单后订单服务先在自己的库里创建订单然后要通知积分服务给这个用户增加 100 积分。我们不允许在创建订单的本地事务里直接远程调用积分服务因为那样又回到了分布式事务的坑里。正确做法是订单服务在同一个本地事务里插入订单记录、插入一条本地消息消息内容是“给用户 ID 为 9527 的用户增加 100 积分关联订单号 ORDER-20250101-0001”。事务提交后后台任务扫描到这条消息通过 HTTP 调用积分服务提供的接口。积分服务收到请求后先检查这条消息是否已经处理过没处理过才真正加积分然后返回处理成功。后台任务收到成功响应后把这条消息的状态标记为已完成。如果积分服务挂了调用失败消息状态保持待发送重试次数加一。下一次扫描再试。如果一直失败超过最大重试次数消息进入死信状态由人工排查处理。这个例子里订单记录和积分数据最终一定是一致的只是中间可能有延迟。对大多数业务来说下单后积分延迟几秒甚至几分钟到账完全不是问题。2.2 本地消息表的字段设计消息表是整个方案的核心载体字段设计直接影响后续开发的复杂度和可靠性。下面是我自己整理过的一版结构虽然不是最精简的但各方面考虑得比较周全。CREATE TABLE local_message ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, msg_id VARCHAR(64) NOT NULL COMMENT 全局唯一消息ID消费端幂等依据, biz_key VARCHAR(128) NOT NULL COMMENT 业务键例如订单号:ORDER-20250101-0001, msg_type VARCHAR(32) NOT NULL COMMENT 消息类型例如 POINT_ADD, msg_body TEXT NOT NULL COMMENT 消息体JSON格式存放下游需要的数据, status TINYINT NOT NULL DEFAULT 0 COMMENT 状态0待发送1已发送2已完成3死信, retry_count INT NOT NULL DEFAULT 0 COMMENT 已重试次数, next_retry_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 下次可发送时间, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_msg_id (msg_id), KEY idx_status_next_retry_time (status, next_retry_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT本地消息表;字段设计的逻辑msg_id是全局唯一消息 ID一般用 UUID 或者雪花算法生成。它有两个作用一个是消息表本身唯一另一个是下游消费端拿它做幂等判断。如果你没有这个 ID下游面对多次投递只能靠业务键去重很容易漏所以必须一开始就定下来。biz_key是业务键比如订单号、支付流水号。排查问题的时候你通过biz_key就能快速找到某个订单关联的所有消息不用去翻无聊的日志。它也能辅助消费端做幂等比如消息类型是“增加积分”消费端可以用“订单号用户ID”建唯一索引天然去重。msg_body存放 JSON 格式的消息内容。我不建议只存一个下游服务的接口 ID因为这样排查和补偿的时候你根本不知道当时要发什么数据。把完整的 JSON 放进去后续扩容、死信重放都很方便。status是状态字段。0、1、2、3 四个值足够用了不需要更精细的状态。有些人会加一个“发送中”状态但我觉得没必要因为你没法区分“发送中”和“超时失败”不如用待发送加重试次数来处理。next_retry_time很关键。定时任务扫描的时候不是扫所有待发送的消息而是只扫next_retry_time小于当前时间的消息。这个字段是实现指数退避的基础没有它消息失败后只能固定频率重试容易把下游打到半死。索引设计上不要单独对status建索引因为数据量大了之后状态区分度不高。(status, next_retry_time)联合索引才是真正会被查询条件用到的索引。2.3 消息的状态流转与生命周期消息在表里面会经历几个状态理解清楚了才能写出正确的处理逻辑。消息插入时状态为 0即待发送。后台任务扫描时会把状态为 0 且next_retry_time到时的数据捞出来然后一条一条投递。投递前我会建议把状态先更新为 1即已发送。这么做的目的是告诉其他节点“这条消息我正在投递你别碰”。如果投递成功再把状态更新为 2如果投递失败回滚状态到 0并更新重试次数和下次重试时间。为什么投递前要先置为已发送因为这里涉及分布式环境下的并发问题。如果你不改状态两个定时任务节点可能同时扫描到同一条待发送的消息然后同时去调用下游接口造成重复投递。虽然下游有幂等兜底但能少一次重复就少一次。失败回滚时的逻辑要小心。当投递失败后不能简单地把状态改回 0还要同时把retry_count加一并计算新的next_retry_time。如果重试次数达到上限就把状态置为 3即死信。状态机的规则可以整理成下面这个表格操作原状态新状态附带更新插入消息无0 待发送设置next_retry_time为当前时间开始投递0 待发送1 已发送无投递成功1 已发送2 已完成更新update_time投递失败且未达上限1 已发送0 待发送retry_count1重设next_retry_time投递失败且达上限1 已发送3 死信记录最后一次错误信息人工重放死信3 死信0 待发送重置retry_count2.4 主流程串联起来看把前面这些内容串成一个完整时序大致是这样用户在订单服务下单订单服务开启本地事务插入订单数据同时生成一条消息插入local_message表。事务提交。后台定时任务每秒扫描一次发现待发送消息取出消息内容调用积分服务接口。积分服务收到请求后先检查自己的去重记录表里有没有这条msg_id没有则执行加积分操作并记录msg_id返回成功。定时任务收到成功响应将消息状态更新为已完成。如果积分服务返回失败或者超时定时任务捕获异常把消息状态改回待发送重试次数加一下次重试时间按退避策略往后推。如果重试到第 10 次还是失败消息进入死信状态等待人工介入。整个过程设计下来你会看到没有任何一个环节依赖第三方中间件。本地事务保证业务数据和消息数据同时成功同时失败后台任务保证消息最终会被投递出去消费端幂等保证即使重复投递也不会产生重复数据。这三点合起来就是最终一致性。3. SpringBoot 实现落地本地事务与定时投递3.1 工程依赖与项目结构实现层面我用 SpringBoot 来写。需要引入的基础依赖包括 Spring Web、Spring Data JPA或者 MyBatis、MySQL Driver再加一个spring-boot-starter-web用来提供下游接口。如果你用的是 MyBatis-Plus也可以核心逻辑不受影响。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency单机演示可以直接用Scheduled做定时任务。如果是多节点部署后面我会讲怎么加分布式锁。3.2 核心代码订单事务与消息表写入同进同退这一步是整个方案的灵魂。很多初学者会把“业务操作”和“消息写入”拆成两个事务去做先写订单再写消息表认为只要都成功了就没事。但问题在于如果订单写成功了消息表写入时数据库突然宕机或者事务提交时部分成功会出现订单有记录但没消息的情况。没有消息下游就永远不会被通知数据不一致。正确姿势是把业务操作和消息表插入放在同一个Transactional事务里。我用一个简化版的订单创建来演示Service public class OrderService { Resource private OrderRepository orderRepository; Resource private LocalMessageRepository messageRepository; Transactional(rollbackFor Exception.class) public void createOrder(OrderCreateRequest request) { // 1. 业务操作创建订单 Order order new Order(); order.setOrderNo(request.getOrderNo()); order.setUserId(request.getUserId()); order.setAmount(request.getAmount()); orderRepository.save(order); // 2. 构造消息体 MapString, Object body new HashMap(); body.put(userId, request.getUserId()); body.put(orderNo, request.getOrderNo()); body.put(points, 100); // 3. 写入本地消息表 LocalMessage message new LocalMessage(); message.setMsgId(UUID.randomUUID().toString()); message.setBizKey(order.getOrderNo()); message.setMsgType(POINT_ADD); message.setMsgBody(JSON.toJSONString(body)); message.setStatus(0); message.setRetryCount(0); message.setNextRetryTime(new Date()); messageRepository.save(message); } }这里的Transactional确保了要么订单和消息都提交要么都回滚。如果订单保存失败消息也不会落库如果消息落库失败订单同样被回滚。注意事务方法内部不要有任何远程调用也不要为了“优化性能”在事务里先调远程再删除消息这些都是反面教材。事务里只做本地数据库操作远程投递一定是事务提交之后的事情。3.3 后台定时任务扫描并投递消息事务提交后消息静静躺在表里。接下来就需要一个后台任务去把它捞出来投递给下游。先用一个最简单的Scheduled版本Component public class MessageSenderJob { Resource private LocalMessageRepository messageRepository; Scheduled(fixedDelay 1000) // 每轮执行完间隔1秒 public void scanAndSend() { // 捞取待发送且到期的消息 ListLocalMessage messages messageRepository.findPendingMessages(new Date(), 100); for (LocalMessage message : messages) { // 先把状态置为已发送防止其他节点重复处理 int updated messageRepository.compareAndSetStatus(message.getId(), 0, 1); if (updated 0) { continue; } try { // 投递远程调用积分服务 PointAddResult result pointClient.addPoints( message.getMsgBody()); if (result.isSuccess()) { messageRepository.updateStatus(message.getId(), 2); } else { handleFail(message); } } catch (Exception e) { log.error(message send failed, msgId{}, message.getMsgId(), e); handleFail(message); } } } }这里有个细节值得说扫描之后、投递之前先做一次状态比较更新CAS。SQL 大概长这样UPDATE local_message SET status 1 WHERE id ? AND status 0这个更新影响行数为 1才说明当前节点抢到了这条消息的处理权。如果影响行数为 0说明有其他节点已经把状态改成 1 了当前节点直接跳过。这是在多节点部署环境下防止重复投递的第一道防线。3.4 失败重试与死信判断先看handleFail方法怎么写。这里需要决定重试策略。我采用的是一种指数退避策略按照 1 分钟、5 分钟、30 分钟、2 小时、12 小时、24 小时这样的节奏去推。每次失败根据已重试次数算出下一次可执行时间。private static final int MAX_RETRY_COUNT 10; private void handleFail(LocalMessage message) { int nextRetryCount message.getRetryCount() 1; if (nextRetryCount MAX_RETRY_COUNT) { // 超过最大重试次数标记死信 messageRepository.updateDead(message.getId()); log.error(message reaches max retry count, msgId{}, message.getMsgId()); return; } Date nextTime calcNextRetryTime(nextRetryCount); messageRepository.updateRetry(message.getId(), 0, nextRetryCount, nextTime); } private Date calcNextRetryTime(int retryCount) { long delayMinutes Math.min(24 * 60, 1L Math.min(retryCount, 10)); return new Date(System.currentTimeMillis() delayMinutes * 60 * 1000); }这段代码的核心是把重试间隔从短到长逐步扩大避免在下游服务恢复之前疯狂重试把下游打垮。重试到第 10 次依然失败就进入死信等待人工处理。3.5 下游消费端幂等实现消息投递到下游后下游需要保证幂等。为什么一定需要因为网络是不可靠的即使上游做了“已发送状态”的 CAS依然可能出现这种情况下游处理成功了但返回响应的过程超时了。上游捕获到异常认为投递失败于是重置状态为待发送再次投递。下游如果没做幂等同一条加积分消息就被执行两次用户凭空多出 200 分。幂等设计的最稳妥方式是下游建一张消费记录表以msg_id作为唯一键CREATE TABLE point_received_message ( id BIGINT NOT NULL AUTO_INCREMENT, msg_id VARCHAR(64) NOT NULL, order_no VARCHAR(64) NOT NULL, user_id BIGINT NOT NULL, status TINYINT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_msg_id (msg_id) ) ENGINEInnoDB;积分服务接收到请求后先尝试插入一条消费记录。如果插入成功说明这是第一次处理继续执行加积分逻辑。如果插入时抛出唯一键冲突异常说明之前已经处理过直接返回成功。PostMapping(/addPoints) public Result addPoints(RequestBody PointAddRequest request) { try { receivedMessageRepository.save(new ReceivedMessage(request.getMsgId())); } catch (DuplicateKeyException e) { return Result.success(); // 已处理过直接返回成功 } pointService.addPoints(request.getUserId(), request.getOrderNo(), request.getPoints()); return Result.success(); }技巧这里的去重表插入要和加积分业务操作放在同一个本地事务里。因为如果你先插入消费记录再执行加积分加积分失败但消费记录已提交下次消息来了直接返回成功积分还是没加上。反过来如果加积分成功但消费记录提交失败下次又会重复加积分。所以幂等表中插入记录与业务更新必须同事务。一个更简单的变通方案是如果你的业务表本身有唯一索引比如积分流水表有order_no的唯一索引那么可以直接在流水表上做幂等不用额外建消费记录表。但通用性不如独立消费记录表。4. 实际项目中的坑点与排查经验方案虽然简单但落地过程中有不少隐藏的坑。我按照自己踩过的顺序把最典型的几个问题整理出来。4.1 最大坑在本地事务里做了远程调用我见过不止一个团队把“本地消息表”做歪了。他们的代码是这样的Transactional public void createOrder(OrderCreateRequest request) { saveOrder(request); saveMessage(request); // 在事务内部直接远程调用积分服务 pointClient.addPoints(request); }这样写之后事务的提交被拉得很长数据库连接被长期占用。如果积分服务响应慢事务要等它如果积分服务挂了事务可能超时报错导致订单也创建失败。更严重的是如果远程调用成功但事务随后提交失败消息表里没有记录但积分服务已经加了分数据不一致又出现了。正确做法本地事务里只写业务数据和消息然后把事务提交最后通过定时任务去投递。如果你确实需要在事务提交后立刻投递可以用TransactionSynchronizationManager.registerSynchronization在事务提交后触发一次投递但也要注意这种一次性的触发不能代替定时任务兜底只能作为加速手段。4.2 消息重复投递不是偶发现象而是常态很多新人会把精力花在“尽量避免重复投递”上但投递超时导致的重试重复在这个网络环境下根本无法完全避免。真正成熟的做法是接受重复然后在消费端用唯一索引或者状态机去冪等。我自己在实际项目里是这么分层的上游用 CAS 状态更新降低重复投递概率下游用唯一键兜底彻底解决重复消费问题。两层都做系统才稳。4.3 定时任务在集群环境下重复执行单机环境直接用Scheduled没问题。但一旦订单服务部署了多个节点问题就来了每个节点都会启动一个定时任务同一秒内多个节点同时扫描消息表虽然 CAS 状态更新能挡住大部分重复处理但会造成不必要的数据库压力也会让下游被无谓地调用多次。解决办法主要有两个。一个是引入分布式锁在任务执行前尝试获取锁拿到锁的节点才执行本轮扫描。用 Redis 的SETNX加一个带超时时间的锁就能实现。另一个是把扫描任务交给独立的调度平台定时任务只在一个节点上运行。如果你用的是 XXL-Job 这类平台直接配成单节点路由策略就行。我个人更推荐第二种。因为分布式锁本身也有过期时间设置的问题可能锁还没执行完就过期了另一个节点抢到锁造成重复执行。独立调度平台则简单得多。4.4 消息表数据无限膨胀本地消息表的数据会一直增长成功的消息如果不清理一年下来可能有几千万条占用大量存储空间还会拖慢索引。我的做法是分级处理状态为已完成2的消息保留 3 天之后由清理任务批量删除。状态为死信3的消息保留 30 天方便人工排查超过 30 天后人工确认没问题再删除。清理任务不要在业务高峰期大批量删除要控制每批删除的数量一次删个几百条避免产生长时间行锁。4.5 消息体变更与下游兼容消息体是用 JSON 存的这给了我们很大的灵活性。但有一个问题需要提前约定下游消费接口必须对 JSON 中的未知字段保持兼容。比如你今天在消息体里加了一个sourceChannel字段老版本的积分服务如果用了严格的反序列化配置可能会报错。解决方式是在msg_type里加版本号比如POINT_ADD_V2一旦消息体结构发生不兼容变化就换一个新的msg_type。下游旧版本只处理POINT_ADD新版本同时处理两种。这个习惯能帮你省掉很多线上兼容性问题。5. 方案选型评估本地消息表和其他分布式事务方案怎么选5.1 常见方案横向对比在决定用本地消息表之前有必要把主流的分布式事务方案放在一起比较一下。方案一致性类型是否需要中间件开发成本数据一致性时效典型场景XA 两阶段提交强一致需要数据库支持低即时单库多表单体应用TCC最终一致否高需手写Confirm/Cancel即时资金类核心操作可靠消息事务RocketMQ最终一致需要MQ中秒级异步通知、数据同步最大努力通知最终一致否中不确定依赖重试回调通知场景本地消息表最终一致否低秒级到分钟级中小团队微服务、内部系统本地消息表在“开发成本”这一项上优势很明显比 TCC 轻、比 XA 简单适合那些业务链路不长、对一致性时效要求不那么苛刻的场景。5.2 本地消息表的优势与适用场景我总结下来下面这些情况非常适合用本地消息表第一个是团队规模不大没有专门的中间件运维能力不想为了一个积分通知功能就去搭一套 RocketMQ。第二个是业务本身允许秒级甚至分钟级延迟比如积分到账、站内信通知、数据同步用户晚几分钟看到完全没影响。第三个是上游和下游共享同一个数据库或者可以接受在各自库中维护一张消息表。第四个是希望保留完整的消息记录方便对账和问题排查。我在上一家公司的做法就是所有核心业务的数据变更统一定义成消息体写入各自服务的本地消息表然后由一个分布式调度平台驱动投递。这样做的好处是所有一致性问题都收敛到一个模式里团队没有学习成本排查问题时直接从消息表追溯链路非常方便。5.3 本地消息表的边界与不适用场景这个方案也有明显的短板。它不适合高并发、大流量的场景。消息表本身就是一张数据库表每秒扫描几万条待发送消息对数据库压力不小。如果你要做秒杀、抢购类的系统本地消息表很可能成为瓶颈。它不适合强一致要求的场景。比如转账、支付退款这种必须即时生效的操作用户转完账立刻要能看到余额变了你不可能告诉用户“请稍等几秒钟再查余额”。这类场景还是得用 TCC 或者可靠的实时调用 手动补偿。它不适合跨组织、跨异构存储的场景。如果下游是外部公司的系统你不可能在他们库里也建一张本地消息表消息表投递失败后的协调会变得非常麻烦。5.4 本地消息表和 MQ 可以混合演进这个方案并不是和消息中间件对立的。等业务量上来了你可以很平滑地把“定时任务扫描发送”替换成“事务提交后发送到 MQ”。因为消息表里的数据已经结构化了你要做的只是把投递逻辑换一下消息还是那些消息下游接口也不用改。我实际遇到过的情况是项目初期用本地消息表跑了大半年后来业务规模上来团队也有能力维护 MQ 了才逐步把部分高频消息切换到 RocketMQ。切换过程没有伤筋动骨就是因为早期的消息模型设计得干净。所以不用一上来就追求一步到位先把一致性做对再在性能需要时演进。写在最后的实际操作心得做这个方案一年下来我最大的体会是技术方案的价值不在于它听起来多高级而在于能不能用最少的成本把问题解决干净。本地消息表虽然朴实但它把“分布式事务”这个听着吓人的概念实实在在地落地到了每一行 SQL、每一个定时任务里。最后分享一个小技巧。线上排查数据不一致问题时与其看日志找半天不如直接在消息表上按biz_key查一遍。比如用户投诉积分没到账你就拿订单号去查消息表看消息状态停在哪个环节消息根本没生成说明上游本地事务有问题消息停在待发送说明定时任务出故障了消息停在已发送或者死信说明下游消费或者网络有问题。一查一个准比什么分布式追踪都好用。另外提醒一点消息表一定要记得加最后修改时间和错误信息字段最好再加入一个单独的last_error字段用来记录最后一次失败原因这样看死信消息时能立刻知道是下游返回了业务错误还是连接超时不用每次都在代码里加日志重新跑一遍。这个方案没有太多花哨的东西但它足够稳。如果你正在为微服务的数据一致性发愁又不想背上沉重的中间件包袱不妨从本地消息表开始。
阅读完成 · 觉得有帮助?