为什么Actor模型是高并发事务的终极解决方案?

首先看看道友提出的一个问题:
用户甲的操作
1.开始事务
2.访问表A
3.访问表B
4.提交事务
乙用户在操作
1.开始事务
2.访问表B
3.访问表A
4.提交事务

如果甲用户和乙用户的两个事务同时发生,甲事务锁住了表A未释放(因为整个事务未完成),正在准备访问B表,而乙事务锁住了表B未释放(因为整个事务未完成),正在准备访问A表,可是A表被甲事务锁住了,等甲事务释放,而甲事务真正等待乙事务释放B表,陷入了无限等待,也就是死锁Dead Lock。

也有道友使用多线程来模拟存储过程:http://www.jdon.com/45727,每个线程里开启一个事务,类似上述问题也会出现死锁。

问题出在哪里?

是我们的思路方向出现问题:

其实无论是使用数据库锁 还是多线程,这里有一个共同思路,就是将数据喂给线程,就如同计算机是一套加工流水线,数据作为原材料投入这个流水线的开始,流水线出来后就是成品,这套模式的前提是数据是被动的,自身不复杂,没有自身业务逻辑要求。适合大数据处理或互联网网站应用等等。

但是如果数据自身要求有严格的一致性,也就是事务机制,数据就不能被动被加工,要让数据自己有行为能力保护实现自己的一致性,就像孩子小的时候可以任由爸妈怎么照顾关心都可以,但是如果孩子长大有自己的思想和要求,他就可能不喜欢被爸妈照顾,他要求自己通过行动实现自己的要求。

数据也是如此。

只有我们改变思路,让数据自己有行为维护自己的一致性,才能真正安全实现真正的事务。

数据+行为=对象,有人问了,对象不是也要被线程调用吗?

例如下述代码,因为对象的行为要被线程调用,我们要使用同步锁synchronized :


public class A {
private volatile int lower, upper; //两个状态值
public int getLower() { return lower; }
public int getUpper() { return upper; }
public synchronized void setAUpper(int value){
if (value < a.getUpper())
a.setLower(value);
}

public asynchronization void setALower(int value){
if (value > a.getLower())
a.setUpper(value);
}
}

上面这段代码业务逻辑是想实现lower<upper:

1. lower和upper的初始值是(0, 5),
2.一个客户端请求线程A: setLower(4)
一个客户端请求线程B: setUpper(3)
3. lower和upper是 (4, 3)

这个结果破坏了lower<upper这个逻辑一致性,所以,用锁并不能保证逻辑一致性,而且还带来了堵塞。锁用错了地方,不但没有得到想要的,而且还失去更多。

下图展示了锁带来堵塞,每个时刻只能允许一个线程工作,如同只能允许一个人蹲马桶一样。


从历史上看,锁的问题如鬼魂一直伴随着我们:
1.用数据表一个字段来表示状态,比如1表示已付款未发货,2表示已付款已发货,然后用户来一个请求用SQL或存储过程修改,这时使用的数据库锁。

2.用ORM实现,比如Hibernate JPA来修改状态,虽然不用SQL了,但是Hibernate的悲观锁和乐观锁也让人抓狂。

3.彻底抛弃数据库,直接在内存缓存中进行修改,使用Java的同步锁,性能还是不够,吞吐量上不去。如上图提示,只能一个厕所蹲位一个人用,其他人必须排队。

4.Actor模型。

Actor模型原理
Actor模型=数据+行为+消息。

Actor模型内部的状态由自己的行为维护,外部线程不能直接调用对象的行为,必须通过消息才能激发行为,这样就保证Actor内部数据只有被自己修改。

Actor模型如何实现?

Scala或ErLang的进程信箱都是一种Actor模型,也有Java的专门的Actor模型,这里是几种Actor模型比较

明白了Actor模型原理,使用Disruptor这样无锁队列也可以自己实现Actor模型,让一个普通对象与外界的交互调用通过Disruptor消息队列实现,比如LMAX架构就是这样实现高频交易,从2009年成功运行至今,被Martin Fowler推崇。

回到本帖最初问题,如何使用Actor模型解决高并发事务呢?

转账是典型的符合该问题的案例,转账是将A帐号到B帐号转账,使用Actor模型解决如下:

发出是否可转出消息--->消息队列--->A

A作为一个对象,注意不是数据表,对象是有行为的,检查自己余额是否可转账,如果可以,冻结这部分金额,比如转账100元,冻结100元,从余额中扣除。因为外部命令是通过消息顺序进来的,所以下一个消息如果也是扣除,再次检查余额是否足够......

具体详细流程可见:REST和DDD

那么,既然Actor模型如此巧妙,而解决方向与我们习惯的数据喂机器的方式如此不同,那么如何在实战中能明显发现某个数据修改应该用Actor模型解决呢?因为我们习惯将数据喂机器的思路啊?

使用DDD领域驱动设计或CQRS架构就能明显发现这些特殊情况,CQRS是读写分离,其中写操作是应领域专家要求编写的功能,在这类方向,我们都有必要使用Actor模型实现,因为在这个方向上,领域专家的要求都表达为聚合根实体,聚合根就是用Actor模型实现最合适不过了。而读方向,比如大数据处理,报表查询,OLTP等等都是数据喂机器的方式。

有的道友会疑问,我们经常使用SSH,也就是Spring + Hibernate架构,这个默认是哪种方向呢?很显然,默认是数据喂机器的方向,所以在实现写操作时,特别警惕高并发发生死锁等影响性能问题,当然也包括EJB架构。

有一种togaf架构,将企业软件架构分为数据架构和应用架构等,实际是EJB或SSH的变相描述,这种架构的问题我们已经一目了然了,特别这样的系统如果从面向内部管理转向到SaaS模型时,这类高并发死锁问题就特别容易发生,几乎不具备可用性。前期12306火车票系统是这类问题的典型体现。

[该贴被banq于2013-09-12 08:09修改过]
[该贴被admin于2013-09-15 07:13修改过]
[该贴被admin于2013-09-15 07:29修改过]
[该贴被admin于2013-09-15 07:30修改过]

我们可以将目前企业软件分为两大类:license软件和云计算SaaS。

卖license的软件要过渡到Saas几乎不可能,需要重新编写,因为过去
企业软件架构存在致命问题:高并发事务处理。

卖license的软件一般是SOA架构,我认为由于SOA太强调服务,而服务是一种机器处理,如果在数据写方向使用SOA,这是一种数据喂机器的方式,必然会遭遇高并发事务和死锁问题。在这个写方向使用REST要比SOA更合适一些,因为REST侧重资源与状态,与DDD强调的实体与行为是一脉相承的。

函数式编程前段时间很火,但是纯FP的语言逐渐被对象和函数混合式的语言如Scala赶超,关键还是可变的状态是现实中无法回避的问题,可变的使用对象式的Actror是最合适的,而不变性使用函数这种机器处理方式也是最合适的。可变性对应写操作,不变性对应读操作,如大数据处理,报表BI等。

有人说,为什么大多数互联网软件都是数据喂机器,或函数式即可,比如twitter或facebook,因为他们没有事务要求,一般涉及到钱等重要交易都需要事务,而发个言,写段话等等都是非高事务要求,也可以这么说,非结构化的数据一般都没有事务要求,结构化聚合的数据才有事务要求。



[该贴被banq于2013-09-12 08:47修改过]

板桥老师,如果事物性要求不高的情况下,采用Actor模型与传统的模式相比,性能差异会大吗?

2013-09-12 09:23 "@DK
"的内容
如果事物性要求不高的情况下,采用Actor模型与传统的模式相比,性能差异会大吗? ...

事务性要求不高这词比较含糊,要么没有事务要求,要么有事务要求,有事务要求就要用事务机制,用了事务机制(数据喂机器的方式)+高并发就会出现死锁,死锁频率和并发的频率是呈正比。

