CAP本质是用消息兜住事务,通过本地消息表与DbTransaction绑定实现原子性;EF Core集成需同时配置UseEntityFramework<T>()和UseSqlServer等存储驱动,且必须指向同一数据库实例。

直接说结论:CAP 不是“用事务去包消息”,而是用消息来兜住事务——它靠本地消息表 + 事务绑定机制,把数据库操作和事件发布绑在同一个 DbTransaction 里,失败就一起回滚,成功才落库+发消息。别想着手动控制两阶段提交,那是反模式。
EF Core 集成时,UseEntityFramework<T>() 必须配 UseSqlServer 等存储驱动
很多人只写 x.UseEntityFramework<AppDbContext>() 就跑,结果启动报错:Unable to resolve service for type 'DotNetCore.CAP.IStorage' 。这是因为 CAP 的 EF 集成只是“事务协调器”,不提供底层存储能力。
-
UseEntityFramework<T>()只负责拦截SaveChanges、自动注入事务、写入[cap].[Published]表 - 但
[cap].[Published]和[cap].[Received]表存在哪?得靠UseSqlServer、UseMySql这类显式声明 - 二者必须指向同一数据库实例(不能 EF 写 A 库,CAP 存储写 B 库),否则事务无法原子生效
- 若用 SQLite 或内存数据库做测试,需额外确认其支持事务嵌套和 DTC 兼容性(通常不推荐)
手动 ADO.NET 场景下,BeginTransaction() 必须传给 _capPublisher.PublishAsync()
EF 是自动的,但纯 ADO.NET 要自己把事务“透传”进去。漏掉这一步,消息会立刻发出去,跟数据库事务完全脱钩——出现“订单写了,库存没扣”或“库存扣了,订单没写”的经典裂口。
- 正确写法:
await _capPublisher.PublishAsync("OrderCreated", order, new CapPublishOptions { Transaction = transaction }); - 错误写法:
await _capPublisher.PublishAsync("OrderCreated", order);(默认走独立连接+自动提交) -
CapPublishOptions.Transaction类型必须是DbTransaction,不能是SqlConnection或IDbConnection - 如果用 Dapper,注意
connection.Execute(..., transaction: tx)和PublishAsync(..., tx)必须是同一个tx实例
FailedRetryCount 和 FailedRetryInterval 不影响本地事务,只管投递失败后的重试
这两个配置常被误解为“事务重试次数”。其实它们只作用于 CAP 从 [cap].[Published] 表读出消息、往 RabbitMQ/Kafka 发送失败后的补偿逻辑。本地数据库事务一旦提交,就不可逆。
- 事务已提交 → 消息写进
[cap].[Published]→ CAP 后台线程异步发送 → 发送失败 → 按FailedRetryCount重试 - 事务未提交(如抛异常)→
[cap].[Published]不写入 → CAP 根本看不到这条消息 → 不触发任何重试 - 所以重试失败达到阈值后,
FailedThresholdCallback收到的是“已持久化但发不出去”的消息,不是“未执行的业务逻辑” - 线上建议设
FailedRetryCount = 5~10,FailedRetryInterval = 30(秒),避免压垮 MQ
最易忽略的一点:CAP 的事务绑定只发生在调用 PublishAsync 的那一刻。如果你在事务内先查数据、再判断条件、最后才发消息,而这个判断逻辑本身有 bug 导致不该发却发了,CAP 挡不住——它不校验业务语义,只保事务边界。真正的最终一致性,得靠下游消费端的幂等 + 补偿设计兜底。


















