优秀的Actor设计
我看到这一章节真是如获至宝,毕竟它阐释了如何使用Akka,以及优秀的设计模式。
大系统小做
其实作者想告诉我们的是要尽量提前关注actor级别的设计,而不是仅仅考虑大的架构。因为设计一个分布式、高并发的系统时,actor的功能边界也非常重要,下层基础决定上层建筑。
封装actor中的状态
actor一个非常重要的特性就是以线程安全的方式管理状态。所以状态对于actor就需要以合适的方法来管理。
使用字段封装状态
最简单的状态管理方法就是用actor中的可变私有字段去记录状态,这是相对简单的实现。比如有一个long类型的字段记录当前actor处理过的消息个数。这种方法简单也最基础。但是当状态太多的时候,就变得难以管理了。比如设置A字段的时候还需要检验B字段的有效性,这也可以直接在actor处理消息的时候验证,但如果验证方法变了,是不是又得修改actor的行为呢。所以就需要把相关的逻辑代码从actor里面抽象出来。可以创建能够堆叠到actor上的trait,这些trait包含改变可变状态的逻辑代码。这样actor就变成只包含与并发机制相关的逻辑代码了。其实这种方法就是简单的把字段的校验逻辑封装起来了,那如果状态太多怎么办呢。这就需要进一步封装了。
使用“状态”容器封装状态
如果状态很多,那就用一个类封装状态。当然了校验逻辑还是可以放在trait中的。但无论此时封装状态的方法还都是以可变变量来实现的。但Scala、Akka还是比较提倡面向不可变性编程的。幸运的是Akka提供了相关的机制,来消除可变性。
使用become封装状态
状态管理的另一个方法就是使用beconme功能。Akka认为行为和状态的变化事实上是一回事,简单来说它们都是一种变化。这种封装的方法简单来说就是,使用beconme修改actor行为的同时,给行为处理函数传入当前的状态,这样actor中就没有var类型的状态了,是不是很牛。当然了,这只有在actor具有多个行为的时候才更有用。很适用的一个场景就是状态机。当存在多个行为转换,且其中每个转换又可能具有不同的状态时,最好通过使用become去捕获该状态,这样可以简化逻辑,而不使其更复杂,同时降低了认知开销。
但我不太喜欢become来封装状态,毕竟变来变去还挺麻烦的,如果行为没有变化,使用become仅仅是要变化某个字段,看起来还挺恶心的。
将future与actor混合
future是将并发引入系统的有效途径,但是在Akka中需要特别小心。因为他们是两个不同的并发模型。但某些时候把他们混用还挺合适的,比如可以将某个IO操作丢给future。我就经常这样做,我最喜欢把数据库相关的操作丢给future。但需要注意的时,需要设置future的执行上下文,避免与actor争用线程池,这一点很重要。使用future的时候最好用pipe模式,毕竟pipeTo会明显的显示操作不会立即发生,而是在将来的某个时间点发生,即不需要查看相关的签名代码就能知道里面的逻辑涉及future的相关操作。
在future中要谨慎对待sender,毕竟你以为的不一定就是你以为的。
Ask模式和替代方案
Ask模式是Akka中一个常见模式,跟普通的函数调用有点类似。但我的建议是在Akka系统内部,永远都不要使用ask。
关于ask模式的问题,原书已经讲的很清楚了,给出的替代方案是用forward转发消息,确保对原始发送方的引用。但万一忘了forward呢?毕竟那么多actor。最后一个办法是把Promise作为消息的一部分来传递,通过管道发送promise,以便管道的最后阶段可以完成该Promise。Promise是一个承诺未来有值的接口。通过与其关联的future获取,一旦对Promise赋值 ,则与之对应的future就会完成,而future是一种单纯的一步机制。
其实要我说,akka内部,完全没有用ask模式的必要,只有在其他系统与akka通信的时候才可以用ask。关于具体怎么用,这里就不说了,可以看我的开源任务调度器:https://github.com/gabrywu/lemon-schedule
命令与事件
与actor相关的消息可以分为两大类:命令和事件。一条命令是指对将来某一个时刻发生的事情的请求。当然最后可能发生也可能不会发生。可以类比成OOP对象的一个函数调用。事件是记录已经发生的动作的消息。它是过去的事情,不能改变。但其他actor可以对它作出反应。了解命令和事件的区别非常重要,它可以消除系统模块之间的依赖关系。我前面也说过,akka到最后就是面向消息编程!对消息进行分类还是很有好处的。
构造函数的依赖注入
感觉作者的思维还是很跳跃的,突然就开始讲依赖注入了。其实就是actor与其他actor需要通信,那么怎么获取其他actor的引用呢。其中一个简单的方法就是在创建actor的时候把其他actor的引用通过构造函数的方式传递进去。当然这不算很好的方法,毕竟耦合性太强了。
使用路径查找actor
如果我们知道actor的路径,那么就可以使用actorSelection来查找actor。但actorSelection可能会查到很多actor,发送消息时实际上是将其广播给所有actor。这其实非常糟糕,毕竟对于源actor的负担加重了,他需要知道到底查找到了多少个actor,如果不管有多少个actor,那么如果所有的actor都进行了回复,那么是不是意味着消息被处理多遍?
将acotor引用作为一个消息
改变actor之间负载依赖关系的一个更好的方法就是将actor引用作为一个消息发送给需要它的actor。这种是我非常喜欢的一种方式,毕竟actor引用是一个值,通过消息传递还是比较合理的。而且这种方法需要我们好好的设计actor之间的层次结构。
结论
这一章节作者提供了一些基本的原则和技巧,但都是泛泛而谈,也没有说明各个方案的背景和适用场景,这就需要大家慢慢琢磨了。其实更多的时候,我们只有用akka设计了更多的系统,遇到了很多问题,回过头来再看这些原则会更加感同身受。毕竟自己的坑自己最清楚。