老哥们,周二好。问一个跨库保证一致性的问题

2019-11-12 15:53:30 +08:00
 ayonel

业务背景: 有一份数据,需要双写到两份存储( Mysql、Neo4j )

项目基于 springboot.我已经配置了 mysql 数据源的事务管理器,neo4j 没有配置。在不使用分布式事务的前提下,以下这种做法能否保证两份数据的一致性:

@Transactional
public void doService() {
	// 第一步,插 mysql
	insertMysql();
    // 第二步,插 neo4j
    insertNeo4j();
}

思路:

  1. 如果插 mysql 出现异常,此时 mysql 事务直接回滚,neo4j 还没来得及插入。所以最终表现是:两个引擎数据都没写入,保持一致。
  2. 如果插入 neo4j 出现异常,此时 mysql 事务也会回滚。而 neo4j 由于插入失败,也没有数据。所以最终表现是:两个引擎数据都没写入,保持一致。

看起来,这样简单的做法能保证一致性,老哥们能指出这种做法有啥坑吗?( PS:假设 mysql、neo4j 都是一条操作语句)

5707 次点击
所在节点    Java
45 条回复
reus
2019-11-12 17:08:23 +08:00
@ayonel 如果 mysql 提交失败,同样可能导致 neo4j 有数据,mysql 没数据。这个概率不小了吧?

反正你这样做,就是错的。
ayonel
2019-11-12 17:12:09 +08:00
@reus mysql 先与 neo4j 操作的。如果 mysql 失败,应该直接回滚了,不会执行后面的 neo4j 逻辑。是这样吗?
lhx2008
2019-11-12 17:14:46 +08:00
如果 neo4j 插入到一半拋异常,那么必须确保回滚到之前的状态再重新抛出,如果 neo4j 挂掉的话,也要确保这一点。
lhx2008
2019-11-12 17:16:04 +08:00
还是我说的,由于网络原因,可能会出现插入成功,但是这边不知道的情况。
VensonEEE
2019-11-12 17:16:53 +08:00
两个不同的处理单元,理论上不可能强一致性。不过可以通过其他方式实现,就看你可以承受系统的性能损耗了。其实数据库存储也有很多步骤,没见过谁去纠结这个。
最简单的是包装这个单元,关闭事务,弄完之后做完整性检查,逻辑检查结果返回最终,否则删除。
lazyfighter
2019-11-12 17:19:20 +08:00
这应该拆分开吧,放在 MQ 异步处理更好,做好消息的重复消费,同时还需要考虑消息丢失的情况
reus
2019-11-12 17:19:39 +08:00
@ayonel
我说的是 mysql 的提交,不是 mysql 的查询。
neo4j 执行完,mysql 才提交,如果提交出错,mysql 是会回滚,但是 neo4j 不会啊,它已经执行完了。
KentY
2019-11-12 17:25:01 +08:00
假设
1.你的"一份数据"就是单一的一条记录
2.你的这个 method 就这两行
3. 所有网络不会断, 机器不死机

一样会有问题.
你用了 @Transactional, 也就是说 mysql 那个 insert 是有 transaction scope 的, 但是 transcation commit 会在整个 method 运行完发生. 但是 insertMysql()没 exception 不代表就会成功 commit, 比如当 flush()后有 key 的冲突, 或者 commit 的时候有 lock 等等导致 commit 失败. 可是你的 neoinsert 不在这个 txn 里, 已经"committed", 这样你就有了 inconsistent data.

如果你以"几率很小"来判断, 就没必要考虑这些了, 因为 exception 之所以叫 exception 就是因为几率小, "异常"么. 如果都是"常态"就没必要做 exception handling 了.
wangyzj
2019-11-12 17:25:30 +08:00
先做好幂等的 api
然后 delete api 前面加上一个 mq
post 之前实在不行加个用户级别 nx 锁
1ffree
2019-11-12 18:06:06 +08:00
neo4j 成功,mysql 提交失败 楼上讲的很清楚了
加一点 通常事务内部不会加其他的远程调用( neo4j) 如果通信超时 占用 mysql 连接池 涉及的 mysql 操作不可用 可能把整个应用拖垮
ayonel
2019-11-12 18:10:54 +08:00
@lhx2008 neo4j 的一条 cypher 可以认为是原子的,不会插到一半
ayonel
2019-11-12 18:33:44 +08:00
@KentY 感谢大佬,了解了,手动 100 个赞
ayonel
2019-11-12 18:35:51 +08:00
@wangyzj 思路太过跳跃,抱歉没明白您的回答
ayonel
2019-11-12 18:36:21 +08:00
@1ffree 是的,这种做法其实强一致性,会降低系统吞吐
Raymon111111
2019-11-12 19:29:35 +08:00
分布式事务有个很大的毛病是性能会有瓶颈

如果业务可接受, 用 MQ 保证最终一致会好一点
optional
2019-11-12 19:36:34 +08:00
@ayonel 下面没代码了。。。。没有讨论意义。
如果你确定下面没有了,最好也来一个 requires_new
wysnylc
2019-11-12 20:48:27 +08:00
别想太多,强一致性做不了的
目前的分布式事务解决方案都在出现异常时回滚保证重复执行的幂等
你要是能解决,可以开讲座,不开玩笑
ayonel
2019-11-12 20:55:00 +08:00
@wysnylc 只要数据源支持 XA 协议,我理解可以做到强一致吧。不过对于我的业务场景,最终一致也能接受。我关注的是如何『最简单地』以及『尽可能地』保障一致性
codeyung
2019-11-12 21:26:35 +08:00
网络中断,机器挂了,Neo4j 写成成功了你抛异常 mysql 回滚数据不一致
如果业务可以接收像楼上讲的用 MQ。
都是 MySQL 还好 ebay 之前有一套解决方案。
如果核心业务就需要 做 TCC 自己写业务验证。
MQ 做最终一致性能高,有些假提交对连接性能压力交大。
还有你这样在 mysql 的事务里调用 Neo4j,如果 Neo4j 延迟交大 事务提交缓慢 连接池打满啥的 都有可能
有过教训,LZ 如果是小业务那当然能用就行 但是量上来了可能就因为你一个这个操作 tps 降低吞吐都低了
crclz
2019-11-12 21:27:02 +08:00
普通解决方案:
表的字段设置一个 UpdatedAt,一个 CreatedAt。写一个脚本,定期( maybe 0.1s )将新插入的记录或者修改的记录刷写到在 neo4j 中。同时在 neo4j 中记录这张表上次刷到哪个时间戳了,下次就从这个开始。注意时间戳的精度问题:java,c#的精度会和数据库中的不一样,会出大问题。所以就应该用 int64 存 UtcTicks。

进阶的:
这其实就是一主多从中的复制。看看有没有一个实用性广的解决方案,避免自己写过多的复制逻辑。

最高级的:
采用 CQRS+事件溯源。业务通过事件的形式写入(持久化的)消息队列。需要单独的程序根据事件定期增量式刷新物化视图(主要的)、更新某些(可能更加 denormalized 的)用于加速查询的数据库(例如 redis,neo4j )。
缺点:实现复杂,并且业务多多少少需要一些强一致性来简化开发。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/618835

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX