具体例子求解,异步后数据弱导致的决策失误问题

最近看了 cqrs和lamx.
采用 富领域模型 ,event source. 异步操作.
这个有个bad case, 我不知道采用什么方案解决.
钱有100,两口子之前有约定要剩下90. 老公看到有100,花10元,花完以后因为事件异步,数据不一致,此时老婆刷新页面也看到100,再花10元. 最终所有异步事件都处理完之后最终是80元. 不符合用户的预期.
这个问题如何解决?

你这个案例是数据一致性问题。数据一致性分强一致性和弱一致性。你现在要求的是强一致性,而异步属于弱一致性,当然解决不了强一致性。

DDD聚合根实体使用行为守护状态,能够保证状态变化符合强一致性。所以,这里建立一个聚合根Money,里面封装的是余额100元。通过Money的add和remove方法保证余额的高一致性。

正因为有了DDD聚合根将特殊的高一致性对付了,缺省的其他普遍的才可以使用异步这种弱一致性方法,就如同足球比赛,将C罗这个特殊人才看住了,其他就好办了,擒贼先擒王。

函数编程也是这个道理。将状态可变看住了,其他就可以都要用不变性。两者都是相通的。

2014-06-18 09:35 "@banq"的内容
通过Money的add和remove方法保证余额的高一致性 ...

能结合上面的例子具体说说如何保证强一致性么?
上面例子中两个都是remove.
万分感谢.
还有一个问题,先在这里问了.之后整理成专门的帖子.
cqrs 架构中 多个事务生成的领域event 谁先执行谁后执行?
两个实体Teacher ,Student
事务1 产生事件, TeacherEvent1,StudentEvent1,事务2 产生事件 TeacherEvent2, StudentEvent2.
如何记录这些事件,是否需要 把N个领域事件和事务Id绑定,以便之后按事务顺序执行这些事件.确保整体事件的成功或者失败时,整体事件事务回滚.确保数据的正确性?

[该贴被sinaID87521于2014-06-18 23:49修改过]

2014-06-18 23:41 "@sinaID87521"的内容
能结合上面的例子具体说说如何保证强一致性么 ...

可以参考我的什么是流式思维中案例,类似其中TV的ChannelState 。这里大概用代码重新写一下,


class Money{
private AtomicInteger balance = ..;

public void add(int v){
balance.addAndGet(v);
}

public void remove(int v){
balance.decrementAndGet(v);
}

}

这里使用Java的并发控制AtomicInteger,实际上强一致性都要通过锁实现,比如使用数据库表锁,事务锁等,以保证每个时刻只有一个线程修改状态,优雅是像LMA使用Disruptor使用单线程访问。Jdon框架使用Disruptor支持上述修改。代码如下:

class Money{
private int balance = ..;

@[author][author]OnCommand[/author][/author]("addCommand")
public void add(int v){
balance=balance +v;
}

@[author][author]OnCommand[/author][/author](
"removeCommand")
public void remove(int v){
balance=balance -v;
}

}

通过Jdon框架提供的元注释来保证这两个方法是单线程访问,这样,如果方法体内部复杂步骤,无需synchronized 或JTA的事务机制。



[该贴被banq于2014-06-19 06:47修改过]

2014-06-19 06:46 "@banq"的内容
单线程访问 ...

上面代码演示的是内存并发编程; 引申到事件流式执行. 只是解决并发导致的原子问题, 但没有解决上述的业务问题.
上述方案最终钱还是80元,没有避免异步事件框架下妻子扣钱成功的问题.

昨天咨询了下我们的高T.这个场景在国外银行很常见,国外有夫妻卡.,他认为是这样实现的:
先说说不用异步事件框架实现是如何保持一致性的.
丈夫显示100元, 进行消费,向后端 传递聚合跟的objectVersion 1, 正常扣钱10. 传递聚合跟的objectVersion值+1变为2.
妻子由于也是显示100元,进行消费,所以递聚合跟的objectVersion也是1, 在你的调用方法前会做业务校验,由于版本号objectVersion不匹配,妻子会得到错误.页面重新刷新,显示90元. 妻子就不会继续消费了.

再说说异步事件架构下:
丈夫显示100元,进行消费,. 变成事件1存储. 正常返回给前端.但不是真的出钱. 而是告知用户"后台正在处理,请稍等".
妻子由于也是显示100元,进行消费,同样变成事件2存储.正常返回给前端.同样不出钱,告知用户"后台正在处理,请稍等".
这时候 列里有两个事务产生的事件. 顺序看上面两个事务commit的次序. 加时丈夫事件1先被执行. 检查聚合跟的objectVersion,成功.通过一个新接口告知给前端钱已扣除.妻子的事件2后执行,检查聚合跟的objectVersion,失败.通过一个新接口告知给前端钱无法扣除,无法消费.
同步架构采用异步架构,整个业务流程都变了. 需要新增加一个接口.还有就是.异步事件执行可能因为网络等原因产生偶然Exception.需要有重试的机制.
总结: 同步流程采用异步后. 对于开发者和产品经理来说都更复杂了.
对于开发者来说: 需要1.保存事件 2. 重试机制 3. 新增加一个接口(即异步框架里的回调接口) 4.告知产品经理流程已变成异步化.
对产品经理来说: 需要把原来的一个同步流程思考为多个流程.
那么到底是谁决定谁呢?
开发和产品经理是互相决定,影响的. 一方面当产品经理处于用户体验的角度,可能会主动把一个同步流程,拆成多个异步流程.增加步骤和接口. 这时候开发者坑并不愿意,因为增加了工作量. 另一方面开发者处于性能,并发量的考虑,可能会把PM思考的一个同步调用改成异步. 这时候产品经理需要知道要有新的页面提醒用户"后台处理中", 流程已变.
[该贴被sinaID87521于2014-06-19 11:37修改过]

