写在开头一般来说,并发线程中的通信有两种策略:共享数据和消息传递。使用共享数据的并发编程面临的最大问题之一是数据条件竞争。处理各种锁是一件非常头疼的事情。大多数传统流行语言的并发都是基于多线程间共享内存,使用同步的方式来防止写竞争,Actor使用消息模型,每个Actor一次最多处理一个消息,并且可以向其他Actor发送消息,原则上保证分开写。这巧妙地避免了多线程写入争用。与共享数据方式相比,消息传递机制的最大优点是不会产生数据竞争条件。常见的消息传递类型有两种:基于通道的(golang是典型代表)消息传递和基于actor的消息传递(erlang是代表)。Actor介绍Actor模型=数据+行为+消息。Actor模型是一种通用的并发编程模型,不属于某种语言或框架,几乎可以在任何编程语言中使用,其中最典型的就是erlang,它在语言层面上提供了对Actor模型的支持,一个杀手级应用RabbitMQ是基于erlang开发的。更多面向对象的Actor类似于面向对象编程(OO)中的对象,每个actor实例都封装了自己的相关状态,并且与其他actor在物理上是隔离的。举一个游戏玩家的例子,每个玩家都是Actor系统中ActorPlayer的一个实例,每个玩家都有自己的属性,比如Id、昵称、攻击力等,这些都体现在代码层面和我们的OO代码没有太大区别,在系统内存层面有多个OO实例classPlayerActor{publicintId{get;关注锁和内存原子性等一系列线程问题,而Actor模型内部状态是自己维护的,即它的内部数据只能自己修改(通过消息传递状态修改),所以使用并发编程的Actor模型可以很好的避免这些问题。Actor内部是以单线程的方式执行的,类似于redis,所以Actor完全可以实现类似于分布式锁的应用。异步每个Actor都有一个专门的MailBox来接收消息,这也是Actor实现异步的基础。当一个Actor实例向另一个Actor发送消息时,并不直接调用Actor的方法,而是将消息传递给对应的MailBox,就像邮递员一样,不是直接将邮件投递给收件人,而是将其放入每个邮箱,以便邮递员可以快速转移到下一个工作。所以在Actor系统中,一个Actor发送消息是非常快的。这种设计的主要优点是解耦了actor,数以万计的actor并发运行,每个actor按照自己的步调运行,收发消息不会阻塞。隔离每个Actor实例维护自己的状态,并与其他Actor实例物理隔离,不像多线程+锁模式那样基于共享数据。Actor通过消息方式与其他Actor进行通信,不同于OO式的消息传递方式。Actors之间的消息传递是真正的物理消息传递。本质上是分布式的,每个Actor实例的位置是透明的,无论Actor地址是在本地机器还是远程机器,对于代码来说都是一样的。每个Actor实例都非常小,最多也就几百个字节,所以很容易在一台机器上创建几十万个Actor实例。如果你写过golang代码,你会发现Actor其实在权重上和Goroutine非常相似。由于位置透明,Actor系统可以随意横向扩展来处理并发。对于调用者来说,被调用Actor的位置是本地的。当然,这也得益于Actor系统强大的路由系统。生命周期每个Actor实例都有自己的生命周期,就像C#java中的GC机制一样。对于需要淘汰的Actor,系统会销毁释放内存等资源,保证系统的连续性。其实在actor系统中,actor的销毁可以人工干预,也可以由系统自动销毁。容错性说到Actor的容错性,不得不说是相当让人意外的。传统的编程方式是在以后可能出现异常的地方捕获异常,以保证系统的稳定性。这就是所谓的防御性编程。但是防御性编程也有其自身的缺点。与现实类似,防御方永远无法100%防御未来所有可能的代码缺陷。比如java代码中很多地方都充斥着判断一个变量是否为nil。这些是最典型的防御性编码案例。但是Actor模型的程序并不进行防御性编程,而是遵循“让它崩溃”的哲学,让Actor的管理者来处理这些崩溃。例如,在一个actorcrash之后,manager可以选择创建一个新的实例或者记录一个日志。每个actor的crash或exception信息可以反馈给manager,保证了actor系统管理每个actor实例的灵活性。缺点没有完美的语言,框架/模型也是如此。Actor作为一种分布式并发模型,也有其不足之处。由于同一类型的Actor对象分散在多个主机中,收集多个Actor对象是一个弱点。例如,在电子商务系统中,商品是一类Actor。大多数情况下,查询一个商品列表会经历以下过程:首先根据查询条件筛选出一系列商品id,根据商品id得到一个商品Actor列表(很可能生成产品搜索服务,无论是使用es还是其他搜索引擎)。如果金额很大,就有网络风暴的危险(虽然概率很小)。在实时性要求不是太高的情况下,其实可以独立创建产品Actor列表,通过MQ接收产品信息修改信号来处理数据一致性问题。在很多情况下,在基于actor模型的分布式系统中,缓存很可能是一个进程内缓存,这意味着每个actor在进程中实际上保存了自己的状态信息。业界通常将这种服务称为有状态服务。但是每个Actor都有自己的生命周期,会不会有问题呢?呵呵,也许吧。想一想,我们以一个产品为例。如果环境是非Actor并发模型,productcache可以使用LRU策略淘汰不活跃的productcache,保证内存不会被过度使用。如果是基于Actor模型的进程内缓存呢?,每个actor其实就是cache本身,要用LRU策略来保证内存使用不是那么容易的,因为actor的活跃状态你是不知道的。分布式东西的问题,其实这是所有分布式模型都会面临的问题,并不是因为Actor。还是以商品演员为例。添加商品时,商品actor和统计商品actor(很多时候确实设计成两类actor服务)需要保证事物的完整性和数据的一致性。在很多情况下,可以牺牲实时一致性来保证最终的一致性。每个Actor的邮箱可能堆满了也可能满了。当出现这种情况时,新消息的处理方法应该被丢弃或等待。因此,在设计Actor系统时,需要注意邮箱的设计。升华通过上面的介绍,由于Actor对位置是透明的,所以任何一个Actor对于其他Actor来说就好像是本地的一样。基于这个特性,我们可以做很多事情。在传统的分布式系统中,服务器A要与服务器B通信,要么调用RPC(http调用不常用),要么使用MQ系统。但是在Actor系统中,服务器之间的通信已经变得非常优雅了。虽然本质上也是一个RPC调用,但是在coder看来好像调用了一个本地函数。事实上,现在Streaming方式更流行。由于Actor系统的执行模型是单线程异步的,任何类似的有资源竞争的功能都非常适合Actor模型,比如秒杀activity。基于上面的介绍,Actor模型在设计层面天生就支持负载均衡,并且支持水平扩展很好。当然Actor的分布式系统还需要服务注册中心。Actor虽然是单线程执行模型,但并不代表每个Actor都需要占用一个线程。实际上,在Actor上执行的任务就像Golang的goroutine一样,可以是一个轻量级的东西,一个宿主Actor上的所有任务可以共享一个线程池,保证了业务代码的最大化,同时使用最少的线程资源。