所以,要对应用进行事务要求和非事务要求的划分,缩小事务要求的使用范围,这也是EBay架构的一个原则。

Actor前提是事务+高并发,注意,没有通用的银弹,但是我们要认识到每个解决方案的适用边界,就像十字螺丝刀适合十字形钉子,切忌拿了一把锤子,看到每个东西都是钉子,都想用锤子锤锤,锤子也有其适用范围。

Actor模型和STM软事务比较也应该适用不同,软事务也是一种数据喂机器的方式,当我们用词中用到“事务” “存储过程” “线程” “服务”这些函数式动词时,都是一种倾向数据喂机器的方式。

只要是数据喂机器的思维方式,就没有考虑到数据自身的逻辑一致性,也就不会想到要用行为保证其一致性,见:领域模型的行为设计

Actor模型=数据+行为+消息。

这个模型让我想到了DDD中的aggregate+event sourcing的结合体。
ddd中的aggregate封装了数据和业务规则,aggregate保证了自己的数据不能被外部随便修改,要修改必须用我自身提供的方法来实现。然后我自身提供的方法会确保我自己内部的数据总是满足要求的业务规则,这也是ddd中我们常说的聚合的不变性;

而消息,则是,当一个聚合的状态变化时,会产生领域事件,然后领域事件会由框架发布出去,然后外部的对象就可以响应事件,然后做相应处理,比如一些event handler会响应事件,然后产生command,然后command handler可能会通知另一个聚合根做什么事情,从而形成了聚合之间的消息通信回路。这样的机制应该和actor中所说的消息吻合的吧?

但是因为聚合与聚合之间没有事务一致性,也就是强一致性,聚合之间的消息是异步发送和响应的,所以一定是最终一致性;而actor中的消息是什么层面的呢?actor模型中的消息传递是一个什么概念?

banq, 那几个关于actors的图呢?

就转帐那个例子,有几个问题想问下,

两个扣款的msg发出去了,假如某一方未收到或收到后出现异常,
另一方改怎么办呢?

估计是有类似“冲正”的补偿机制吧。

英文PPT供大家参考:Going Reactive: Event-Driven, Scalable, Resilient &Responsive Systems 走向Reactive:事件驱动,可扩展,弹性响应系统

其26页谈了锁的很多问题,提出解决方向:1,避免堵塞 2.走异步之路

提出了Actors Agents futures/DataFlow Reactive Extensions(RX)几种可选模式。

另外一篇文章:Jave EE a tough sell for Spring Framework users JavaEE很难向Spring框架用户兜售,这篇文章说明JavaEE和Spring之间的争斗,其中提到Spring也有来自Akka(Actors模型) Scala的中间件技术挑战,
Spring is now under pressure, technically, from technologies like Java EE and the Akka Scala middleware technology。
[该贴被banq于2013-09-14 11:24修改过]

public class A {
private volatile int lower, upper; //两个状态值
public int getLower() { return lower; }
public int getUpper() { return upper; }
public asynchronization void setAUpper(int value){
if (value < a.getUpper())
a.setLower(value);
}

public asynchronization void setALower(int value){
if (value > a.getLower())
a.setUpper(value);
}
}
这段代码我不解。我认为会发生重入。不会死锁,两个方法锁住的都是this,
A线程运行setAUpper(),然后,假如运行里面的setLower,这时
一个线程运行时。试图获得已经由他自己持有的锁,发生重入,不会死锁
B线程setALower(),然后运行里面的setUpper();也是重入啊

死锁一般是同步嵌套。A锁住一个。B锁住一个,
A不放手,B也不放手,但是A要B的。B要A的
他们锁住的都是this怎么会发生死锁

还有asynchronization 这个和synchronized一样作用?

2013-09-14 20:00 "@c3tc3tc3t
"的内容
他们锁住的都是this怎么会发生死锁

还有asynchronization 这个和synchronized一样作用? ...

多谢指正,这两个地方确实存在错误,用这段代码主要想说明引入了锁,性能降低,业务逻辑也没有达到要求,不是存在死锁。

