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

2016-02-15 10:17:49 +08:00
 tabris17
以 A 账户向 B 相互转账为例。
在账户余额有限制的情况下,比如余额必须大于 0 且小于 100 。
当事务处理到 pedding 状态,完成了 A 账户的扣款,向 B 账户打款时,发现 B 帐户余额会超过 100 元,打款失败,然后回滚事务,将 A 账户之前的扣款还回时发现, A 账户余额已经发生变动,还款会导致余额超过 100 。
如果采取先打款再扣款的方式,则会碰到余额不足的情况。

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

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

没用。

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

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

确实,这个称作模拟事务日志比较恰当。
如果 mongo 要实现事务,除了模拟事务日志外,为了避免脏读,不可重复读,幻影行,还要实现 MVCC ,为了数据一致性,还要实现行(文档)锁、页锁、集合锁……
li24361
2016-02-15 13:14:01 +08:00
A->B 操作没完成之前, A 不能修改,否则不叫事务
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
2016-02-15 13:46:11 +08:00
(补:纯 MongoDB 的话可以利用 Tailable Cursor 实现锁

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

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

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

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

© 2021 V2EX