原文地址:http://my.oschina.net/yilian/blog/208920
这一章 开始讲actors角色模型系列,讲诉什么是角色模型 ,角色模型应用的领域 并发 事务 消息驱动 消息中间件 网络传输 ,本系列第一张 讲解自己对角色模型的理解 以及 基本认识使用 以后所有文章演示章节的源代码 我以后将会放到我的一个项目下,这是我前面讲querydsl时候 做的一个项目。现在基本架构放到了github https://github.com/zhuyuping/zyp 地址 演示地址 http://recordback1314.duapp.com/
-
什么是角色模型 角色模型的原理 (与 事件驱动有什么相同不同)
先引入网上一段介绍
角色模型是一种不同的并发进程建模方式。与通过共享内存与锁交互的线程不同,角色模型利用了 “角色” 概念,使用邮箱来传递异步消息。在这里,邮箱 类似于实际生活中的邮箱,消息可以存储并供其他角色检索,以便处理。邮箱有效地将各个进程彼此分开,而不用共享内存中的变量。
角色充当着独立且完全不同的实体,不会共享内存来进行通信。实际上,角色仅能通过邮箱通信。角色模型中没有锁和同步块,所以不会出现由它们引发的问题,比如死锁、严重的丢失更新问题。而且,角色能够并发工作,而不是采用某种顺序方式。因此,角色更加安全(不需要锁和同步),角色模型本身能够处理协调问题。在本质上,角色模型使并发编程更加简单了。
角色模型并不是一个新概念,它已经存在很长时间了。一些语言(比如 Erlang 和 Scala)的并发模型就是基于角色的,而不是基于线程。实际上,Erlang 在企业环境中的成功(Erlang 由 Ericsson 创建,在电信领域有着悠久的历史)无疑使角色模型变得更加流行,曝光率更高,而且这也使它成为了其他语言的一种可行的选择。Erlang 是角色模型更安全的并发编程方法的一个杰出示例。
我的理解是,他是一种事件驱动的变体,他的角色只作为事件消息接收 发送,他的角色发送的事件消息才是并发实体,他是基于actor对象组件的高级并发模型
他不在需要像事件驱动多线程去执行轮训或者调度,因为传统的事件驱动 ,一般事件消息接收通知事件然后消息的发送处理者根据接收消息事件,然后并行的发起多个消息处理者多线程运行,相反,角色模型里面,事件处理者actors接收消息,唯一事件处理者代理actorRef 发送消息,然后接收的消息 放入共享队列(角色模型里面称为信箱)这样发送的消息只针对角色与角色之间,接收的消息 只有同一个时间一个角色,这样无需处理容易产生并发锁问题,但是注意的是 消息接收时候 如果在当前本角色处理方法中调用本角色 再次创建新的角色 也是可能会产生死锁的问题的,这个大家知道的,发送 接收逇消息 放入信箱中,信箱再交给底层的多线程 lock 等等去调度,这样 actors 之间只能通过消息来同步 ,每个Actors在同一时间处理最多一个消息,可以发送消息给其他Actors。在同一时间可以于一个Java虚拟机存在数以百万计的参与者,构架是一个分层的父层(管理) - 子层,其中父层监控子层的行为。还可以很容易地扩展Actor运行在集群中各个节点之间 - 无需修改一行代码。每个演员都可以有内部状态(字段/变量) ,但通信只能通过消息传递,不会有共享数据结构(计数器,队列),这些每个角色存在唯一的path 他是一种链接形式标识引用。类似与zookeeper 节点的(文件目录)上下级关系,总的来说 就是 我要做什么改变 一切通过角色发送消息 来通知自己还是别人,就如发送邮件一样,可以回应 可以发送,但是 你要对什么做出改变的通知事件消息 必须通过当前或者其他角色来发送消息对自己做出改变。这是我以前看了akka的一些源代码 做出的总结
-
角色模型的框架akka
akka 框架是一个基于JVM上的scala 语言编写的角色模型的框架 官方网站是 http://akka.io/ 其实我稍微看了下源代码 ,我觉得java也是能实现的,只要把角色的地址应用actorPath 以及属性 通过反射 代理生成 类似EJB的机制,也是能够实现的同样的效果的
-
角色模型的框架akka使用
1.akka 的角色基本概念
actor角色 :akka 中的actor主要分为本地角色local actor 和远程角色 actor 默认是本地角色 ,因为默认配 置为 本地角色akka.actor.LocalActorRefProvider 本地角色 可以理解成同一JVM 不会发生网络间进程通讯
远程角色为akka.remote.RemoteActorRefProvider,每个角色 都有一个只有一个并且一对一唯一的角色代理对象 具有角色的所有功能,他是角色的发送消息行为的决策者,具有actor的所有功能,实现角色只需要extends UntypedActor 实现onRecevice方法,,每个角色 就有一个绝对地址名称 actorPath 他是uri形式
akka://<actorsystemname>@<hostname>:<port>/<actor path> ,他的管理方式是树形式,如果由当前角色创建,新角色便是当前的的子节点,如果由根系统创建 便是顶级节点,就如同文件管理器的路径一样,所以便存在相对地址与绝对地址,因为每个角色对应唯一一个的actorRef ,所以 你可以大致的认为,actorRef是主动做出改变的,而actor是对接收的消息回调receive后 进行回应与改变的,本质是一样的,另外对于一些角色处理完成接收的或者path没有获取到,actor 以及他的actorpath 会放入到一个死信邮箱,这些角色可以重新初始化被引用实现的,我们说了因为他们有一个绝对唯一的引用地址ActorPath ,actorPath是由名称 父节点名称 。。。一直接下去的引用 所以只要保证当前actor创建的所有子类没有重名就够了
因为actorRef我们可以理解为主动做出改变的,actor 只是接受回调并相应改变 对自己进行处理,所以后面文字我说的角色你们可以把它当成actorRef 或者actor 他们本质是一样的 一对一唯一引用代理而已
有几个特殊的角色引用要注意的,一个是root guarad 根系统角色 一个是临时节点 ,而我们说的远程还是本地角色 只是他的一个分类,因为我们前面说了path 唯一 所以创建角色引用可以使用actorPath创建,也可以通过根系统创建,可以通过当前actor创建,这就是大体actorRef创建的集中方法,后面细说。每个actor 的具有上下文的引用对象context,就有自身的引用对象self 具有对接收来自某角色消息的 某角色的引用sender
这样 我们可以在actor中获得上面三类应用,比如进行回应自己,回应来源,创建新角色 进行回应。这样实现消息只通过actor actor之间进行交互,这是学习actor的基本概念 后面会实例详细讲解 实现我们前面说的那个并发情境,午休时间到了 晚上接着更新
-
akka 的消息机制 以及 基本使用
前面说了actor 与 actorRef 的职责与功能,关键在只能通过角色消息交互来决定角色的行为动作,这很类似与基于jms的一些消息中间件 像rabbitmq activemq zeroMQ..等等,不同的是他们底层是并发委派调度那些消息产生者 ,本质没有脱离生产消费的模型,然后消息通过队列等并发容器共享,而actor不同 ,在actor模型中如果只有一个actor 与另一个actor 交互每一次消息会话创建压根就没有并发,因为他压根就是反应器模式或者其他代理模式 ,actor他的并发在消息会话以及消息信箱 上,这就是不同,actor注重每一次消息过程,每一次发送消息 并进行交互 你可以看成一次并发,这是我看了一些actor角色模型的框架源代码的一些认识理解,如果不准确 请忽略
一 actor角色的理解
1.对于角色我们前面讲了 他是actorPath树组织模式,每一个父节点是下面的监管者,而顶级监管者是actorSystem 他的actorPath是“/system” 所以我们可以从顶级创建 或者从每一个actor的上下文作为创建,因为actorpath唯一所以 可以重复利用过去已经放入死信邮箱(回收站)里面的actor
12345678910111213141516171819202122232425262728293031323334353637ActorSystem rootSystem = ActorSystem.create(
"zypSystem"
);
//这里是actorSystem你可以看成一个管理上下父子树组的actor角色模型框架的门面模板 这里没有加入配置,使用上面说的默认localProvider配置
ActorRef firstRef = rootSystem.actorOf(Props.create(FirstActor.
class
, name);,
"First1"
);
//这里是创建actorRef(上面已经讲了)
下面我们创建一个角色actor用于接收actorRef发出的改变的消息通知
import
akka.actor.UntypedActor;
/**
*
*
*
* @author zhuyuping
* @version 1.0
* @created 2014-3-18 下午7:42:56
* @function:我是第一个actor角色
*/
public
class
FirstActor
extends
UntypedActor{
@Override
public
void
onReceive(Object message)
throws
Exception {
// TODO Auto-generated method stub
}
}
角色之间 交互通知方式 有 Actorref.tell(Object message,ActorRef reply) 总的来说就是 actorRef发送一个做出改变消息message到与该actorRef唯一一对一对应的actor,通过message我们知道我们要做出什么动作 执行什么方法, 然后设置该actor的sender为reply,这样比如你要给reply发送消息在该actor中就可以使用getSender获取到reply对象, 记住actor的三个常用的角色(在UntypedActor) 一个是自身self() 一个是引用别的actor 还有一个是sender 就是你接收消息的来源 比如A接收到B发送过来的消息,然后A把消息处理后发送给C 并回复给B 只要在A的actor的接收消息方法中写入这么一段,
//处理a接收到的message
ActorRef C= getContext().actorOf(同上);
C.tell(message被处理后的数据,B);
//这样 Cactor会接受到被处理的数据 并设置sender为B
下一步
这样在C的接收方法中使用getSender()就获取到了B对象
getSender().tell(
"这就完成了一个回复B的过程"
,noSender());
//使用noSender与null一样 不设置回复对象
还有ask方式 forword方式 他是创建临时的引用actorRef /tmp 并返回一个future 后面我会慢慢讲到
以后所有文章演示章节的源代码 我以后将会放到我的一个项目下,这是我前面讲querydsl时候 做的一个项目。现在基本架构放到了github https:
//github.com/zhuyuping/zyp
地址 演示地址 http:
//recordback1314.duapp.com/
角色基本概念的理解到这里了 后面详细的讲解
二。角色的创建与发送消息 接收 回复消息
对于角色的创建 通过上面的大致描述 我们可以总结为下面几种
1. 通过根系统节点创建 ActorSystem.actorFor/ActorSystem.actorOf of与for的区别在于of是从当前节点创建一个新的子节点,for是创建一个过去已经创建但是完成了工作生命周期已经停止的actor
2.通过上下文context 我们知道对于实现一个actor需要继承UntypedActor 这样角色能够接受来自自身唯一一对一的引用的actorRef的消息,他有山下context 还有自身的引用self 还有对回复者的引用sender 这些都可以从untypeActor源码中方法看到 所以可以是用getContext().actorFor/ActorOf 另外我们知道 每一个actor对应一个唯一的actorPath 所以还可以使用绝对路径 与相对路径 ,比如 创建remote的getContext().actorFor("akka://remoteName@远程ip:端口/user/remoteTestActor"); 可以这样创建 对于这里 /user 其实很容易理解的 跟系统默认是/system 根系统直接创建的角色 也就是他的第一级子节点默认是/user 然后我们一级一级下去 通过上面我们创建时候props的定义的名称属性 一步一步决定每一个actor的唯一actorpath ,还有就是有时候我们要获取父类的所有节点getContext().actorFor("../user"); 使用相对地址便很容易来获取
3.通过actor的属性Props创建 props是一个配置类用于创建actors时,你可以定义配置文件使用prop导入 初始化。正常时候ActorRef firstActor = system.actorOf(new Props(FirstActor.class).withDispatcher("my-dispatcher"), "firstactor");//这个方法源码你如果去看的会发现 它实际上是调用 工厂方法创建的
-
1234
new
UntypedActorFactory() {
public
UntypedActor create() {
return
new
FirstActor();
}
所以创建时候可以使用他,有人觉得这样还有重写factory方法 干什么,因为默认就是这样,当我们使用factory创建时候主要在 我们firstActor 需要传递参数 没有默认构造函数,那么上面的直接props包装的方式就不能使用了 这时候需要重写Factory类 new Props(new UtypeActorFactory(){ ....}); 如
1
2
3
4
|
new
UntypedActorFactory() {
public
UntypedActor create() {
return
new
FirstActor(name);
}
|
对于角色的消息交互主要有下面几种
-
正如上面讲角色的基本概念的时候讲的那样,我们可以通过tell方法 ,使用方式见上面讲述
-
第二种就是ask方式 他是创建临时节点/tmp 执行回应,比如Patterns.ask(FirstActor角色ref, "要发送的消息", 1000);也就是firstActor发送message 然后1000ms内作出回应 相应到、/tmp节点 并返回Future 同java的并发包一样,Future就是一个将来的待处理回调的存储结果的类,可以接着使用Await.result 也可以接收的future重写 future.onSuccess() 等等方法
3.第三种就是forward 使用方式myActor.forward(message, getContext()); 从一个角色转发消息到另一个角色
这里注意的是使用ask主要是一些需要耗时较长 需要同步等待结果处理的 ,所以会影响性能
消息的接收前面已经已经讲过了 这里不再讲了
后面要提的是 生命周期
我们可以覆盖重写actor的生命周期,比如我们常在preStart执行初始化方法 比如初始化数据库连接 初始化actor 对于每一个actor只要创建成功 就会自动开始其生命周期
在postStop()执行销毁结束相关工作
你只要重写该方法即可,常用的其他生命周期过程 可以见下图
3.akka实战演示
现在假如我们要实现一个需求 要执行5个任务,每个任务要并发执行,都执行执行完了 返回执行任务成功的的任务名称
首先 我们往pom先加入actor的相关依赖
1
2
3
4
5
6
7
8
9
10
|
< dependency >
< groupId >com.typesafe.akka</ groupId >
< artifactId >akka-actor_2.10</ artifactId >
< version >2.2.4</ version >
</ dependency >
< dependency >
< groupId >com.typesafe.akka</ groupId >
< artifactId >akka-kernel_2.10</ artifactId >
< version >2.2.4</ version >
</ dependency >
|
第二步 我们编写任务角色类
我们分析 简单的思考 只需要2个角色 , 首先第一个是执行任务的角色 还有一个就是汇总统计成功任务的角色,要传递的消息有2个 1个是任务对象 还有1个是成功的任务处理后的结果,OK下面编写2个角色
首先是执行任务的角色
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
|
* @author
zhuyuping
* @version
1.0
* @created
2014 - 3 - 19
下午 7 : 55 : 43
* @function :这是执行任务的角色
*/ public
class
ExecuteTaskActor extends
UntypedActor{
/**
* Logger for this class
*/
private
static
final
Logger logger = Logger.getLogger(ExecuteTaskActor. class );
// private ActorRef taskReturnActor = getContext().actorOf(
//
// Props.create(new Creator<UntypedActor>() {
// /**
// *
// */
// private static final long serialVersionUID = 1L;
//
// @Override
// public UntypedActor create() throws Exception {
// return new TaskReturnActor(); //这里复习 一下上面的只是 使用UntypedActorFactory 在2.2版本后被遗弃 使用create替代
// // 这里我特意用这种工厂方式写 是因为踏实的taskReturnActor可以自定义构造函数传递参数
// }
// }),"taskReturn");
ActorRef taskReturnActor=getContext().actorOf(Props.create(TaskReturnActor. class ));
private
Random rnd= new
Random();
@Override
public
void
preStart() throws
Exception {
//这里一般进行初始化工作
logger.info( "初始化角色。。。。。。。" );
}
@Override
public
void
onReceive(Object message) throws
Exception {
if (message instanceof
Task){
//接收到前面发送过来的任务
Task task=(Task) message;
logger.info( "将会执行 任务 " +task.getName());
Result result=handler(task);
//这里我们要往统计任务结果的actor发送消息,有2中方式 一种是 前面发送task时候 把统计结果的TaskReturnActor 作为sender 2,方式二 是 在此类中初始化taskReturnActor
//第一种很简单 不说了 这里我们第二种
//我吧结果全部返回吧 不加result判断了 if(result.isSuccess()){} 我们在后面taskReturn 加吧
taskReturnActor.tell(result, self());
} else
if (message instanceof
Result){
taskReturnActor.tell( "getResult" , null );
} else
unhandled(message);
}
private
Result handler(Task task) {
logger.info(task.getName()+ " 任务正在处理中" );
//这里我们执行任务 成功返回success 失败 返回 fail 为了演示 我们随机产生正确失败结果 模拟 可能会出现失败成功
if (rnd.nextInt( 20 )> 10 ){
return
new
Result(task.getName(), "success" );
} else {
return
new
Result(task.getName(), "fail" );
}
}
}
|
然后是统计任务结果的actor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
public
class
TaskReturnActor extends
UntypedActor{
/**
* Logger for this class
*/
private
static
final
Logger logger = Logger.getLogger(TaskReturnActor. class );
private
Map<String,Object> results=Maps.newHashMap();
@Override
public
void
onReceive(Object message) throws
Exception {
if (message instanceof
Result){
Result rs=(Result) message;
logger.info( "接收到 被处理的结果 信息 任务 " +rs.getTaskName()+ " 该任务执行的成功失败情况 " +rs.getResult());
if (rs.getResult().equals( "success" )){
results.put(rs.getTaskName(), rs.getResult());
}
if (results.size()== 5 ){
//这里我只是简单的写一下 实际上我们可以传递参数 获取任务数目 为了回顾上面讲的知识 我们
getSender().tell( new
Result(), null ); //这里是我注定 告诉 已经处理完了 ok 可以返回结果了 ,如果有些任务等不及 ,你使用 ExecuteTaskActorRef.tell(new Result(),null)同样可以获得中间的结果
}
} else
if (message instanceof
String){
//获得结果 我们就输出算了
logger.info( "执行任务成功的任务名 有 " +results.keySet().toString());
} else
unhandled(message);
}
}
|
下面就是消息对象,由于字数限制 我一直在删,所以简略的贴出截图 源代码我push 到github上了
下面我们运行执行结果
这里备注 一下 ,就是在actor里面 每一次消息交互 就是相当于一次并发线程。。。。上面我们讲的很详细了,大家看到实战演示后 可以跳回去看 前面我说的总结话语,这样更容易理解
字数太多了 。。。。一直删 ,最后发现图片没事,就截图。源码在github 稍后我会push上去,后面的系列 会更新完下面的内容 ,有什么问题 可以在下面留言
-
akka常用的类的使用 创建 分派route 配置 path路径 远程引用 配置 生命周期 异步futures
-
akka的角色树管理形式 监管者 被监管者
-
与spring整合 实现网络消息中间件 集群
-
actor角色模型 整合 netty5.0实现 高吞吐 高性能服务器