.NET Core单微服务场景下如何实现同步事务系统保障数据一致性
.NET Core单服务双数据源写入一致性方案
首先纠正一个常见认知偏差:SAGA模式并非跨微服务异步场景专属,单服务内同步多数据源写入完全可以用轻量编排式同步SAGA实现,不需要依赖RabbitMQ/Kafka这类消息组件;而2PC完全不适合你的场景——MongoDB、Neo4j都没有成熟的XA协议兼容驱动,强推2PC会带来极高的性能损耗和锁冲突问题,落地成本极高。
你构思的两个方案的固有缺陷
你想到的两个方向都是对的,但单独用都有明显漏洞:
- 纯
try-catch+删除补偿:依赖内存中异常捕获逻辑的可靠执行,一旦Neo4j崩溃是由服务进程退出、机器宕机、网络瞬断导致的,catch块根本没有执行机会,Mongo里的脏数据会永久残留;就算catch块正常执行,Mongo删除操作如果因为集群同步问题没生效,同样会留脏数据。 - 仅加
Pending/Completed状态字段:如果没有配套的兜底逻辑,进程崩溃时残留的Pending状态数据会一直存在,而且你需要在所有业务查询点都加Status=Completed的过滤条件,漏加一次就会把无效数据查出来,线上埋坑风险很高。
推荐落地实现(无额外中间件依赖,适配同步接口场景)
这套方案本质是同步编排式SAGA+持久化状态机兜底,完全适配你当前单服务的同步CreateEntity接口场景,实现成本极低:
- 先给Mongo的对应实体集合加3个固定字段:
Status:整型枚举,取值为Pending=0/Completed=1/Failed=2CreatedAt:UTC时间戳,记录实体首次插入时间LastError:字符串字段,存储Neo4j写入失败的异常信息,方便问题排查
- 接口执行流程固定为以下顺序,不要随意调整:
- 第一步:写入Mongo,实体
Status初始值固定为Pending,拿到插入后生成的唯一实体ID - 第二步:携带该实体ID调用Neo4j写入逻辑
- 第三步:若Neo4j写入成功,直接更新Mongo中对应ID的实体,将
Status改为Completed,返回接口成功响应 - 第四步:若Neo4j写入抛出异常,立刻执行同步补偿:将Mongo对应实体的
Status改为Failed,把异常信息写入LastError字段,返回接口失败响应
- 第一步:写入Mongo,实体
- 加两层兜底机制,彻底解决进程中断、操作执行到一半失败的极端场景问题:
- 第一层:在数据访问层做全局查询过滤,默认只返回
Status=Completed的实体,从入口挡住无效数据,不需要每个业务查询点单独加过滤条件,避免漏写 - 第二层:用.NET Core原生的
IHostedService实现一个轻量后台扫表任务,每5分钟执行一次,扫描Mongo中CreatedAt早于10分钟、Status仍为Pending的悬停数据:- 先查Neo4j中是否存在对应ID的实体,如果存在,说明之前是Mongo状态更新步骤失败了,直接把对应实体的
Status改为Completed - 如果Neo4j中不存在对应实体,说明Neo4j写入确实未成功,直接把Mongo实体状态改为
Failed,可按需配置告警通知开发人员排查异常
- 先查Neo4j中是否存在对应ID的实体,如果存在,说明之前是Mongo状态更新步骤失败了,直接把对应实体的
- 第一层:在数据访问层做全局查询过滤,默认只返回
- 幂等性增强:Neo4j写入时直接用Mongo生成的实体ID作为唯一主键,就算出现接口重复调用、扫表任务重复执行的情况,也不会产生重复数据,不需要额外写幂等判断逻辑。
不推荐的方案说明
- 不要硬上TCC/2PC等强一致分布式事务:Mongo和Neo4j都没有原生支持,自己实现的逻辑复杂度极高,单服务场景下投入产出比极低
- 不要仅依赖内存中的异常捕获做补偿:没有持久化状态记录的话,进程一旦异常退出,所有补偿逻辑都会失效,脏数据问题无法彻底解决
- 不要为了这个场景强行引入消息队列做异步SAGA:你的接口是同步场景,引入MQ会拉长调用链路,额外带来消息重复、消费延迟、消费失败等一堆需要处理的问题,完全没必要
内容的提问来源于stack exchange,提问作者Mihai Alexandru-Ionut
相关产品推荐
相关产品推荐