这里业务逻辑是lower<upper,我们不能只是取名称,而且要确保代码也能保证这种逻辑一致性。

1. lower和upper的初始值是(0, 5),
2.一个客户端请求线程A: setLower(4)
一个客户端请求线程B: setUpper(3)
3. lower和upper是 (4, 3)

这个结果破坏了lower<upper这个逻辑一致性,所以,用锁并不能保证逻辑一致性,而且还带来了堵塞。

前面我指出传统SOA架构其实无法面对高并发事务,我以国内淘宝网的PPT为案例,分析一下:面向生产环境的SOA系统设计 by 程立

在三十页,使用悲观锁实现服务对资源的并发控制,这种方式最容易发生我在首贴里提到的死锁:


PPT也认为这种方式不适合热点资源,也就是高并发场合。

乐观锁如何呢?



虽然乐观锁短,但是容易产生脏数据。

该PPT虽然否定了乐观和悲观锁的解决方案,但是思路还是在锁的方向,没有可加锁的资源,那么我们人为制造一个锁,以操作实例也就是操作的对象的ID为锁标识,这其实是将整个对象在内存中锁起来,会发生多线程变单线程,一个马桶只能蹲一个人的现象,我在首贴中的画图中描述过这个办法。


当然,以上资料根据2009年淘宝网支付宝公布的文档,不代表他们现在没有更新升级。


[该贴被banq于2013-09-17 11:22修改过]

[A作为一个对象,注意不是数据表,对象是有行为的,检查自己余额是否可转账,如果可以,冻结这部分金额,比如转账100元,冻结100元,从余额中扣除。因为外部命令是通过消息顺序进来的,所以下一个消息如果也是扣除,再次检查余额是否足够......]
这段话...写得太好了...我终于理解了actor模型....
但是...这个actor 视乎不是很容易实现吧.
就拿账户来举例,我有1千万个账户,就得建立 1千万个actor

当然也可以在首次发生转账时 new actor
然后可以使用对象池来管理
但是一个系统里的对象好多哦.不仅仅是账户,还有货物,商品等其他容易发生争用的东西...这样每个都建立actor的话,工作量要增加好多倍...
不知道我的思路对不对.

2013-09-25 16:41 "@px96004
"的内容
但是一个系统里的对象好多哦.不仅仅是账户,还有货物,商品等其他容易发生争用的东西...这样每个都建立actor的话, ...

Akka缺省是使用forkjoin pool来实现的,为防止线程过多也有BalancingDispatcher可选择:
http://doc.akka.io/docs/akka/snapshot/scala/dispatchers.html

2013-09-12 07:59 "@banq
"的内容
A作为一个对象,注意不是数据表,对象是有行为的,检查自己余额是否可转账,如果可以,冻结这部分金额,比如转账100元,冻结100元,从余额中扣除。因为外部命令是通过消息顺序进来的,所以下一个消息如果也是扣除,再次检查余额是否足够...... ...


这个有些疑问,比如A发起转账,在转账中我需要判断A账户余额是否满足转账条件,满足后发送转账消息,那么在还没处理消息时,这是的余额是还没有扣除的,那么A又发起了一个转账,假设余额有1000 那么第一次转账为500 第二次为600 ,如果第二次转账的时候还没处理第一次的消息,那么第二次转账是会被发送消息的。

2013-09-27 22:17 "@w438418754 "的内容
就拿账户来举例,我有1千万个账户,就得建立 1千万个actor ...

我觉得只建立一个AccountActor 实例就行吧。针对不同的主动对象建立一批Actor(例如GoodsActor、prod_Actor...)。
[该贴被panpan于2013-09-28 22:20修改过]

2013-09-28 22:19 "@panpan
"的内容
我觉得只建立一个AccountActor 实例就行吧。针对不同的主动对象建立一批Actor(例如GoodsActor、prod_Actor...)。 ...

那应该就不好实现并发了吧...那样就有点像锁表似的吧