首页   注册   登录
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX  ›  Java

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

  •  
  •   ayonel · 28 天前 · 2219 次点击

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

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

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

    思路:

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

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

    44 回复  |  直到 2019-11-13 14:49:12 +08:00
        1
    qiyuey   28 天前
    放弃分布式事务,使用幂等重试
        2
    ayonel   28 天前
    这种做法也没有使用分布式事务。老哥看下这种 trick 做法可以保证一致性吗
        3
    reus   28 天前
    成功插入 neo4j 之后出现异常,mysql 回滚,而 neo4j 没有,不就不一致了?
        4
    cmingxu   28 天前   ♥ 1
    感觉能完全说明白这个问题得一本厚书, 期待惊艳回答。
        5
    iIli1iIliIllLiL   28 天前
    3L+1
        6
    ayonel   28 天前
    @reus @iIli1iIliIllLiL 成功插入 neo4j 之后都没有其他逻辑了,还能发生异常吗?
        7
    reus   28 天前
    @ayonel 插入之后进程挂了呢?
        8
    lhx2008   28 天前 via Android
    @ayonel 能,网络问题,如果插完 neo4j 的瞬间拔了网线,则无法确定 neo4j 是否插入成功。
        9
    lhx2008   28 天前 via Android
    如果都在一台机器上面其实问题不大,除非进程自己挂了
        10
    Yuicon   28 天前
    在回滚的时候添加逻辑删除 neo4j 多余数据
        11
    optional   28 天前
    关键是你下面还有代码,,如果你下面的代码抛异常了呢?
        12
    ysweics   28 天前
    用 MQ 做最终一致性吧
        13
    lhx2008   28 天前 via Android
    比较简单的方法就是异步同步,先把资料持久化一份( kafka 等),然后开一个后台程序去读,然后写到 neo4j,当然,代价是没有强一致性。不过强一致性不是那么容易搞的。
        14
    ebony0319   28 天前 via Android
    用最终一致性和事务补偿。
        15
    ayonel   28 天前
    @optional 下面可以认为没代码了
        16
    ayonel   28 天前
    @reus 这个概率太小了吧?还有其他 case 能造成数据不一致吗?
        17
    wangyzj   28 天前
    tcc
        18
    lhx2008   28 天前 via Android
    强一致一般可以用 2PC 两阶段提交,两个被调用方要保证落盘一份并能保证插入,然后再返回给调用方 2 个 OK,调用方再发提交给被调用方。如果调用方挂掉,也必须快速重启后跟上进度,否则就会卡住等人工处理。
        19
    ayonel   28 天前
    @ysweics
    @lhx2008 感觉是个比较简单的业务逻辑,引入 MQ 有些大材小用。大神能指出我题目中这种写法的弊端吗?
        20
    ayonel   28 天前
    @wangyzj 不想引入分布式事务。单纯看我题中的解法会产生什么问题
        21
    reus   28 天前
    @ayonel 如果 mysql 提交失败,同样可能导致 neo4j 有数据,mysql 没数据。这个概率不小了吧?

    反正你这样做,就是错的。
        22
    ayonel   28 天前
    @reus mysql 先与 neo4j 操作的。如果 mysql 失败,应该直接回滚了,不会执行后面的 neo4j 逻辑。是这样吗?
        23
    lhx2008   28 天前 via Android
    如果 neo4j 插入到一半拋异常,那么必须确保回滚到之前的状态再重新抛出,如果 neo4j 挂掉的话,也要确保这一点。
        24
    lhx2008   28 天前 via Android
    还是我说的,由于网络原因,可能会出现插入成功,但是这边不知道的情况。
        25
    VensonEEE   28 天前
    两个不同的处理单元,理论上不可能强一致性。不过可以通过其他方式实现,就看你可以承受系统的性能损耗了。其实数据库存储也有很多步骤,没见过谁去纠结这个。
    最简单的是包装这个单元,关闭事务,弄完之后做完整性检查,逻辑检查结果返回最终,否则删除。
        26
    lazyfighter   28 天前
    这应该拆分开吧,放在 MQ 异步处理更好,做好消息的重复消费,同时还需要考虑消息丢失的情况
        27
    reus   28 天前   ♥ 1
    @ayonel
    我说的是 mysql 的提交,不是 mysql 的查询。
    neo4j 执行完,mysql 才提交,如果提交出错,mysql 是会回滚,但是 neo4j 不会啊,它已经执行完了。
        28
    KentY   28 天前   ♥ 4
    假设
    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 了.
        29
    wangyzj   28 天前
    先做好幂等的 api
    然后 delete api 前面加上一个 mq
    post 之前实在不行加个用户级别 nx 锁
        30
    1ffree   28 天前
    neo4j 成功,mysql 提交失败 楼上讲的很清楚了
    加一点 通常事务内部不会加其他的远程调用( neo4j) 如果通信超时 占用 mysql 连接池 涉及的 mysql 操作不可用 可能把整个应用拖垮
        31
    ayonel   28 天前
    @lhx2008 neo4j 的一条 cypher 可以认为是原子的,不会插到一半
        32
    ayonel   28 天前
    @KentY 感谢大佬,了解了,手动 100 个赞
        33
    ayonel   28 天前
    @wangyzj 思路太过跳跃,抱歉没明白您的回答
        34
    ayonel   28 天前
    @1ffree 是的,这种做法其实强一致性,会降低系统吞吐
        35
    Raymon111111   28 天前
    分布式事务有个很大的毛病是性能会有瓶颈

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

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

    最高级的:
    采用 CQRS+事件溯源。业务通过事件的形式写入(持久化的)消息队列。需要单独的程序根据事件定期增量式刷新物化视图(主要的)、更新某些(可能更加 denormalized 的)用于加速查询的数据库(例如 redis,neo4j )。
    缺点:实现复杂,并且业务多多少少需要一些强一致性来简化开发。
        41
    codeyung   28 天前
    不是核心业务就 mq 异步 或这半异步 自己开个线程池写顺便发个 mq 写 做个防重 核心你就要做最终一致 用代码或者组件写你核心逻辑的 然后做业务逻辑的验证 Confirm 和 如果业务失败的 Cancel 一半情况就是自己写验证 看看 MQ 事务 类似的道理
        42
    codeyung   28 天前
    强一致性能 emm
        43
    ayonel   27 天前
    @codeyung 感谢解惑,大概理解了
        44
    iIli1iIliIllLiL   27 天前
    @ayonel 会,只要你是双写,就会有这个问题,相同的场景还有缓存数据库双写一致性的 wen ti,简单来说你这个就是分布式事务的问题。
    关于   ·   FAQ   ·   API   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   975 人在线   最高记录 5043   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.3 · 24ms · UTC 20:17 · PVG 04:17 · LAX 12:17 · JFK 15:17
    ♥ Do have faith in what you're doing.