V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐工具
RoboMongo
推荐书目
50 Tips and Tricks for MongoDB Developers
Related Blogs
Snail in a Turtleneck
tabris17
V2EX  ›  MongoDB

两阶段提交遇到无法恢复也无法回滚的事务该怎么办?

  •  
  •   tabris17 · 2016-02-15 10:17:49 +08:00 · 5707 次点击
    这是一个创建于 2987 天前的主题,其中的信息可能已经有所发展或是发生改变。
    以 A 账户向 B 相互转账为例。
    在账户余额有限制的情况下,比如余额必须大于 0 且小于 100 。
    当事务处理到 pedding 状态,完成了 A 账户的扣款,向 B 账户打款时,发现 B 帐户余额会超过 100 元,打款失败,然后回滚事务,将 A 账户之前的扣款还回时发现, A 账户余额已经发生变动,还款会导致余额超过 100 。
    如果采取先打款再扣款的方式,则会碰到余额不足的情况。

    碰到这种情况,该如何处理?事务挂起,然后手工干预?
    27 条回复    2016-02-16 12:41:15 +08:00
    zacard
        1
    zacard  
       2016-02-15 10:31:05 +08:00
    A 打款给 B 前就应该检查 B 是否超过上限,而不是先扣款
    tabris17
        2
    tabris17  
    OP
       2016-02-15 10:39:45 +08:00
    @zacard 数据无法同步,这种检查意义不大,你无法保证『检查 B 账户余额』和『给 B 账户打款』的两个操作之间, B 账户余额不发生变化
    wy315700
        3
    wy315700  
       2016-02-15 10:46:19 +08:00
    加锁
    pelloz
        4
    pelloz  
       2016-02-15 10:49:22 +08:00
    排它锁?
    noli
        5
    noli  
       2016-02-15 10:49:59 +08:00   ❤️ 1
    “ A 向 B 转账”(记为 T1 )的事务完成或者中止之前能够修改 A 帐户余额?
    我不确定 T1 还能不能叫事务……
    incompatible
        6
    incompatible  
       2016-02-15 10:51:53 +08:00 via iPhone
    @tabris17
    当然可以保证,给 A 账户和 B 账户加事务锁就可以。

    另外,多问一句:从你提到“两阶段提交”来推断, A 账户和 B 账户不在同一个数据源下?所以用了分布式事务?
    EPr2hh6LADQWqRVH
        7
    EPr2hh6LADQWqRVH  
       2016-02-15 10:53:27 +08:00
    Mongodb 不支持事务这件事写在 Cons 里面可不是白写的
    请选用支持事务的数据库比如 postgresql , 或者自己实现软事务
    EPr2hh6LADQWqRVH
        8
    EPr2hh6LADQWqRVH  
       2016-02-15 10:56:21 +08:00
    tabris17
        9
    tabris17  
    OP
       2016-02-15 10:56:51 +08:00
    能想到的是,在账户的 document 上增加一个锁标志字段,在对 document 操作前都手动检查一下锁标志位
    tabris17
        10
    tabris17  
    OP
       2016-02-15 11:14:02 +08:00
    zacard
        11
    zacard  
       2016-02-15 11:24:20 +08:00
    @tabris17 所有对余额的操作都检查啊。。。可以借用 mongodb 的原子增减做检查
    tabris17
        12
    tabris17  
    OP
       2016-02-15 11:30:59 +08:00
    @zacard

    没用。

    A 向 B 转账事务操作:
    1. 检查 B 账户余额接受金额后是否会超出 100 的限额;
    2. 检查通过则对 A 账户进行扣款;
    3. 向 B 账户打款。

    然而 1 和 3 操作之间,可能有其他的操作(比如 B 账户充值)会对 B 账户余额进行操作,第一步的检查白做
    gy911201
        13
    gy911201  
       2016-02-15 11:33:41 +08:00
    @tabris17 排他锁,在转账事务完成 /超时之前,阻塞其他所有的针对两个账户余额变动的操作
    tabris17
        14
    tabris17  
    OP
       2016-02-15 11:37:44 +08:00
    @gy911201 阻塞的排他锁很麻烦,由于事务要分别获得 document A 和 document B 两个文档的锁,所以会造成死锁,还要自己实现死锁检测
    Mirana
        15
    Mirana  
       2016-02-15 11:38:59 +08:00
    A 预提交了之后是会被锁住的
    incompatible
        16
    incompatible  
       2016-02-15 11:50:13 +08:00 via iPhone
    @tabris17
    抱歉,一开始没有看到是在 mongodb 节点下。
    我的建议是: mongodb 完全不是为这种场景设计的,请考虑改用 mysql 或者 postgresql 来实现你的账户余额功能。这些支持 acid 的数据库很容易就可以支持你主贴中的业务场景,完全无需自己实现两阶段提交(事实上两阶段提交本来也不是用来干这个的,它通常用来实现多数据源的分布式事务)
    tabris17
        17
    tabris17  
    OP
       2016-02-15 11:56:05 +08:00
    @incompatible

    确实,这个称作模拟事务日志比较恰当。
    如果 mongo 要实现事务,除了模拟事务日志外,为了避免脏读,不可重复读,幻影行,还要实现 MVCC ,为了数据一致性,还要实现行(文档)锁、页锁、集合锁……
    li24361
        18
    li24361  
       2016-02-15 13:14:01 +08:00
    A->B 操作没完成之前, A 不能修改,否则不叫事务
    breeswish
        19
    breeswish  
       2016-02-15 13:44:46 +08:00
    就这个例子而言,可以在 pending 后进行检查,如果检查失败则回滚( MongoDB 文档 Rollback Operations 下的 Transactions in Pending State )。

    你担心检查完和修改记录之间有其他操作,这个和两阶段递交没关系,你想做的是确保一次只有一个事务占用资源。可以实现一个锁。对于这个问题而言,可以在查询中增加条件解决(显然能解决的问题不如锁那么多):

    db.accounts.update(
    { _id: t.destination, pendingTransactions: { $ne: t._id }, balance: { $lt: 100 - t.value} },
    { $inc: { balance: t.value }, $push: { pendingTransactions: t._id } }
    )

    注意增加了一个 balance: { $lt: 100 - t.value} 条件。 update 失败( 0 affected )直接 rollback 即可
    breeswish
        20
    breeswish  
       2016-02-15 13:46:11 +08:00
    (补:纯 MongoDB 的话可以利用 Tailable Cursor 实现锁
    hantsy
        21
    hantsy  
       2016-02-15 13:47:22 +08:00
    看成 Two phase transcation 了。。。
    zacard
        22
    zacard  
       2016-02-15 13:49:37 +08:00
    @tabris17 1 和 3 操作之间如果有对余额修改(充值),要么会检查出此次充值失败,要么就会检查出这次转账失败。原子性来保证这一点。
    tabris17
        23
    tabris17  
    OP
       2016-02-15 13:56:59 +08:00
    @zacard mongo 原子性之针对单个文档的操作,这里涉及到事务隔离
    palmers
        24
    palmers  
       2016-02-15 15:23:01 +08:00
    能不能使用第三方账户转账呢? 优先检查 B 账户余额 满足立即转账否则直接退回, 第三方账户肯定可以控制不会在转账期余额被操作 这样可以吗?
    tianice
        25
    tianice  
       2016-02-15 22:29:30 +08:00
    mongo 支持原子操作,并不支持事务。也不能叫两阶段提交,两阶段提交数据库在第一次预提交成功之后,必须保证事务最终能提交或回滚。不能说预提交成功之后,让你回滚你滚不回去,让你提交提交不了,不能出现这两种情况,否则预提交没有意义。
    tianice
        26
    tianice  
       2016-02-15 22:30:58 +08:00
    涉及到钱的业务还是用关系型数据库吧,至少要保证事务一致
    future0906
        27
    future0906  
       2016-02-16 12:41:15 +08:00
    @tabris17
    先冻结 A 100 块,这一百块不能交易,转出;转账给 B ,不过成功的话,直接扣除,不成功解除冻结。

    银行信用卡也是采用类似模型(预授权)
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   2872 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 04:01 · PVG 12:01 · LAX 21:01 · JFK 00:01
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.