1、分布式理论
1.1 分布式架构系统回顾
1.1.1 分布式系统概念
分布式系统
- 分布式系统是一个硬件或软件组件分布在不同的网络计算机上,彼此之间仅仅通过消息传递进行通信和协调的系统
- 通俗的理解,所谓分布式系统,就是一个业务拆分成多个子业务,分布在不同的服务器节点,共同构成的系统称为分布式系统
- 同一个分布式系统中的服务器节点在空间部署上是可以随意分布的
- 这些服务器可能放在不同的机柜中,也可能在不同的机房中,甚至分布在不同的城市
分布式与集群的区别
- 集群:多个人在一起作同样的事
- 分布式 :多个人在一起作不同的事
- 集群和分布式的对比图
分布式系统的特点
- 分布性:分布式系统最显著的特点肯定就是分布性,以电商网站为例不同的微服务(如用户微服务、产品微服务,订单微服务等)部署在不同的tomcat中,不同的服务器中,甚至不同的集群中,整个架构都是分布在不同的地方的,在空间上是随意的,而且随时会增加,删除服务器节点
- 对等性:对等性是分布式设计的一个目标,还是以电商网站为例,比如订单服务,为了防止订单服务出现问题就需要2个或者以上的订单服务,这些服务之间完全是对等的,功能是完全一致的
- 并发性:一个分布式系统中的多个节点,可能会并发地操作一些共享资源
- 缺乏全局时钟:在分布式系统中节点可能是任意位置的,每个节点都有自己的时间系统,很难定义两个事务究竟谁先谁后,通过时间服务器解决
- 故障总是会发生:任何一个节点都可能出现停电、死机等现象,服务器集群越多出现故障的可能性就越大,怎么样保证在系统某些节点出现故障的情况下分布式系统是可用的,也是分布式系统架构应该考虑的
1.1.2 分布式系统的发展
什么是IOE
- I指的是IBM小型机
- O指的是Oracle数据库
- E指的是EMC的高端存储
为什么要去IOE
- 升级单机处理能力的性价比越来越低
- 单机处理能力存在瓶颈
- 稳定性和可用性这两个指标很难达到
1.1.3 分布式架构的演变
- 阶段一:单体应用架构
- 阶段二:应用服务器与数据服务器分离
- 阶段三:应用服务器集群
- 阶段四:应用服务器负载用户请求
- 阶段五:数据库读写分离
- 阶段六:添加所搜引擎解决读库的压力
- 阶段七:添加缓存机制解决数据库压力
- 阶段八:数据库的水平/垂直拆分
- 阶段九:应用拆分
- 阶段十:服务化
1.2 分布式系统面临的问题
1.2.1 通信异常
- 网络本身的不可靠性,因此每次网络通信都会伴随着网络不可用的风险
- 光纤、路由、DNS等硬件设备或系统的不可用等都会导致最终分布式系统无法顺利进行一次网络通信
- 即使分布式系统各节点之间的网络通信能够正常执行,其延时也会大于单机操作,存在巨大的延时差别,也会影响消息的收发过程,因此消息丢失和消息延迟变的非常普遍
1.2.2 网络分区
- 网络之间出现了网络不连通,但各个子网络的内部网络是正常的,从而导致整个系统的网络环境被切分成了若干个孤立的区域
- 分布式系统会出现局部小集群
- 在极端情况下,这些小集群会独立完成原本需要整个分布式系统才能完成的功能,包括数据的事务处理,这就对分布式一致性提出非常大的挑战
1.2.3 节点故障
- 节点故障是分布式系统下另一个比较常见的问题,指的是组成分布式系统的服务器节点出现的宕机或"僵死"现象
- 根据经验来说,每个节点都有可能出现故障,并且经常发生
1.2.4 三态
- 分布式系统每一次请求与响应存在特有的"三态"概念(成功、失败和超时)
- 分布式系统中,由于网络是不可靠的,虽然绝大部分情况下,网络通信能够接收到成功或失败的响应,但当网络出现异常的情况下,就会出现超时现象
- 通常有两种情况导致超时:
1)由于网络原因,该请求并没有被成功的发送到接收方,而是在发送过程就发生了丢失现象
2)该请求成功的被接收方接收后,并进行了处理,但在响应反馈给发送方过程中,发生了消息丢失现象
1.3 分布式理论:一致性
1.3.1 什么是分布式一致性
分布式数据一致性,指的是数据在多份副本中存储时,各副本中的数据是一致的
1.3.2 副本一致性
- 如果是一台数据库处理所有的数据请求,那么通过ACID四原则,基本可以保证数据的一致性
- 分布式系统当中,数据往往会有多个副本。这就带来了同步的问题,因为我们几乎没有办法保证可以同时更新所有机器当中的包括备份所有数据
- 网络延迟,即使我在同一时间给所有机器发送了更新数据的请求,也不能保证这些请求被响应的时间保持一致存在时间差,就会存在某些机器之间的数据不一致的情况
- 总得来说,我们无法做到既保证数据的一致性,同时又不影响系统运行的性能。于是,一致性级别由此诞生(一致性分类)
1.3.3 一致性分类
强一致
这种一致性级别是最符合用户直觉的,它要求系统写入什么,读出来的也会是什么,用户体验好,但实现起来往往对系统的性能影响大。但是强一致性很难实现。
弱一致
这种一致性级别约束了系统在写入成功后,不承诺立即可以读到写入的值,也不承诺多久之后数据能够达到一致,但会尽可能地保证到某个时间级别(比如秒级别)后,数据能够达到一致状态。
- 读写一致性:用户读取自己写入结果的一致性,保证用户永远能够第一时间看到自己更新的内容
方案1:一种方案是对于一些特定的内容我们每次都去主库读取
方案2:我们设置一个更新时间窗口,在刚刚更新的一段时间内,我们默认都从主库读取,过了这个窗口之后,我们会挑选最近有过更新的从库进行读取
方案3:我们直接记录用户更新的时间戳,在请求的时候把这个时间戳带上,凡是最后更新时间小于这个时间戳的从库都不予以响应 - 单调读一致性:本次读到的数据不能比上次读到的旧
解决方案:就是根据用户ID计算一个hash值,再通过hash值映射到机器。同一个用户不管怎么刷新,都只会被映射到同一台机器上。这样就保证了不会读到其他从库的内容,带来用户体验不好的影响。 - 因果一致性:指的是:如果节点 A 在更新完某个数据后通知了节点 B,那么节点 B 之后对该数据的访问和修改都是基于 A 更新后的值。于
此同时,和节点 A 无因果关系的节点 C 的数据访问则没有这样的限制。 - 最终一致性:最终一致性是所有分布式一致性模型当中最弱的。不考虑所有的中间状态的影响,只保证当没有新的更新之后,经过一段时间之后,最终系统内所有副本的数据是正确的。它最大程度上保证了系统的并发能力,也因此,在高并发的场景下,它也是使用最广的一致性模型。
1.4 分布式理论:CAP定理
1.4.1 CAP 定理
- CAP定理是指一个分布式系统不可能同时满足一致性(C:Consistency),可用性(A: Availability)和分区容错性(P:Partition tolerance)这三个基本需求,最多只能同时满足其中的2个
- CAP分别解读
选项 | 描述 |
C 一致性 | 分布式系统当中的一致性指的是所有节点的数据一致,或者说是所有副本的数据一致 |
A 可用性 | Reads and writes always succeed. 也就是说系统一直可用,而且服务一直保持正常 |
P 分区容错性 | 系统在遇到一些节点或者网络分区故障的时候,仍然能够提供满足一致性和可用性的服务 |
- CAP只能3选2
- 三种排列组合
选择 | 描述 |
舍弃A(可用性),保留CP(一致性和分区容错性) | 一个系统保证了一致性和分区容错性,舍弃可用性。也就是说在极端情况下,允许出现系统无法访问的情况出现,这个时候往往会牺牲用户体验,让用户保持等待,一直到系统数据一致了之后,再恢复服务。 |
舍弃C(一致性),保留AP(可用性和分区容错性) | 这种是大部分的分布式系统的设计,保证高可用和分区容错,但是会牺牲一致性。 |
舍弃P(分区容错性),保留CA(一致性和可用性) | 如果要舍弃P,那么就是要舍弃分布式系统,CAP也就无从谈起了。可以说P是分布式系统的前提,所以这种情况是不存在的。 |
1.5 分布式理论:BASE理论
1.5.1 什么是BASE理论
- BASE全称:Basically Available(基本可用)、Soft state(软状态)、 Eventually consistent(最终一致性)三个短语的缩写,来自 ebay 的架构师提出。
- BASE是对CAP中一致性和可用性权衡的结果,BASE理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。
1.5.2 Basically Available(基本可用)
- 基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性,但这绝不等价于系统不可用
- 响应时间上的损失:正常情况下一个在线搜索引擎需要在0.5秒之内返回给用户相应的查询结果,但由于出现故障(比如系统部分机房发生断电或断网故障),查询结果的响应时间增加到了1~2秒
- 功能上的损失:正常情况下,在一个电子商务网站(比如淘宝)上购物,消费者几乎能够顺利地完成每一笔
订单。但在一些节日大促购物高峰的时候(比如双十一、双十二),由于消费者的购物行为激增,为了保护
系统的稳定性(或者保证一致性),部分消费者可能会被引导到一个降级页面
1.5.3 Soft state(软状态)
- 硬状态:相对于一致性,要求多个节点的数据副本都是一致的
- 软状态:允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本之间进行数据同步的过程中存在延迟
1.5.4 Eventually consistent(最终一致性)
- 最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态
- 最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性
1.6 分布式理论:分布式事务
1.6.1 数据库事务
事务有4个非常重要的特性,即我们常说的(ACID)
- Atomicity(原子性):是说事务是一个不可分割的整体,所有操作要么全做,要么全不做;只要事务中有一个操作出错,回滚到事务开始前的状态的话,那么之前已经执行的所有操作都是无效的,都应该回滚到开始前的状态
- Consistency(一致性):是说事务执行前后,数据从一个状态到另一个状态必须是一致的,比如A向B转账(A、B的总金额就是一个一致性状态),不可能出现A扣了钱,B却没收到的情况发生
- Isolation(隔离性):多个并发事务之间相互隔离,不能互相干扰。并发事务是指两个事务操作了同一份数据的情况;而对于并发事务操作同一份数据的隔离性问题,则是要求不能出现脏读、幻读的情况,而为了解决这个问题,常用的手段就是加锁了,对于数据库来说就是通过数据库的相关锁机制来保证
- Durablity(持久性):事务完成后,对数据库的更改是永久保存的
1.6.2 分布式事务
- 其实分布式事务从实质上看与数据库事务的概念是一致的,既然是事务也就需要满足事务的基本特性(ACID)
- 分布式事务相对于本地事务而言其表现形式有很大的不同
1.7 分布式理论:一致性协议2PC
1.7.1 什么是一致性协议2PC
2PC ( Two-Phase Commit缩写)即两阶段提交协议,是将整个事务流程分为两个阶段,准备阶段(Preparephase)、提交阶段(commit phase),2是指两个阶段,P是指准备阶段,C是指提交阶段
1.7.2 一致性协议2PC的两个阶段过程
- 准备阶段(Prepare phase):事务管理器给每个参与者发送Prepare消息,每个数据库参与者在本地执行事务,并写本地的Undo/Redo日志,此时事务没有提交。 (Undo日志是记录修改前的数据,用于数据库回滚,Redo日志是记录修改后的数据,用于提交事务后写入数 据文件)
- 提交阶段(commit phase):如果事务管理器收到了参与者的执行失败或者超时消息时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据事务管理器的指令执行提交或者回滚操作,并释放事务处理过程中使用的锁资源。注意:必须在最后阶段释放锁资源
1.7.3 一致性协议2PC执行流程
- 执行成功的流程
- 执行中断的流程
1.7.3 一致性协议2PC的优缺点
- 优点:原理简单,实现方便
- 缺点:同步阻塞,单点问题,数据不一致,过于保守
1)同步阻塞:二阶段提交协议存在最明显也是最大的一个问题就是同步阻塞,在二阶段提交的执行过程中,所有参与该事务操作的逻辑都处于阻塞状态,也就是说,各个参与者在等待其他参与者响应的过程中,无法进行其他操作。这种同步阻塞极大的限制了分布式系统的性能
2)单点问题:协调者在整个二阶段提交过程中很重要,如果协调者在提交阶段出现问题,那么整个流程将无法运转,更重要的是:其他参与者将会处于一直锁定事务资源的状态中,而无法继续完成事务操作
3)数据不一致:假设当协调者向所有的参与者发送 commit 请求之后,发生了局部网络异常或者是协调者在尚未发送完所有 commit请求之前自身发生了崩溃,导致最终只有部分参与者收到了 commit 请求。这将导致严重的数据不一致问题
4)过于保守:二阶段提交协议没有设计较为完善的容错机制,任意一个节点失败都会导致整个事务的失败
1.8 分布式理论:一致性协议3PC
1.8.1 什么是一致性协议3PC
- 一致性协议3PC全称 “three phase commit”,是 2PC 的改进版,将 2PC 的 “提交事务请求” 过程一分为二,共形成了由CanCommit、PreCommit和doCommit三个阶段组成的事务处理协议
- 流程图:
1.8.2 一致性协议3PC的3个阶段
- 阶段一:CanCommit
1)事务询问:协调者向所有的参与者发送一个包含事务内容的canCommit请求,询问是否可以执行事务提交操作,并开始等待各参与者的响应
2)各参与者向协调者反馈事务询问的响应:参与者在接收到来自协调者的包含了事务内容的canCommit请求后,正常情况下,如果自身认为可以顺利执行事务,则反馈Yes响应,并进入预备状态,否则反馈No响应 - 阶段二:PreCommit
情况一:执行事务预提交
1)发送预提交请求:协调者向所有参与者节点发出preCommit请求,并进入prepared阶段
2)事务预提交:参与者接收到preCommit请求后,会执行事务操作,并将Undo和Redo信息记录到事务日志中
3)各参与者向协调者反馈事务执行的结果:若参与者成功执行了事务操作,那么反馈Ack
情况二:中断事务
1)发送中断请求:协调者向所有参与者发出abort请求
2)中断事务:无论是收到来自协调者的abort请求或者等待协调者请求过程中超时,参与者都会中断事务 - 阶段三:doCommit
情况一:事务提交
1)发送提交请求:进入这一阶段,假设协调者处于正常工作状态,并且它接收到了来自所有参与者的Ack响应,那么他将从预提交状态转化为提交状态,并向所有的参与者发送doCommit请求
2)事务提交:参与者接收到doCommit请求后,会正式执行事务提交操作,并在完成提交之后释放整个事务执行过程中占用的事务资源
3)反馈事务提交结果:参与者在完成事务提交后,向协调者发送Ack响应
4)完成事务:协调者接收到所有参与者反馈的Ack消息后,完成事务
情况二:事务回滚
1)发送中断请求:协调者向所有的参与者节点发送abort请求
2)事务回滚:参与者收到abort请求后,会根据记录的Undo信息来执行事务回滚,并在完成回滚之后释放整个事务执行期间占用的资源
3)反馈事务回滚结果:参与者在完成事务回滚后,向协调者发送Ack消息
4)中断事务:协调者接收到所有参与者反馈的Ack消息后,中断事务
1.8.3 2PC与3PC对比
- 在2PC中,只有协调者拥有超时机制,即如果在一定时间内没有收到参与者的消息则默认失败;3PC对于协调者和参与者都设置了超时机制,主要是避免了参与者在长时间无法与协调者节点通讯(协调者挂掉了)的情况下无法释放资源的问题,而这种机制也侧面降低了整个事务的阻塞时间和范围
- 通过CanCommit、PreCommit、DoCommit三个阶段的设计,相较于2PC而言,多设置了一个缓冲阶段保证了在最后提交阶段之前各参与节点的状态是一致的
- PreCommit是一个缓冲,保证了在最后提交阶段之前各参与节点的状态是一致的
- 3PC协议也没有完全解决数据不一致问题
1.9 分布式理论:一致性算法Paxos
1.9.1 什么是Paxos算法
Paxos算法是Lamport提出的一种基于消息传递的分布式一致性算法
1.9.2 Paxos解决了什么问题
- 解决了分布式系统一致性问题
- Paxos算法需要解决的问题就是如何在一个可能发生异常的分布式系统中,快速且正确地在集群内部对某个数据的值达成一致
- 这里某个数据的值并不只是狭义上的某个数,它可以是一条日志,也可以是一条命令(command)。。。根据应用场景不同,某个数据的值有不同的含义
1.9.3 Paxos相关概念
- 提案(Proposal):Proposal信息包括提案编号 (Proposal ID) 和提议的值 (Value)
- 客户端(Client):客户端向分布式系统发出请求并等待响应,例如,对分布式文件服务器中文件的写请求
- 提案发起者(Proposer):提案者提倡客户请求,试图说服Acceptor对此达成一致,并在发生冲突时充当协调者以推动协议向前发展
- 决策者(Acceptor):Acceptor可以接受(accept)提案;如果某个提案被选定(chosen),那么该提案里的value就被选定了
- 最终决策的学习者(Learners):学习者充当该协议的复制因素
1.9.4 问题描述
假设有一组可以提出提案的进程集合,那么对于一个一致性算法需要保证以下几点
- 在这些被提出的提案中,只有一个会被选定
- 如果没有提案被提出,就不应该有被选定的提案
- 当一个提案被选定后,那么所有进程都应该能学习(learn)到这个被选定的value
1.9.5 Paxos算法描述
算法执行过程
- 阶段一
1)Proposer选择一个提案编号N,然后向半数以上的Acceptor发送编号为N的Prepare请求。
2)如果一个Acceptor收到一个编号为N的Prepare请求,且N大于该Acceptor已经响应过的所有Prepare请求的编号,那么它就会将它已经接受过的编号最大的提案(如果有的话)作为响应反馈给Proposer,同时该Acceptor承诺不再接受任何编号小于N的提案。 - 阶段二
1)如果Proposer收到半数以上Acceptor对其发出的编号为N的Prepare请求的响应,那么它就会发送一个针对[N,V]提案的Accept请求给半数以上的Acceptor。注意:V就是收到的响应中编号最大的提案的value,如果响应中不包含任何提案,那么V就由Proposer自己决定
2)如果Acceptor收到一个针对编号为N的提案的Accept请求,只要该Acceptor没有对编号大于N的Prepare请求做出过响应,它就接受该提案
1.9.6 Learner学习被选定的value
1.9.7 如何保证Paxos算法的活性
- 通过选取主Proposer,并规定只有主Proposer才能提出议案
- 这样一来只要主Proposer和过半的Acceptor能够正常进行网络通信,那么但凡主Proposer提出一个编号更高的提案,该提案终将会被批准
- 这样通过选择一个主Proposer,整套Paxos算法就能够保持活性
1.10 分布式理论:一致性算法Raft
1.10.1 什么是Raf算法
- 概念:Raft是一种为了管理复制日志的一致性算法
- Raft算法将一致性算法分解成了3模块:
领导人选举
日志复制
安全性 - Raft算法分为两个阶段,首先是选举过程,然后在选举出来的领导人带领进行正常操作,比如日志复制等
1.10.2 领导人Leader选举
- Raft通过选举一个领导人,然后给予他全部的管理复制日志的责任来实现一致性
- 在Raft中任何一个服务器都可以扮演下面的角色之一
领导者(leader):处理客户端交互,日志复制等动作,一般一次只有一个领导者
候选者(candidate):候选者就是在选举过程中提名自己的实体,一旦选举成功,则成为领导者
跟随者(follower):类似选民,完全被动的角色,这样的服务器等待被通知投票 - 选举的过程
➢初始状态下集群中的所有节点都处于 follower 状态
➢某一时刻,其中的一个 follower 由于没有收到 leader 的 heartbeat 率先发生 election timeout 进而发起选举
➢只要集群中超过半数的节点接受投票,candidate 节点将成为即切换 leader 状态
➢成为 leader 节点之后,leader 将定时向 follower 节点同步日志并发送 heartbeat
1.10.3 节点异常
节点的异常大致可以分为四种类型:
- loader不可用
- follower不可用
- 多个candidate或者多个leader
- 新节点加入集群
1.10.3.1 leader不可用
➢一般情况下,leader 节点定时发送 heartbeat 到 follower 节点
➢由于某些异常导致leader不再发送heartbeat,或follower无法收到heartbeat
➢当某一follower发生election timeout时,其状态变更为candidate,并向其他follower发起投票
➢当超过半数的follower接受投票后,这一节点将成为新的leader,leader的步进数加1,并开始向follower同步日志
➢当一段时间之后,如果之前的leader再次加入集群,则两个leader比较彼此的步进数,步进数低的leader将切换自己的状态为follower
➢较早前leader中不一致的日志将被清除,并与现有leader中的日志保持一致
1.10.3.2 follower不可用
➢集群中的某个follower节点发生异常,不再同步日志以及接收heartbeat
➢经过一段时间之后,原来的follower节点重新加入集群
➢这一节点的日志将从当时的leader处同步
1.10.3.3 多个candidate或者多个leader
➢初始状态下集群中的所有节点都处于follower状态
➢两个节点同时成为candidate发起选举
➢两个candidate都只得到了少部分follower的接受投票
➢candidate继续向其他的follower询问
➢由于一些follower已经投过票了,所以均返回拒绝接受
➢candidate也可能向一个candidate询问投票
➢在步进数相同的情况下,candidate将拒绝接受另一个candidate的请求
➢由于第一次未选出leader,candidate将随机选择一个等待间隔(150ms ~ 300ms)再次发起投票
➢如果得到集群中半数以上的follower的接受,这一candidate将成为leader
➢稍后另一个candidate也将再次发起投票
➢由于集群中已经选出leader,candidate将收到拒绝接受的投票
➢在被多数节点拒绝之后,并已知集群中已存在leader后,这一candidate节点将终止投票请求、切换为follower,从leader节点同步日志
1.10.3.4 新节点加入集群
➢新来的follower节点加入集群,此时已经有了leader
➢这一节点的日志将从当时的leader处同步
1.10.4 日志复制(保证数据一致性)
- 日志复制的过程:Leader选出后,就开始接收客户端的请求。Leader把请求作为日志条目(Log entries)加入到它的日志中,然后并行的向其他服务器发起AppendEntries RPC复制日志条目。当这条日志被复制到大多数服务器上,Leader将这条日志应用到它的状态机并向客户端返回执行结果
- 过程图
- 过程步骤描述
➢客户端的每一个请求都包含被复制状态机执行的指令
➢leader把这个指令作为一条新的日志条目添加到日志中,然后并行发起RPC给其他的服务器,让他们复制这条信息
➢跟随者响应ACK,如果follower宕机或者运行缓慢或者丢包,leader会不断的重试,直到所有的follower最终都复制了所有的日志条目
➢通知所有的Follower提交日志,同时领导人提交这条日志到自己的状态机中,并返回给客户端
2、分布式系统设计策略
2.1 心跳检测
2.1.1 概念
- 心跳,顾名思义就是以固定的频率向其他节点汇报当前节点状态的方式
- 收到心跳,一般可以认为一个节点和现在的网络拓扑是良好的当
- 心跳汇报时,一般也会携带一些附加的状态、元数据信息,以便管理
2.1.2 流程图
如下图所示,Client请求Server,Server转发请求到具体的Node获取请求结果。Server需要与三个Node节点保持心跳连接,确保Node可以正常工作
注:收到心跳可以确认节点正常,但是收不到心跳也不能认为该节点就已经宣告“死亡”。此时,可以通过一些方法帮助Server做决定: 周期检测心跳机制、累计失效检测机制
2.1.3 周期检测心跳机制
Server端每间隔t秒向Node集群发起监测请求,设定超时时间,如果超过超时时间,则判断"死亡"
2.1.4 累计失效检测机制
- 在周期检测心跳机制的基础上,统计一定周期内节点的返回情况(包括超时及正确返回),以此计算节点的"死亡"概率
- 对于宣告"濒临死亡"的节点可以发起有限次数的重试,以作进一步判断
- 通过周期检测心跳机制、累计失效检测机制可以帮助判断节点是否"死亡",如果判断"死亡"可以把该节点踢出集群
2.2 高可用设计
2.2.1 概念
- 高可用(High Availability)是系统架构设计中必须考虑的因素之一,通常是指经过设计来减少系统不能提供服务的时间
- 系统高可用性的常用设计模式包括三种:主备(Master-SLave)、互备(Active-Active)、集群(Cluster)
2.2.2 主备模式(Master-Slave)
- 主备模式就是Active-Standby模式
- 当主机宕机时,备机接管主机的一切工作
- 待主机恢复正常后,按使用者的设定以自动(热备)或手动(冷备)方式将服务切换到主机上运行
- 在数据库部分,习惯称之为MS模式。MS模式即Master/Slave模式,这在数据库高可用性方案中比较常用,如MySQL、Redis等就采用MS模式实现主从复制
2.2.3 互备模式(Active-Active)
- 互备模式指两台主机同时运行各自的服务工作且相互监测情况
- 在数据库高可用部分,常见的互备是MM模式。MM模式即Multi-Master模式
- 指一个系统存在多个master,每个master都具有read-write能力,会根据时间戳或业务逻辑合并版本
2.2.3 集群模式(Cluster)
- 集群模式是指有多个节点在运行,同时可以通过主控节点分担服务请求如Zookeeper
- 集群模式需要解决主控节点本身的高可用问题,一般采用主备模式
2.3 容错性
2.3.1 概念
- 容错顾名思义就是IT系统对于错误包容的能力
- 容错的处理是保障分布式环境下相应系统的高可用或者健壮性,一个典型的案例就是对于缓存穿透问题的解决方案
2.3.2 大量发起缓存中不存在的查询问题
- 一个比较巧妙的方法是,可以将这个不存在的key预先设定一个值。比如,key=“null”
- 在返回这个null值的时候,我们的应用就可以认为这是不存在的key,那我们的应用就可以决定是否继续等待访问,还是放弃掉这次操作。
- 如果继续等待访问,过一个时间轮询点后,再次请求这个key,如果取到的值不再是null,则可以认为这时候key有值了,从而避免了透传到数据库,把大量的类似请求挡在了缓存之中
2.4 负载均衡
2.4.1 概念
- 负载均衡:其关键在于使用多台集群服务器共同分担计算任务,把网络请求及计算分配到集群可用的不同服务器节点上,从而达到高可用性及较好的用户操作体验
2.4.2 负载均衡解决方案
- 硬件解决方案:F5
- 软件解决方案:LVS、HAProxy、Nginx等
2.4.2 Nginx的负载均衡策略
- 轮询:即Round Robin,根据Nginx配置文件中的顺序,依次把客户端的Web请求分发到不同的后端服务器
- 最少连接:当前谁连接最少,分发给谁
- IP地址哈希:确定相同IP请求可以转发给同一个后端节点处理,以方便session保持
- 基于权重的负载均衡:配置Nginx把请求更多地分发到高配置的后端服务器上,把相对较少的请求分发到低配服务器
3、分布式架构网络通信
在分布式服务框架中,一个最基础的问题就是远程服务是怎么通讯的,在Java领域中有很多可实现远程通讯的技术,例如:RMI、Hessian、SOAP、ESB和JMS等
3.1 基本原理
- 要实现网络机器间的通讯,首先得来看看计算机系统网络通信的基本原理
- 在底层层面去看,网络通信需要做的就是将流从一台计算机传输到另外一台计算机,基于传输协议和网络IO来实现
- 其中传输协议比较出名的有tcp、udp等等,tcp、udp都是在基于Socket概念上为某类应用场景而扩展出的传输协议
- 网络IO,主要有bio、nio、aio三种方式,所有的分布式应用通讯都基于这个原理而实现,只是为了应用的易用,各种语言通常都会提供一些更为贴近应用易用的应用层协议
3.2 详解RPC
3.2.1 概念
- RPC全称为remote procedure call,即远程过程调用
- 借助RPC可以做到像本地调用一样调用远程服务,是一种进程间的通信方式
- 比如两台服务器A和B,A服务器上部署一个应用,B服务器上部署一个应用,A服务器上的应用想调用B服务器上的应用提供的方法,由于两个应用不在一个内存空间,不能直接调用,所以需要通过网络来表达调用的语义和传达调用的数据
- RPC并不是一个具体的技术,而是指整个网络远程调用过程
3.2.2 RPC架构
一个完整的RPC架构里面包含了四个核心的组件,分别是Client、Client Stub、Server、Server Stub;这里的Stub可以理解为存根
- 客户端(Client):服务的调用方
- 客户端存根(Client Stub):存放服务端的地址消息,再将客户端的请求参数打包成网络消息,然后通过网络远程发送给服务方
- 服务端(Server):真正的服务提供者
- 服务端存根(Server Stub):接收客户端发送过来的消息,将消息解包,并调用本地的方法
3.2.3 RPC调用过程
- 客户端(client)以本地调用方式(即以接口的方式)调用服务
- 客户端存根(client stub)接收到调用后,负责将方法、参数等组装成能够进行网络传输的消息体(将消息体对象序列化为二进制)
- 客户端通过sockets将消息发送到服务端
- 服务端存根(server stub)收到消息后进行解码(将消息对象反序列化)
- 服务端存根(server stub)根据解码结果调用本地的服务
- 本地服务执行并将结果返回给服务端存根(server stub)
- 服务端存根(server stub)将返回结果打包成消息(将结果消息对象序列化)
- 服务端(server)通过sockets将消息发送到客户端
- 客户端存根(client stub)接收到结果消息,并进行解码(将结果消息发序列化)
- 客户端(client)得到最终结果
3.2.4 java中的RPC框架
- 常见的RPC框架有Hessian、gRPC、Thrift、HSF (High Speed Service Framework)、Dubbo等
- 对于RPC框架而言,核心模块就是通讯和序列化
3.3 详解RMI
3.3.1 RMI简介
- Java RMI指的是远程方法调用(Remote Method Invocation),是java原生支持的远程调用
- 采用JRMP(Java Remote Messageing protocol)作为通信协议,可以认为是纯java版本的分布式远程调用解决方案
- RMI主要用于不同虚拟机之间的通信,这些虚拟机可以在不同的主机上、也可以在同一个主机上
- 这里的通信可以理解为一个虚拟机上的对象调用另一个虚拟机上对象的方法
3.3.2 RMI中的角色
- 客户端
1)存根/桩(Stub):远程对象在客户端上的代理
2)远程引用层(Remote Reference Layer):解析并执行远程引用协议
3)传输层(Transport):发送调用、传递远程方法参数、接收远程方法执行结果 - 服务端
1)骨架(Skeleton):读取客户端传递的方法参数,调用服务器方的实际对象方法,并接收方法执行后的返回值
2)远程引用层(Remote Reference Layer):处理远程引用后向骨架发送远程方法调用
3)传输层(Transport):监听客户端的入站连接,接收并转发调用到远程引用层 - 注册表(Registry):以URL形式注册远程对象,并向客户端回复对远程对象的引用
3.3.3 RMI的流程
远程调用过程
- 客户端从远程服务器的注册表中查询并获取远程对象引用
- 桩对象与远程对象具有相同的接口和方法列表,当客户端调用远程对象时,实际上是由相应的桩对象代理完成的
- 远程引用层在将桩的本地引用转换为服务器上对象的远程引用后,再将调用传递给传输层(Transport),由传输层通过TCP协议发送调用
- 在服务器端,传输层监听入站连接,它一旦接收到客户端远程调用后,就将这个引用转发给其上层的远程引用层
- 服务器端的远程引用层将客户端发送的远程应用转换为本地虚拟机的引用后,再将请求传递给骨架(Skeleton)
- 骨架读取参数,又将请求传递给服务器,最后由服务器进行实际的方法调用
结果返回过程
- 如果远程方法调用后有返回值,则服务器将这些结果又沿着"骨架->远程引用层->传输层"向下传递
- 客户端的传输层接收到返回值后,又沿着"传输层->远程引用层->桩"向上传递,然后由桩来反序列化这些返回值,并将最终的结果传递给客户端程序
3.3.4 开发流程
服务端
- 定义Remote子接口,在其内部定义要发布的远程方法,并且这些方法都要Throws RemoteException
- 定义实现远程接口,并且继承:UnicastRemoteObject
- 启动服务器:依次完成注册表的启动和远程对象绑定
客户端
- 通过符合JRMP规范的URL字符串在注册表中获取并强转成Remote子接口对象
- 调用这个Remote子接口对象中的某个方法就是为一次远程方法调用行为
3.3.5 代码实现
服务端
- 实体类
User.java
,需要实现序列化接口Serializable
package com.yuyz.entity;
import java.io.Serializable;
/**
* 引用对象应该是可序列化对象,这样才能在远程调用的时候<br/>
* 1. 序列化对象<br/>
* 2. 拷贝<br/>
* 3. 在网络中传输<br/>
* 4. 服务端反序列化<br/>
* 5. 获取参数进行方法调用<br/>
* 这种方式其实是将远程对象引用传递的方式转化为值传递的方式
*/
public class User implements Serializable {
private String name;
private Integer age;
public String getName() {
return name;
}
// 省略set/get方法和toString方法
- 接口
Hello.java
和实现类HelloImpl.java
package com.yuyz.server;
import com.yuyz.entity.User;
import java.rmi.Remote;
import java.rmi.RemoteException;
/**
* 远程服务对象接口必须继承Remote接口;同时方法必须抛出RemoteExceptino异常
*/
public interface Hello extends Remote {
/**
* 方法
* @param user
* @return
* @throws RemoteException
*/
public String sayHello(User user) throws RemoteException;
}
package com.yuyz.server.impl;
import com.yuyz.entity.User;
import com.yuyz.server.Hello;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class HelloImpl extends UnicastRemoteObject implements Hello {
public HelloImpl() throws RemoteException {
super();
}
public String sayHello(User user) throws RemoteException {
System.out.println("服务端收到请求,name:" + user.getName());
return "success";
}
}
- 服务端启动类
ServerMain.java
package com.yuyz;
import com.yuyz.server.Hello;
import com.yuyz.server.impl.HelloImpl;
import java.net.MalformedURLException;
import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
public class ServerMain {
public static void main(String[] args) {
try {
// 创建一个远程对象,同时也会创建stub对象、skeleton对象
Hello hello = new HelloImpl();
// 本地主机上的远程对象注册表Registry的实例,并指定端口为8888,这一步必不可少(Java默认端口是1099),必不可缺的一步,缺少注册表创建,则无法绑定对象到远程注册表上
LocateRegistry.createRegistry(8888);
// 绑定的URL标准格式为:rmi://host:port/name
try {
Naming.bind("//127.0.0.1:8888/hello", hello);
} catch (AlreadyBoundException e) {
e.printStackTrace();
} catch (MalformedURLException e) {
e.printStackTrace();
}
System.out.println("服务端启动完成!");
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
客户端
-
pom.xml
文件中引入服务端jar包
<dependency>
<groupId>com.yuyz</groupId>
<artifactId>rmi_server</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
- 客户端调用类
MyClient.java
package com.yuyz.client;
import com.yuyz.entity.User;
import com.yuyz.server.Hello;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
public class MyClient {
public static void main(String[] args) {
try {
// 在RMI服务注册表中查找名称为RHello的对象,并调用其上的方法
Hello hello = (Hello) Naming.lookup("//127.0.0.1:8888/hello");
User user = new User();
user.setName("zhangsan");
user.setAge(20);
String result = hello.sayHello(user);
System.out.println(result);
} catch (NotBoundException e) {
e.printStackTrace();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
3.4 详解BIO、NIO、AIO
3.4.1 同步和异步
同步(synchronize)、异步(asychronize)是指应用程序和内核的交互而言的
同步
- 指用户进程触发IO操作等待或者轮训的方式查看IO操作是否就绪
- 举例:银行取钱,我自己去取钱,取钱的过程中等待
异步
- 当一个异步进程调用发出之后,调用者不会立刻得到结果。而是在调用发出之后,被调用者通过状态、通知来通知调用者,或者通过回调函数来处理这个调用
- 使用异步IO时,Java将IO读写委托给OS处理,需要将数据缓冲区地址和大小传给OS,OS需要支持异步IO操作
- 举例:我请朋友帮我取钱,他取到钱后返回给我
3.4.2 阻塞和非阻塞
阻塞和非阻塞是针对于进程访问数据的时候,根据IO操作的就绪状态来采取不同的方式
阻塞
- 阻塞方式下读取和写入将一直等待
- 举例:ATM机排队取款,你只能等待排队取款
非阻塞
- 读取和写入方法会理解返回一个状态值
- 举例:柜台取款,取个号,然后坐在椅子上做其他事,等广播通知,没到你的号你就不能去,但你可以不断的问大堂经理排到了没有.
同步阻塞、同步非阻塞、异步阻塞、异步非阻塞的案例
3.4.3 BIO
3.4.3.1 BIO介绍
- 同步阻塞模式BIO:最简单的IO模型,用户线程在内核进行IO操作时如果数据没有准备好会被阻塞
- 服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理
3.4.3.2 BIO代码
服务端
package com.yuyz.server;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;
public class NIOServer {
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket();
SocketAddress socketAddress = new InetSocketAddress("127.0.0.1", 8081);
serverSocket.bind(socketAddress);
while (true) {
Socket socket = serverSocket.accept(); // 同步阻塞
System.out.println("服务端监听到请求");
Thread thread = new Handle(socket);
thread.start();
}
}
}
package com.yuyz.server;
import java.io.IOException;
import java.net.Socket;
public class Handle extends Thread {
private Socket socket;
public Handle(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
byte[] bytes = new byte[1024];
int len = socket.getInputStream().read(bytes); // 同步阻塞
System.out.println(new String(bytes, 0, len));
socket.getOutputStream().write(bytes, 0, len);
socket.getOutputStream().flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端
package com.yuyz.client;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
public class NIOClient {
public static void main(String[] args) throws Exception {
Socket socket = new Socket("127.0.0.1", 8081);
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
outputStream.write("hello".getBytes());
outputStream.flush();
byte[] bytes = new byte[1024];
int read = inputStream.read(bytes);
System.out.println(new String(bytes, 0, read));
}
}
3.4.4 NIO
3.4.4.1 NIO介绍
- 同步非阻塞IO(non-blocking IO / new io)
- 服务器实现模式为一个请求一个通道,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有IO请求时才启动一个线程进行处理
通道(Channels)
NIO新引入的最重要的抽象是通道的概念(Channel:数据连接的通道)。数据可以从Channel读到Buffer中,也可以从Buffer写到Channel中
缓冲区(Buffers)
通道channel可以向缓冲区Buffer中写数据,也可以响buffer中存数据
选择器(Selector)
使用选择器,借助单一线程就可对数量庞大的活动I/O通道实时监控和维护
3.4.4.2 NIO特点
- 当一个连接创建后,不会需要对应一个线程,这个连接会被注册到多路复用器
- 所以所有的连接需要一个线程就可以操作,该线程的多路复用器会轮训,发现连接有请求时,才开启一个线程处理
- NIO与IO对比
- 举例
3.4.5 AIO
- 异步非阻塞IO。A代表asynchronize
- 当有流可以读时,操作系统会将可以读的流传入read方法的缓冲区,并通知应用程序
- 对于写操作,OS将write方法的流写入完毕是操作系统会主动通知应用程序。
- 因此read和write都是异步的,完成后会调用回调函数。
- 使用场景:连接数目多且连接比较长(重操作)的架构,比如相册服务器。重点调用了OS参与并发操作,编程比较复杂。Java7开始支持
3.5 详解Netty
3.5.1 Netty认识
- Netty是由JBOSS提供一个异步的、基于事件驱动的网络编程框架
- Netty可以帮助你快速简单的开发出一个网络应用,相当于简化和流程化了NIO的开发过程
- 作为当前最流行的NIO框架,Netty在互联网领域、大数据分布式计算领域、游戏行业、通信行业等获得了广泛的应用
- 采用了Netty的框架:Elasticsearch和Dubbo
3.5.1 为什么要使用Netty
NIO缺点
- NIO的类库和API繁杂,使用麻烦。熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等
- 可靠性不强,开发工作量和难度都非常大
- NIO的Bug。例如Epoll Bug,它会导致Selector空轮询,最终导致CPU达到100%
Netty优点
- 对各种传输协议提供统一的 API
- 高度可定制的线程模型——单线程、一个或多个线程池
- 更好的吞吐量,更低的等待延迟
- 更少的资源消耗
- 最小化不必要的内存拷贝
3.5.2 线程模型
单线程模型
线程池模型
Netty模型
3.5.3 Netty核心组件
3.5.3.1 ChannelHandler 及其实现类
- ChannelHandler接口定义了许多事件处理的方法,我们可以通过重写这些方法去实现具体的业务逻辑
- 我们经常需要自定义一个Handler类去继承ChannelInboundHandlerAdapter, 然后通过重写相应方法实现业务逻辑
- 常用方法
// 通道就绪事件
public void channelActive(ChannelHandlerContext ctx)
// 通道读取数据事件
public void channelRead(ChannelHandlerContext ctx, Object msg)
// 数据读取完毕事件
public void channelReadComplete(ChannelHandlerContext ctx)
// 通道发生异常事件
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
3.5.3.2 ChannelPipeline
- ChannelPipeline是一个Handler的集合,它负责处理和拦截inbound或者outbound的事件和操作,相当于一个贯穿Netty的链
- 常用方法:
// 把一个业务处理类(handler)添加到链中的第一个位置
ChannelPipeline addFirst(ChannelHandler... handlers)
// 把一个业务处理类(handler)添加到链中的最后一个位置
ChannelPipeline addLast(ChannelHandler... handlers)
3.5.3.3 ChannelHandlerContext
- 这是事件处理器上下文对象,Pipeline链中的实际处理节点
- 每个处理节点ChannelHandlerContext中包含一个具体的事件处理器ChannelHandler
- 同时ChannelHandlerContext中也绑定了对应的pipeline和Channel的信息,方便对ChannelHandler进行调用
- 常用方法:
// 关闭通道
ChannelFuture close()
// 刷新
ChannelOutboundInvoker flush()
// 将数据写到ChannelPipeline中当前ChannelHandler 的下一个 ChannelHandler 开始处理(出站)
ChannelFuture writeAndFlush(Object msg)
3.5.3.4 ChannelFuture
- 表示Channel中异步I/O操作的结果,在Netty中所有的I/O操作都是异步的,I/O的调用会直接返回,调用者并不能立刻获得结果
- 可以通过ChannelFuture来获取I/O操作的处理状态
- 常用方法:
// 返回当前正在进行IO操作的通道
Channel channel()
// 等待异步操作执行完毕
ChannelFuture sync()
3.5.3.5 EventLoopGroup和其实现类NioEventLoopGroup
- EventLoopGroup是一组EventLoop的抽象,Netty为了更好的利用多核CPU资源,一般会有多个EventLoop同时工作,每个EventLoop维护着一个Selector实例
- EventLoopGroup提供next接口,可以从组里面按照一定规则获取其中一个EventLoop来处理任务
- 在Netty服务器端编程中,我们一般都需要提供两个EventLoopGroup,例如:BossEventLoopGroup和WorkerEventLoopGroup
- 常用方法:
// 构造方法
public NioEventLoopGroup()
// 断开连接, 关闭线程
public Future<?> shutdownGracefully()
3.5.3.6 ServerBootstrap和Bootstrap
- ServerBootstrap是Netty中的服务器端启动助手,通过它可以完成服务器端的各种配置
- Bootstrap是Netty中的客户端启动助手,通过它可以完成客户端的各种配置
- 常用方法:
// 该方法用于服务器端,用来设置两个EventLoop
public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup)
// 该方法用于客户端, 用来设置一个 EventLoop
public B group(EventLoopGroup group)
// 该方法用来设置一个服务器端的通道实现
public B channel(Class<? extends C> channelClass)
// 用来给ServerChannel添加配置
public <T> B option(ChannelOption<T> option, T value)
// 用来给接收到的 通道添加配置
public <T> ServerBootstrap childOption(ChannelOption<T> childOption, T value)
// 该方法用来设置业务处理类(自定 义的handler)
public ServerBootstrap childHandler(ChannelHandler childHandler)
// 该方法用于服务器端, 用来设置占用的端口号
public ChannelFuture bind(int inetPort)
// 该方法用于客户端, 用来连接服务器端
public ChannelFuture connect(String inetHost, int inetPort)
3.5.4 Netty版案例实现
- 引入依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.6.Final</version>
</dependency>
- 服务端
NettyServer.java
package com.yuyz.server;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
// 接受客户端请求
public class NettyServer {
public static void main(String[] args) throws InterruptedException {
// 1.创建 NioEventLoopGroup的两个实例:bossGroup workerGroup
// bossGroup接收客户端传过来的请求
EventLoopGroup bossGroup = new NioEventLoopGroup();
// workerGroup处理请求
EventLoopGroup workerGroup = new NioEventLoopGroup();
// 2、创建服务启动辅助类:组装一些必要的组件
ServerBootstrap serverBootstrap = new ServerBootstrap();
// 设置组,第一个bossGroup负责连接, workerGroup负责连接之后的io处理
serverBootstrap
.group(bossGroup, workerGroup)
// channel方法指定服务器监听的通道类型
.channel(NioServerSocketChannel.class)
// 设置channel handler , 每一个客户端连接后,给定一个监听器进行处理
.childHandler(
new ChannelInitializer<NioSocketChannel>() {
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
// 传输通道
ChannelPipeline pipeline = nioSocketChannel.pipeline();
// 在通道上添加对通道的处理器 , 该处理器可能还是一个监听器
pipeline.addLast(new StringEncoder());
pipeline.addLast(new StringDecoder());
// 监听器队列上添加我们自己的处理方式
pipeline.addLast(
new SimpleChannelInboundHandler<String>() {
protected void channelRead0(
ChannelHandlerContext channelHandlerContext, String msg)
throws Exception {
System.out.println(msg);
System.out.println("server 接收到消息:" + msg);
}
});
}
});
ChannelFuture f = serverBootstrap.bind(8000).sync();
System.out.println("服务端启动 完成!");
f.channel().closeFuture().sync();
}
}
- 客户端
NettyClient.java
package com.yuyz.client;
import java.util.Date;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringEncoder;
public class NettyClient {
public static void main(String[] args) throws InterruptedException {
// 线程池的实例
NioEventLoopGroup group = new NioEventLoopGroup();
// 客户端的启动辅助类
Bootstrap bootstrap = new Bootstrap();
bootstrap
.group(group)
// channel方法指定通道类型
.channel(NioSocketChannel.class)
// 通道初始化了
.handler(
new ChannelInitializer<Channel>() {
protected void initChannel(Channel channel) throws Exception {
channel.pipeline().addLast(new StringEncoder());
}
});
Channel channel = bootstrap.connect("127.0.0.1", 8000).channel();
while (true) {
channel.writeAndFlush(new Date() + ": hello world!");
Thread.sleep(2000);
}
}
}
3.6 基于Netty自定义RPC
3.6.1 分析
3.6.1.1 需求
- 模仿dubbo,消费者和提供者约定接口和协议
- 消费者远程调用提供者,提供者返回一个字符串,消费者打印提供者返回的数据
- 底层网络通信使用Netty
3.6.1.2 步骤
- 创建一个公共的接口项目以及创建接口及方法,用于消费者和提供者之间的约定
- 创建一个提供者,该类需要监听消费者的请求,并按照约定返回数据
- 创建一个消费者,该类需要透明的调用自己不存在的方法,内部需要使用Netty请求提供者返回数据
3.6.2 代码实现
3.6.2.1 公共模块rpc-common
-
pom.xml
文件中引入netty依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.6.Final</version>
</dependency>
- 定义接口
UserService.java
package com.lagou;
public interface UserService {
public String sayHello(String word);
}
3.6.2.2 服务提供者rpc-provider
- 接口实现类
UserServerImpl.java
package com.lagou.server;
import com.lagou.UserService;
import com.lagou.handler.UserServerHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
public class UserServerImpl implements UserService {
public String sayHello(String word) {
System.out.println("调用成功--参数:" + word);
return "调用成功--参数:" + word;
}
public static void startServer(String hostName, int port) {
try {
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap
.group(eventLoopGroup)
.channel(NioServerSocketChannel.class)
.childHandler(
new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new StringEncoder());
pipeline.addLast(new StringDecoder());
pipeline.addLast(new UserServerHandler());
}
});
serverBootstrap.bind(hostName, port).sync();
System.out.println("server start!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 自定义handler类
UserServerHandler.java
package com.lagou.handler;
import com.lagou.server.UserServerImpl;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
public class UserServerHandler extends ChannelInboundHandlerAdapter {
/**
* @param ctx
* @param msg
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("UserServerHandler的channelRead方法被调用");
// 如何符合约定,则调用本地方法,返回数据
if (msg.toString().startsWith("UserService")) {
// msg的构成:UserServer#sayHello#参数
String result =
new UserServerImpl().sayHello(msg.toString().substring(msg.toString().lastIndexOf("#")));
ctx.writeAndFlush(result);
}
}
}
- 启动类
ServerBoot.java
package com.lagou.boot;
import com.lagou.server.UserServerImpl;
public class ServerBoot {
public static void main(String[] args) {
UserServerImpl.startServer("127.0.0.1", 8999);
}
}
3.6.2.3 服务消费者rpc-consumer
- 创建代理相关的类
RPCConsumer.java
package com.lagou.client;
import com.lagou.handler.UserClientHandler;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
public class RPCConsumer {
// 线程池
public static ExecutorService executorService =
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
public static UserClientHandler client;
public Object creatProxy(Class<?> serverClass, String param) {
return Proxy.newProxyInstance(
Thread.currentThread().getContextClassLoader(),
new Class[] {serverClass},
new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (client == null) {
initClient();
}
// 设置参数
client.serParam(param + args[0]);
return executorService.submit(client).get();
}
});
}
private void initClient() {
try {
client = new UserClientHandler();
NioEventLoopGroup loopGroup = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap
.group(loopGroup)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(
new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new StringDecoder());
pipeline.addLast(new StringEncoder());
pipeline.addLast(client);
}
});
bootstrap.connect("127.0.0.1", 8999).sync();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 自定义handler类
UserClientHandler.java
package com.lagou.handler;
import java.util.concurrent.Callable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
public class UserClientHandler extends ChannelInboundHandlerAdapter implements Callable {
private ChannelHandlerContext context;
private String param;
private String result;
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
this.context = ctx;
}
/**
* 收到服务数据,唤醒等待的线程
*
* @param ctx
* @param msg
* @throws Exception
*/
@Override
public synchronized void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
this.result = msg.toString();
notify();
}
/**
* 写数据,然后等待唤醒
*
* @return
* @throws Exception
*/
@Override
public synchronized Object call() throws Exception {
context.writeAndFlush(param);
wait();
return result;
}
public void serParam(String param) {
this.param = param;
}
}
- 启动类
ConsumerBoot.java
package com.lagou.boot;
import com.lagou.UserService;
import com.lagou.client.RPCConsumer;
public class ConsumerBoot {
public static final String providerName = "UserService#sayHello#";
public static void main(String[] args) throws InterruptedException {
RPCConsumer rpcConsumer = new RPCConsumer();
UserService userService = (UserService) rpcConsumer.creatProxy(UserService.class, providerName);
while (true) {
Thread.sleep(2000);
String result = userService.sayHello("are you ok ?");
System.out.println(result);
}
}
}