2014-06-19 11:35 "@sinaID87521"的内容
只是解决并发导致的原子问题, 但没有解决上述的业务问题 ...

可能我一下把结果写出来,造成你的理解错误。你的版本校验方式其实也是一种对象锁,类似synchronized, 这种锁效率也很低,还不如Java 8的并发编程提供的读写锁呢,更无法和Disruptor 单写原则

我再详细写一下代码:



class Money{
private int balance = ..;

@ OnCommand("removeCommand")
public void remove(int v){
if (balance-10)<90{
//如果 余额小于90就返回错误
}
balance=balance -v;
}

}

丈夫调用remove扣除10元后,妻子哪怕同时调用remove,但是进入remove方法内部只能允许一个线程操作,因此总是有一个先后操作,妻子如果是最后操作remove,根据条件预判断不符合业务规则约束(余额不能小于90)。

2014-06-19 13:03 "@banq"的内容
if (balance-10)<90{ ...

这个把两夫妻之前约定的90硬编码显然是不合理的.每个夫妻的约定都不用.
我依然认为上面我给的解决方案算是比较完美的同步异步化的解决方案了.
重点强调下:
我这里的事件是把一个前端http请求(用户要消费多少的)的参数先保存成事件. (这里有消费数目,目前的钱, objectVersion) 然后立马返回,异步启动一个Spring @Transaction事务去处理这个.
可能和领域事件稍有不同.


[该贴被sinaID87521于2014-06-20 13:38修改过]

2014-06-20 13:36 "@sinaID87521"的内容
这个把两夫妻之前约定的90硬编码显然是不合理的.每个夫妻的约定都不用 ...

晕倒,if (balance-10)<90 代表的是业务规则,等同于if( 业务规则为真 )。业务规则属于模型的一部分,或者是核心部分,也就是每个夫妻的约束,我这里是伪编码。

你的版本校验方法类似同步锁,类似于数据库锁,当然可以用,但是并发性能不是非常好,是堵塞式的,既然我们在这里谈CQRS,谈事件,当然是追求非堵塞的。

这个业务规则所有人都是不同的,是无限的.不可能这么来解决.

2014-06-20 14:22 "@banq"的内容
类似同步锁,类似于数据库锁 ...

其实不并没有锁. 是乐观锁. 乐观锁并没有真正的锁行.
数据库只有在写的时候才加排它锁.而且我上面提到的那个解决方案.通过事件把夫妻两的对Money的并发写锁的问题.变成了异步顺序事件流写.只不过妻子的事件被执行的时候会抛错,消费不成功.不管成功不成功都会通知给前端调用者.告知他下一步怎么操作.



[该贴被sinaID87521于2014-06-20 15:52修改过]

2014-06-20 15:52 "@sinaID87521"的内容
其实不并没有锁. 是乐观锁. 乐观锁并没有真正的锁行 ...

乐观锁当然也是锁,看这篇文章:http://www.jdon.com/performance/singlewriter.html

英文原文:http://en.jdon.com/single-writer-principle.html

英文原始地址:http://www.javacodegeeks.com/2012/08/single-writer-principle.html

因为你开篇提到LMAX,所以,我应该将LMAX disruptor革命性特点告诉你。

再提供一个事务与锁方面老文章:http://www.jdon.com/45728

[该贴被banq于2014-06-20 16:05修改过]

2014-06-20 16:03 "@banq"的内容
乐观锁当然也是锁 ...

恩.这里面乐观锁的概念是失败后会循环. 你的事务可能是读提交. 类似cpu的CAS.
业务场景下.用乐观锁概念并不会重试. 事务处于可重复读. 而是认为用户提交的数据是过时的.用户决策的时候数据已经过期了. 需要通过接口把这个结果告知调用者.
我这里乐观锁的概念外扩了, 或许用ObjectVersion的check 概念更合理.
你推荐的这篇文章 http://www.jdon.com/45728 我看看,好像也和事务相关.

[该贴被sinaID87521于2014-06-20 16:14修改过]
[该贴被sinaID87521于2014-06-20 16:19修改过]

2014-06-20 16:03 "@banq"的内容
再提供一个事务与锁方面老文章:http://www.jdon.com/45728
...

这篇文章里的那段代码.. 完全看不懂

求解楼主,看不太懂
1. asynchronization是啥关键字
2. 只有 setAupper setAlower为什么能调用 a.setUpper
3. setAupper为啥调用a.setLower ?
4. a是哪里来的.
最后,我觉得这个永远不可能出现(4,3) 的情况.
可能第3点我没理解,有特别的含义.
[该贴被sinaID87521于2014-06-20 18:02修改过]

2014-06-19 11:35 "@sinaID87521"的内容
昨天咨询了下我们的高T.这个场景在国外银行很常见,国外有夫妻卡.,他认为是这样实现的:
先说说不用异步事件框架实现是如何保持一致性的.
丈夫显示100元, 进行消费,向后端 传递聚合跟的objectVersion 1, 正常扣钱10. 传 ...

冲突次数少干脆用原子类型不就好了,没必要把依赖放到外面。

冲突次数多的话,这方式非常影响消费体验。

夫妻卡对银行来说是特殊情况——只有少许人,而且周期长。这种Token方式更新对于一般卡来说简直就是灾难。为夫妻卡专门开发一个独立系统?我觉得挺可笑的。