你说的是正确的。确实,你说的情况数据库不能保证一致性。但这个问题的根本原因在于使用了中间变量,如果直接使用 update amount=amount+10 这样就一致性问题的。毕竟写的时候都会加X锁。又因为加锁会强制触发当前读,所以amount是当前最新提交的值(已测试证明),因此是一致的。我们多数业务有一致性问题不是数据库本身的问题,是因为我们使用了中间变量,加减操作是在java代码里面完成的,无法触发当前读。因此我们才使用乐观锁 或者 select for update 来解决问题。
但需要注意的是,我们多数情况下只会给修改额度等操作加 for update. 因为 for update 解决的是无法当前读的问题。而seata AT中描述的问题是需要select for update 是来解决脏读问题,也就是读未提交问题。这两者虽然都是用 for update,但它们解决的问题是不一样的。也因为后者是解决脏读问题,for update 操作必须应用在所有的读操作上。而不是某一个或多个读操作。这是有本质差异的。效率也是完全不同。一个只是部分使用,一个是全方位覆盖。
我分析这个问题的思路是这样的。我们的单体应用本身也会负载均衡,也是会出现多个应用实例访问同一个数据库实例。
多个应用都在同一个数据库实例中进行事务的隔离,隔离性靠 单个数据库实例维护,没问题。那么,我们进一步分析 负载均衡 + 分库分表 时的情况,此时是多个app实例和多个数据实例。这里只以纵向分表来说明
(这里假设 Global 是全局事务, APP是应用, Tx是不同数据库的事务,d是库,t 是表)
其实这个情况下就已经出现了分布式事务问题了。之所以我们没有感知是因为事务呈现是这样的
Global { App1 -> Tx1 (d1.t1, d1.t2, d1.t3) -> App2 -> Tx2 (d2.t4, d2.t5, d2.t6)}
如上,每个应用实例 开启的事务访问到的数据库实例都是隔离的。因此Tx1 和Tx2 只需要保证同时提交,同时回滚就行。业务上没有影响。这个多阶段提交的问题是一样的。也很好理解。
seata描述的脏写和脏读问题体现出来的是这样:
Global { app1 -> tx1 (d1.t1, d1.t2, d2.t3) -> tx2(d2.t3, d2.t4, d2.t5) }
这个情况下,本地事务 tx1 涉及到 d1, d2个数据库实例, tx2本地事务 也涉及到 d1 和 d2 数据库实例。 tx1 和 tx2 已经不能仅靠本地数据库实例来维护事务,而需要整个全局事务来协调,这样就会遇到 脏读,脏写问题。而seata协调这个问题就会出现我最初描述的那两个问题。我认为这样的协调效果不太理想。因此,我才提到是不是可以建立规范。不同微服务必须使用不同数据库。这样就能保证分布式事务其实和分库分表遇到的问题是等价的。都只需要解决一个全局事务中的多个本地事务能否同时提交或者同时回滚,不需要关注多个本地事务时间会不会有冲突。
简单总结一下就是我个人认为:不同微服务必须使用不同库可以绕开 一个全局事务中的多个本地事务需要进行读写隔离
。因为只要做到这一点,一个全局事务中的多个本地事务本身就是相互隔离的。不需要seata AT 去帮助进行隔离。seataAT 要做的事情仅仅是保证一个全局事务中的多个本地事务同时提交或者回滚
这一件事。
我不知道您是否能理解我这个说法。其实,我觉得您一直认为我说的这个问题是错误的,是因为seata描述的脏写和脏读问题其实很少遇到。我们项目中其实大多都正好是遵循我说的这个规范的。