首先,感谢公司建立的产研书吧,让产品和研发部门的小伙伴能够有更好的机会读一些好书,提升自己专业技能的同时,也让我们时刻保持向上学习的动力。此外,感谢HRBP小伙伴,让我有机会在阅读之后,发表一些个人的拙见。
我觉得读的书,从个人目的上可以大致分为两类:一类是用以解决当下的困惑,另一类则是用以开拓眼界。开始我想阅读这本《高可用架构》的时候,我是抱着后一种目的的。我希望能从这本书中汲取些比较实用的架构思想,用以帮助自己找到一些灵感去开拓成长为架构师的路。
这本书是个合集,它辑录了很多知名互联网公司专家的技术分享与交流。从日志系统、IM系统的基本架构,到高可用架构、分布式系统的实践,以及容器、大数据相关的架构设计均有涉猎。书中大大小小50来个架构案例,如果没有足够多的知识储备、亲手实践过其中的某一部分内容,是无法体会其中的精髓的。
在这里,我对我个人所感兴趣的点进行一些展开。在讲雪球的IM系统架构的时候,文中介绍到,雪球的IM系统通过Netty+自定义网络协议的方式进行消息的网络传输,然后客户端用Akka做消息同步。接下来我就稍稍分享下我对Akka这个框架的理解。
Akka是帮助构建高并发、分布式和具有容错性的事件驱动应用程序的工具包。它包含Java和Scala两种API。Akka的最小计算单元是Actor。Actor模型是由Carl Hewitt在1973年提出的。
那什么又是Actor呢?在使用Java进行并发编程时需要特别的关注锁和内存原子性等一系列线程问题,而Actor模型内部的状态是由它自己维护的,Actor与Actor之间通过消息传递来进行状态修改。Actor由状态(State)、行为(Behavior)和邮箱(MailBox)三部分组成:
组成 | 含义 |
---|---|
状态(State) | Actor中的状态指的是Actor对象的变量信息,状态由Actor自己管理,避免了并发环境下的锁和内存原子性等问题 |
行为(Behavior) | 行为指定的是Actor中计算逻辑,通过Actor接收到消息来改变Actor的状态 |
邮箱(MailBox) | 邮箱是Actor和Actor之间的通信桥梁,邮箱内部通过FIFO消息队列来存储发送方Actor消息,接受方Actor从邮箱队列中获取消息 |
下面这张图可以简单描述Actor的工作原理:
为了更好地理解Actor模型与Akka的应用,在这里,我分别用传统Java内部锁机制与Akka来实现“转账”功能。
首先,对于单个账户来讲,要保证扣款与入账操作的原子性,所以扣款、入账都得用内部锁作同步:
public class Account {
private int amount;
public synchronized void deduct(int amount) {
this.amount += amount;
}
public synchronized void add(int amount) {
this.amount -= amount;
}
}
然后, 在执行转账操作的时候,把两个账户都加上锁:
public void transfer(Account from, Account to, int amount) {
synchronized(from) {
synchronized(to) {
from.deduct(amount);
to.add(amount);
}
}
}
现在,我们该加锁的地方都加了,但是,这样的程序还是有问题的。假设线程1在执行账户A给账户B转账操作的时候,线程1已经抢占了账户A的锁,而另外有一线程2执行的是账户B给账户A转账的操作,且抢占到了账户B的锁。此时线程1、2都渴望抢夺到已经被对方抢占的锁,于是就发生了死锁。
解决的方案也是有的,可以给账户排序,加锁的时候按照账户顺序来抢占锁,这样便破坏了死锁必要条件中的“循环等待条件”,预防了死锁。
下面我们来看看Akka是如何实现转账功能的。我们为需要转账的账户各建立一个Actor,那么接下来的问题就在于两个Actor是如何通信的了。由于Actor与Actor之间是通过消息来通信的,那么我们首先得定义好消息,包括两种消息,一是扣款消息,二是入账消息:
/**
* 扣款消息
*/
static public class DeductMsg {
private final Integer money;
public DeductMsg(Integer money) {
this.money = money;
}
public Integer getMoney() {
return money;
}
}
/**
* 入账消息
*/
static public class AddMsg {
private final Integer money;
public AddMsg(Integer money) {
this.money = money;
}
public Integer getMoney() {
return money;
}
}
对于一个账户的Actor来说,它是按照FIFO的顺序来处理自身的消息的,账户内部是能够保持同步的,所以实际上就只需要先向账户A发送扣款的消息,执行成功后再向账户B发送入账消息。在这里,为了整体控制转账流程,我还专门创建了个协作Actor,当这个协作Actor接收到转账消息时,会对相应的账户Actor发送消息:
/**
* 转账消息
*/
static public class TransferMsg {
private final ActorRef fromAccount;
private final ActorRef toAccount;
private final Integer money;
public TransferMsg(ActorRef fromAccount, ActorRef toAccount, Integer money) {
this.fromAccount = fromAccount;
this.toAccount = toAccount;
this.money = money;
}
public ActorRef getFromAccount() {
return fromAccount;
}
public ActorRef getToAccount() {
return toAccount;
}
public Integer getMoney() {
return money;
}
}
// 协作Actor接收到转账消息
@Override
public Receive createReceive() {
return receiveBuilder()
.match(TransferMsg.class, transferMsg -> {
// 发送扣款消息
transferMsg.getFromAccount().tell(new AccountActor.DeductMsg(transferMsg.getMoney()), getSelf());
// 发送入账消息
transferMsg.getToAccount().tell(new AccountActor.AddMsg(transferMsg.getMoney()), getSelf());
})
.build();
}
由于代码稍稍有点多,这里就不全部贴出来了,感兴趣的话可以查看https://github.com/sofkyle/Akka-Sample下的Deal实例。
关于Akka还值得一提的是,Actor不单可以在当前JVM中运行,也可以跨JVM在任何机器上运行。我个人觉得在某些方面它可以作为替代Netty+Zookeeper集群的方案,因为Akka有自己的集群实现而不需要依赖其他中间件。
关于Akka就介绍就介绍这么多,如果想了解更多,可以参考文章末尾的参考资料。
回到《高可用架构》这本书本身来,补充一些我学到的小知识。书中在介绍百姓网的亿级规模Elasticsearch优化实战的时候,提到了JVM 32G现象:当JVM堆大小超过32G时,哪怕用更多的内存,效果都不如31G。除此之外,书中还有章节介绍NoSQL的发展历史。在了解到了NoSQL的发展史之后,我丝毫不会再去疑惑“NoSQL是否会最终替代RDB”这样的问题。NoSQL和RDB都有各自适合的场景,我觉得学会根据现实状况做取舍是成长为架构师的第一步。
总而言之,书中所包含内容颇丰,具体涉及到架构案例的章节,也有足够的深度与广度。所以我觉得这本书不适合用来“找灵感”,倒更适合于在具体实践某种架构方案的时候,掌握一些他人已实践过的可能性。
下面说点题外话。《极客与团队》一书里面提到,程序员之间其实是需要多多交流的,因为单打独斗不光是增加了自己失败的风险,而且浪费了自己成长的可能性。因为我们无法知道,我们自己选的路是不是对的。所以,我们要适当的把自己的问题暴露出来。我希望能有更多小伙伴参与到书吧的分享活动中来,一起来分享自己的学习、读书心得。
最后的最后,让我用《高可用架构》里面的一碗毒鸡汤来结束我的这次分享,与诸君共勉:
- 不要奢望其他人会写出高质量的代码。
- 不要以为自己写出来的是高质量的代码。
参考资料:
[1] 高可用架构社区. 高可用架构.第1卷. 北京:电子工业出版社,2017.11
[2] 当多线程并发遇到Actor (https://mp.weixin.qq.com/s/mzZatZ10Rh19IEgQvbhGUg)
[3] Netty和Akka有什么不同? (https://www.cnblogs.com/WangBoBlog/p/7843688.html)
[4] Actor模型 (https://blog.csdn.net/gulianchao/article/details/7249117)
[5] 使用Akka的远程调用 (https://blog.csdn.net/beliefer/article/details/53841919)
[6] Actor模型原理 (https://www.cnblogs.com/MOBIN/p/7236893.html)
[7] Akka Quickstart with Java (https://developer.lightbend.com/guides/akka-quickstart-java/)