package com.cn.middleware;
public class LargeWebsiteSystemAndJavaMiddlewareDocument {
/**
* 网络io实现方式:
* A、BIO 阻塞方式,一个socket套接字使用一个线程来处理。建立连接、读写数据都可能会阻
* 塞。好处:简单。但支持并发时,需要更多的线程来处理。
* B、NIO Nonblocking IO 基于事件驱动的思想,才有Reactor模式,一个线程处理多
* 个套接字。统一通过Reactor对客户端的socket套接字做处理,再派发到不同的线程。
* C、AIO Asynchronous IO 异步IO,采用Proactor模式,AIO与NIO的差别是,AIO在
* 进行读写时,只需要调用相应的reader/write方法,并且需要传入CompletionHandle
* (动作完成的处理器),在动作完成后,会调用CompletionHandle。不同系统上会有些差异,NIO的
* 通知时发生在动作之前,是可读写的时候,Selector发现这些事件后调用Handle处理。
*
* AIO java7 之后引入,AIO与NIO最大的区别,NIO是有通知时可以进行相关操作,AIO有通知时
* 表示相关操作已经完成。
*
* 单机到集群的转变:
* 1、DNS、负载均衡设备
* 2、session共享。解决方案:
* 1> session stick 负载均衡器根据每次请求的回话标识进行请求转发。
* 缺点:
* a.有一台服务宕机,则回话数据丢失,若有登录状态,用户需要重新登录
* b.会话标示是应用层信息,负载均衡器要对同一会话的请求进行解析并保存到同一服务器,就
* 需要对应用层解析,开销比较大。
* c.负载均衡器变成一个有状态的节点。要将会话保存到具体Web服务器的映射。和无状态节点
* 相比,内存消耗更大,容灾方面更麻烦。
* 2> session Replication replication 服务之间进行 session共享
* 缺点:
* a.同步session数据加大宽带开销。
* b.每台服务器都保存session,占用内存比严重。
* 3> session数据集中存储。session集中存储起来,可以是数据库,可以是分布式存储系统,web服务
* 统一来取。
* 缺点:
* a.读写session数据引入了网络操作,对于本机的数据读取来说,存在时延和不稳定性,通
* 信在内网,则问题不大。
* b.如果集中存储session的机器或集群有问题,则会产生影响。
* 比session Replication,web服务器数量比较多,session数比较多的时候,优势比较明显。
* 4> cookie based 通过cookie传递session。session数据放在cookie中,然后在Web服务器上
* 从cookie中生成对应的session数据。好比每次把自己的碗筷带在自己身上,去哪家饭店吃饭可以任意选择。
* 相对于集中存储方式,不依赖外部的一个存储系统,也就不存在从外部系统获取、写入session数据网络时延、不
* 稳定性。
* 不足: 1、cookie长度限制。
* 2、安全性。session数据本来是服务端数据,而这个方案是让服务端数据到了外部网络及客户端
* ,因此存在安全性问题。可以对cookie中的session加密,不过对于安全来说,物理上不能接触才是最
* 安全的。
* 3、带宽消耗,数据中心的整体外部带宽消耗,非web服务器内部间的带宽消耗。
* 4、性能影响。每次http请求响应都带有session数据,对web服务器来说,同样的处理情况下,
* 响应的结果输出越少,支持的并发请求就会越多。
*
* 总结:大型网站中,session stick 跟session数据集中存储比较好。
*
*
* 数据库压力:读写库分离
* 1、数据复制问题
* 2、应用对于数据源选择的问题
* 数据缓存,页面缓存,分布式存储系统
* 数据库拆分,垂直拆分、水平拆分。
* 专库专用,垂直拆分,不同的业务拆分到不同的库,带来了数据库连接池的隔离。拆库之后带来了跨业务的事务。
* 使用分布式事务,其性能明显低于之前的单机事务;去掉事务或不追求强事务支持,则原来单库中可以使用的表关联
* 的查询也需要改变实现。
* 垂直拆分之后,单机遇到瓶颈,水平拆分,同一表中的数据拆分到两个库中,数据水平拆分与读写分离的区别:读
* 写分离解决读压力大的问题,对于数据量大或更新量情况不起作用。水平拆分与垂直拆分区别:垂直拆分把不同的表
* 拆分到不同的库,水平拆分把同一张表拆分到不同的数据库。
*
* 水平拆分影响:
* SQL路由问题
* 主键的处理
* 查询分页问题
*
* 读写分离、分布式存储、数据垂直拆分和水平拆分都是在解决数据方面的问题,接下来看看应用方面的变化。
*
* 应用的拆分,根据功能进行拆分
* 走服务化:业务功能引入远程调用;共享代码集中在各个服务中心;数据库连接由服务中心交互,减少连接数,
* 前后端分离;服务化之后,各个系统之间更利于维护,稳定。
* 消息中间件:异步、解耦
*
* 中间件:
* 远程调用和对象访问中间件:主要解决分布式环境下应用互相访问的问题。这也是支持介绍服务化的基础。
* 消息中间件:解决应用之间的消息传递、解耦、异步问题。
* 数据访问中间件:主要解决应用访问数据库的共性问题的组件。
* JVM Hotspot
* Young/Tenured/Perm 新生代/年老代/持久代
* *******************************************************
* * Perm || Eden | Survivor | Survivor || Tenured *
* *******************************************************
* 一般来说,新的对象会被分配到新生代的Eden区,也有可能被之间分配到老年代。在新生代垃圾回收时,Eden
* 区中存活的对象会被复制到空的Survivor区,而下次新生代垃圾回收的时候,Eden区存活的对象和这个Survivor
* 区中存活的对象会被复制到另外一个Survivor区,并且清空当前的Survivor区。经过多次新生代垃圾回收,还存活
* 的对象会被移动到老年代。而老年代的空间也会根据一定的条件进行垃圾回收。
*
* 在Hotspot中 ,针对新生代提供了以下GC方式:
* 串行GC - Serial Copying
* 并行GC - ParNew
* 并行回收GC - Parallel Scavenge
* 针对老年代,有以下GC方式:
* 串行GC - Serial MSC
* 并行MS GC - Parallel MSC
* 并行Compacting GC - Parallel Compacting
* 并发GC - CMS
* 在 SUN 的java6 update14 中,引入了 Garbage First(G1回收器),G1的目标是取代CMS
* JRockit中内存堆布局情况:Nursery/Tenured
* *********************************************************
* * Keep Area| || Tenured *
* *********************************************************
* Nursery相当于 Hotspot中的Young,Keep Area区域可以使自己区域中的对象跳过下一次的Young GC
* 也就使得Keep Area区域的对象创建后,第二次 Young GC才会被GC。JRockit还有一种内存堆使用方式,
* 不对整个堆进行分代,直接作为一个连续的堆使用。
* IBM JVM 中内存堆的布局情况:Nursery/Tenured
* *********************************************************
* * Allocate | Survivor || Tenured *
* *********************************************************
* 分代名称跟JRockit一样, Allocate、Survivor 跟Hotspot 中的 Eden Survivor类似,只是IBM
* JVM中 Survivor只有一个,不过在新生代进行垃圾回收时,也是从Nursery一个区复制到另一个区。
*
* Java 并发编程的类、接口和方法
*
* 线程池:
* 主要使用线程池 ThreadPoolExecutor ,此外还有定时的 ScheduledThreadPoolExecutor。
* 需要注意的是对于 Executors.newCacheThreadPool()方法返回的线程池使用,该方法返回的线程池
* 是没有线程上限的,在使用时需要注意,以防过多线程占用内存。建议使用有固定线程上限的线程池。
*
* Synchronized
* ReentrantLock
* volatile
* atomic
* wait、notify、notifyAll。调用都在Synchronized代码块里面
* CountDownLatch 当多个线程达到预期状态或完成预期工作上触发事件
* CyclicBarrier
* Semaphore
* Exchanger
* Future FutureTask
*
* 并发容器 线程安全容器,比较有代表的是以CopyOnWrite和Concurrent开头的几个容器
* CopyOnWrite思路,在更改容器的时候,把容器写一份修改,保证正在读的线程不受影响,这种方式
* 用在读多写少的场景中会非常好,实质上是在写的时候重建了一次容器。
* Concurrent是尽量保证读不加锁,并且修改时不影响读,所以会达到比使用读写锁更高的并发性能。
*
* 动态代理
* 反射
* 网络通信实现选择
* jdk1.4中增加了NIO的支持 1.7中增加了AIO的支持。网络通信可以直接使用jdk提供的API,也可以
* 选择其他的框架来简化工作,例如MINA、Netty
*
* 分布式系统中的Java中间件,服务框架帮助我们对应用进行拆分,完成服务化;数据层则帮助我们完成数据的
* 拆分以及整个数据的管理、扩容、迁移等工作;消息中间件帮助我们完成应用的解耦,并向我们提供一种分布式下
* 完成事务的思路。
*
* 服务化框架
* 远程通信问题
* 采用透明代理与调用者、服务提供者直连的解决方案
*
*
* 4.2.3.5 服务端调用端流控处理:
* 处理完正常功能后,还有很多应对异常和可运维而需要做的事。流量控制(流控)保证系统的稳定性,这里说的流控
* 就是加载到调用者的控制功能,为了控制到服务提供者的请求流量。
* 控制方式:
* 1、0-1开关,完全打开不进行流控。
* 2、设置固定值,表示每秒可请求次数,超出则拒绝请求。被拒绝的请求可以直接返回,或进行排队。
* 基于以下两种维度进行控制:
* 1、根据服务端自身的接口、方法做控制,针对不同接口、方法设置阈值,使服务端不同接口、方法间负载互不影响。
* 2、根据来源做控制,对于同样的接口、方法,根据不同来源设置不同限制,一般在比较基础的服务上,多个集群
* 使用同样服务时,根据请求来源的不同级别等进行不同的流控处理。
*
* 序列化和反序列化处理
* 序列化:内存对象---》二进制数据
* 反序列化:二进制数据---》内存对象
* 服务框架要把本地进程内部的方法调用变成远程的方法调用,首先需要把调用所需的信息从调用端的内存对象变为二进制数据,
* 然后通过网络传到远程的服务提供端,再在服务提供端反序列化数据,得到调用的参数后进行相关的调用。
* 序列化方式,对Java而已,需要注意:
* 1、Java序列化或反序列化时自身的性能问题以及跨语言问题。
* 2、序列化和反序列化的性能开销,以及不同方式的性能对比。
* 3、序列化后的长度。
* 归结为易用性、跨语言、性能、序列化后的数据长度等方面综合考虑。
* 从两方面来看协议的部分,一、用于通信的数据报文的自定义协议;二、远程过程调用本身的协议。
* 可以使用http作为通信协议,xml、json或其他二进制表示作为序列化方式,XML定义的格式是服务层面的协议
*
* 网络通信层---》协议解析/反序列化---》定位服务---》调用服务
*
* 服务升级:
* 1、接口不变,代码本身完善。这种情况才用灰度发布的方式验证然后全部发布就可以了。
* 2、修改原有接口:
* a> 接口中增加方法。情况简单,需要使用新方法的直接调用新方法,跟原有方法不冲突。
* b> 对接口中的某些方法修改调用的参数列表,这种情况比较复杂。应对方式如下:
* 一、对使用原有方法的代码进行修改,然后和服务端一起发布。实际中不可行,尤其是同时发布多个系统
* 时,一些系统可能并不会从新修改参数列表的方法中收益。
* 二、通过版本号解决,使用老方法的系统继续调用原来版本的服务,使用新方法的系统则使用新版本服务。
* 三、在设计方法上考虑参数的扩展性。可行但不太好,可扩展一般意味着采用类似map的方式传参,不直观
* 对参数的校验会比较复杂。
*
* 实战中的优化:
* 集中式服务----》分布式服务转变需要注意的几点:
* 1、服务的拆分,要拆分的服务需要是为多方面提供公共功能的,专有的没有必要,反而增加系统复杂度。
* 2、服务的粒度,根据具体的业务划分服务。
* 3、优雅和实用的平衡,服务化的架构看起来比较优雅,但多一次调用就多走一次网络,一些功能直接
* 在服务调用者的机器上实现更合适、经济。
* 4、分布式环境中的请求合并,类似查询下载报表,需要进行复杂运算的可增加缓存,对相同查询条件可直接
* 使用计算好的数据,主要针对比较消耗系统资源的操作。
*
* 服务治理:
* 服务治理主要分为管理服务和查看服务两个方面。相当于数据的写和读,管理需要我们去控制、操作整个
* 分布式系统中的服务,而查看则是看运行时的状态或者一些具体的信息、历史数据等。
* 服务查看:
* 服务信息:服务最基本的信息。
* a> 服务编码,即数字化的服务编码
* b> 支持编码的注册
* c> 根据编码定位服务信息
* 服务质量:根据被调用服务的出错率、响应时间等数据对服务质量进行评估。
* a> 最好、最差的服务排行
* b> 各个服务的质量趋势
* c> 各种查询条件的支持
* 服务容量:根据服务所提供的总能力以及当前已使用的容量进行评估,其中能力是指对于请求数量
* 方面的支撑情况。
* a> 服务容量和当前水位的展示
* b> 历史趋势图
* c> 根据水位的高低排序
* d> 各种查询条件的支持
* 服务依赖:根据服务被调用以及调用其他服务的情况,给出服务与上下游服务的依赖关系,里面除了
* 服务间定性的依赖关系外,还有定量的数据信息。
* a>依赖服务展示
* b>被依赖服务展示
* c>依赖变化
* 服务分布:提供同样服务的机器的具体分布情况,主要看跨机房的分布情况。
* a> 不同机房分布
* b> 不同机柜分布
* c> 分布不均衡服务列表
* 服务统计:服务运行时信息统计
* a> 调用次数统计和排名
* b> 出错次数统计和排名
* c> 出错率统计和排名
* d> 响应时间统计和排名
* e> 响应时间趋势
* f> 出错率趋势
* 服务元数据:服务基本信息查看
* a> 服务的方法和参数
* 服务查询:提供各种条件来检索服务进而查看服务的各种信息的功能。
* a> 服务的应用负责人、测试负责人
* b> 服务所属的应用名称
* c> 服务发布时间
* d> 服务提供者的地址列表
* e> 服务容量
* f> 服务质量
* g> 服务调用次数
* h> 服务依赖
* i> 服务版本及归组信息等
* 服务报表:主要提供非实时服务的各种统计信息的报表,包括不同时间段的对比以及分时统计的
* 信息。
* 服务监视:提供对于服务运行时关键数据的采集、规则处理和警告。注意这里是服务监视非监控,
* 主要是完成对于服务运行数据的收集和处理,但不提供控制,通过监视发现问题后再在相应的服务管理
* 中进行管理工作。服务监视只提供用于决策的数据基础,并根据已定义的规则进行警告。
*
* 接着我们从服务管理的角度看下有哪些事情要做。
* 服务上下线:前面看到的服务是通过 ProviderBean 自动注册的,在治理中我们还需要控制
* 服务的上下线。
* a> 针对一个服务所有机器的上下线。
* b> 针对指定机器的上下线
* c> DoubleCheck 控制
* 服务路由:是对服务路由策略的管理,就是之前看到的基于接口、方法、参数的路由集中管理。
* a> 路由管理界面支持
* b> 路由信息更改前后对比和验证
* c> 路由配置多版本管理和回滚
* d> DoubleCheck 控制
* 服务限流降级:对服务对外流控的统一管理,除此之外,还集中管理服务上的很多开关。
* 例如,在服务调用或执行的地方,除了可以限流还可以停止一些非重要功能的处理,以便主流程可以继续执行。
* a> 根据调用来限流
* b> 根据具体服务来限流
* c> 针对服务开关降级
* d> 流控、降级配置多版本管理和回滚
* e> DoubleCheck 控制
* 服务归组:是在集中控制台调整服务的分组信息,对应我们在服务提供者的配置属性中看到的group属性,
* 可以在集中的控制台对服务的分组进行直接管理
* a> 归组规则的多版本管理和回滚
* b> 归组规则预览
* c> 归组规则的影响范围和评估
* d> DoubleCheck 控制
* 服务线程池管理:是对于服务提供者的服务执行的工作线程池的管理。
* a> 调用方的线程管理,主要是最大并发的管理
* b> 服务端线程工作状况查询
* c> 服务端针对不同服务的多个业务线程池的管理
* d> DoubleCheck 控制
* 机房规则:针对多机房、虚拟机房规则的管理
* a> 规则查询和发布校验
* b> 规则多版本管理和回滚
* c> DoubleCheck 控制
* 服务授权:随着服务和服务调用者的增多,一些重要服务的使用是需要有授权和鉴权的支持的,服务授权就是
* 针对服务调用者的授权管理。
* a> 授权信息查询
* b> 授权规则多版本支持和回滚
* c> DoubleCheck 控制
*
* 服务框架与ESB的对比
* 企业服务总线(ESB)也是系统在服务化时的一个重要支撑产品。是从面向服务体系架构中(SOA)发展过来
* 的,它是对多样系统中服务调用者和服务提供者的解耦。
* ESB本身也可以解决服务化的问题,它提供了服务暴露、接入、协议转换、数据格式转换、路由等方面的
* 支持。它与服务框架的主要差异有两个:
* 一、服务框架是一个点对点的模型,而ESB是一个总线式的模型
* 二、服务框架基本是面向同构的系统,不会重点考虑整合的需求,而ESB会更多考虑不同厂商所提供服务
* 的整合。
*
*
* 五、数据访问层
*
* 数据拆分:垂直、水平
* 垂直拆分影响:
* a> 单机的ACID保证被打破,多机后,原来的单机通过事务处理的逻辑受很大影响,面临的选择,放弃
* 原来的单机事务,修改实现,或引入分布式事务。
* b> 一些Join操作变的困难,不能很方便的利用数据库自身的Join,需要应用或其他方式解决。
* c> 靠外键约束的场景受到影响。
* 水平拆分影响:
* a> 同样有可能ACID被打破的情况
* b> 同样有可能有Join操作被影响的情况
* c> 靠外键约束的场景会有影响
* d> 依赖单库的自增序列生成唯一id会受影响
* e> 针对单个逻辑意义上的表查询要跨库了
* 以上只是列出的部分影响,还有其他的,如存储器、触发器也需要改写才能完成相应的工作了。
*
* 针对出现的问题,解决及应对方案:
* 单机---》多机,事务的处理
* 分布式事务相关内容:
* 分布式事务模型与规范:X/Open提出的分布式事务规范--XA规范,以及其定义的分布式事务处理模型--
* X/Open DTP模型。在该模型中定义了三个组件,Application Program、Resource Manager和
* Transaction Manager。
* 1、Application Program(AP),即应用程序,可以理解为使用DTP模型的程序。它定义了事务边界,并
* 定义了事务边界,并定义了该事务的应用程序的特定操作。
* 2、Resource Manager(RM),资源管理器,可以理解为一个DBMS系统,或者消息服务器管理系统。应
* 用程序通过资源管理器对资源进行控制,资源必须实现XA定义的接口,资源管理器提供了存储共享资源的支持。
* 3、Transaction Manager(TM),事务管理器,负责协调和管理事务,提供给AP应用程序编程接口并
* 管理资源管理器,事务管理器向事务指定标识,监视它们的进程,并负责处理事务的完成和失败,事务分支标识
* (XID)由TM指定,以标识一个RM内的全局事务和特定分支。它是TM中日志与RM中日志之间的相互标记。两
* 阶段提交或回滚需要XID,以便在系统启动时执行再同步操作(也称再同步(resync)),或在需要时允许管理员
* 执行试探操作(也称人工干预)。
*
* 以上三个组件,AP可以和TM、RM通信,TM和RM之间可以互相通信,DTP模型中定义了XA接口,TM和RM通过XA接口进行
* 双向的通信。
* TM协调分布式服务中的一致性,管理全局事务,管理事务的生命周期,并协调资源。
* DTP中定义的其他概念:
* 事务:一个事务是一个完整的工作单元,由多个独立的计算机任务组成,这多个任务在逻辑上是原子的。
* 全局事务:一次性操作多个资源管理器的事务就是全局事务。
* 分支事务:在全局事务中,每个资源管理器有自己独立的任务,这些任务的集合是这个资源管理器的分支任务。
* 控制线程:用来表示一个工作线程,主要是关联AP、TM和RM三者的线程,也就是事务上下文环境。简单的说,就是用
* 来标识全局事务和分布式事务关系的线程。
*
* DTP模型:
* i、AP和RM之间,可以使用RM自身提供的native API进行交互,这种方式就是使用RM的传统方式,并且这个交互
* 不在TM的管理范围内。另外,当AP和RM之间需要进行分布式事务的时候,AP需要得到对RM的连接(此连接由TM管理),然后
* 使用XA的native API来进行交互。
* ii、AP与TM之间,该例子中使用的是TX接口,也是由X/Open所规范的。它用于对事务进行控制,包括启动事务、
* 提交事务和回滚事务。
* iii、TM与RM之间是通过XA接口进行交互的。TM管理了到RM的连接,并实现了两阶段提交。
*
* 两阶段提交
* 2PC,两阶段提交,是相对于单库的事务提交方式来说的,我们在单库上完成相关数据操作后,就会直接提交或回滚,而
* 在分布式系统中,在提交之前,增加了准备的阶段,所以称为两阶段提交。
*
* 大型网站一致性的基础理论--CAP/BASE
* CAP理论:
* Consistency:all nodes see the same data at the same time,即所有的节点在同一时间
* 读到同样的数据,这就是数据上的一致性(用C表示),也就是当数据写入成功后,所有的节点会同时看到这个新的数据。
* Availability:a guarantee that every request receives a response about whether
* it was successful or failed,保证无论是是成功还是失败,每个请求都能够收到一个反馈。这就是数据的可用
* 性(用A表示),这里的重点是系统一定要有响应。
* Partition-Tolerance:the system continues to operate despite arbitrary message
* loss or failure of part of the system,即使系统中有部分问题或者有消息的丢失,但系统仍能继续运行,
* 这被称为分区容忍性(用P表示),也就是在系统一部分出现问题时,系统仍能继续工作。
*
* 分布式系统中并不可能同时满足上面三项,我们可以选择其中两个来提升,而另一个则会收到损失。其实就是选择CA、AP或
* CP的问题。
* 选择CA,放弃分区容忍性,加强一致性和可用性。这其实就是传统的单机数据库的选择。
* 选择AP,放弃一致性,追求分区容忍性及可用性。这是很多分布式系统在设计时的选择,例如很多NoSQL系统就是如此。
* 选择CP,放弃可用性,追求一致性和分区容忍性。这种选择下的可用性会比较低,网络的问题会直接让整个系统不可用。
* 从上面的分析可以看出,在分布式系统中,我们一般还是选择加强可用性和分区容忍性而牺牲一致性。当然,这里所讲的并
* 不是不关心一致性,而是首先满足A和P,然后看如何解决C的问题。
* 我们再来看看BASE模型,BASE涵义如下:
* Basically Available:基本可用,允许分区失败。
* Soft state: 软状态,接受一段时间的状态不同步。
* Eventually consistent:最终一致,保证最终数据的状态是一致的。
* 当我们在分布式系统中选择了CAP中的A和P后,对于C,我们采用的方式和策略就是保证最终一致,也就是不保证数据变化
* 后所有节点立刻一致,但是保证它们最终是一致的。在大型网站中,为了更好地保持扩展性和可用性,一般都不会选择强一致性,
* 而是采用最终一致的策略来实现。
*
* 比两阶段提交更轻量一些的Paxos协议
* 在分布式系统中,节点之间的信息交换有两种方式,一种是通过共享内存共用一份数据;另一种是通过消息投递来完成消息
* 的传递。而在分布式系统中,通过消息投递的方式会遇到很多意外的情况,例如网络问题、进程挂掉、机器挂掉、进程很慢没有
* 响应、进程重启等情况,这就会造成消息重复、一段时间内部不可达等现象。Paxos协议是解决分布式系统中一致性问题的一个
* 方案。
* 使用Paxos协议有一个前提,那就是不存在拜占庭将军的问题。前提是有一个可信的通信环境,也就是说信息都是准确的,没
* 有被篡改。
* Paxos算法的提出过程是,虚拟了一个叫做Paxos的希腊城邦,并通过议会以决议的方式介绍Paxos算法。
* 首先把议员的角色分为了Proposers、Acceptors和Learners,议员可以身兼数职,介绍如下:
* Proposers,提出议案者,就是提出议案的角色。
* Acceptors,收到议案后进行判断的角色。Acceptors收到议案后要选择是否接受(Accept)议案,若议案获
* 得多数Acceptors的接受,则该议案被批准(Chosen)。
* Learners,只能“学习”被批准的议案,相当于对通过的议案进行观察的角色。
* 在Paxos协议中,有两个名词介绍如下:
* Proposal,议案,由Proposers提出,被Acceptors批准或否决。
* Value,决议,议案的内容,每个议案都是由一个{编号,决议}对组成。
* 在角色划分后,可以更精确的定义问题,如下所述:
* 决议(Value)只有被Proposers提出后才能被批准(未经批准的决议成为“议案(Proposal)”)。
* 在Paxos算法的执行实例中,一次只能批准(Chosen)一个Value。
* Learners只能获得被批准(Chosen)的Value。
* 对议员来说,每个议员有一个结实耐用的本子和擦不掉的墨水来记录议案,议员会把表决信息记在本子的背面,本子
* 上的议案永远不会改变,但是背面的信息可能会被划掉。每个议员必须(也只需要)在本子背面记录如下信息:
* LastTried[p],由议员p试图发起的最后一个议案的编号,如果议员p没有发起过议案,则记录未负无穷大。
* PreviousVote[p],由议员p投票的所有表决中,编号最大的表决对应的投票,如果没有投过票则记录为
* 负无穷大。
* NextBallot[p],由议员p发出的所有LastVote(b,v)消息中,表决编号b的最大值。
* 基本协议的完整过程如下:
* 1、议员p选择一个比LastTried[p]大的表决编号b,设置LastTried[p]的值为b,然后将NextBallot(b)
* 消息发送给某些议员。
* 2、从p收到一个b大于NextBallot[q]的NextBallot(b)消息后,议员q将NextBallot[q]设置为b,然后
* 发送一个LastVote(b,v)消息给p,其中v等于PreviousVote[q](b<=NextBallot[q]的NextBallot(b)
* 消息将被忽略)。
* 3、在某个多数集合Q中的每个成员都收到一个LastVote(b,v)消息后,议员p发起一个编号为b、法定人数集为Q、议案
* 为d的新表决。然后它会给Q中的每一个牧师发送一个BeginBallot(b,d)消息。
* 4、在收到一个b=NextBallot[q]的BeginBallot(b,d)消息后,议员q在编号为b的表决中投出他的一票,在设置
* PreviousVote[p]为这一票,然后向p发送Voted(b,q)消息。
* 5、p收到Q中每一个q的Voted(b,q)消息后(这里Q是表决b的法定人数集合,b=LastTried[p]),将d(这轮表决的法令)
* 记录到他的本子上,然后发送一条Success(d)消息给每个q。
* 6、一个议员在接收到Success(d)消息后,将决议d写到他的本子上。
* 综上可总结出Paxos的核心原则是少数服从多数。
* 如果系统中同时有人提议案的话,可能会出现碰撞失败,然后双方都需要增加议案的编号再提交的过程。而再次提交可能仍然存在
* 编号冲突,因此双方需要再增加编号去提交。这就会产生活锁。
* 解决的办法是在整个集群当中设一个Leader,所有的议案都由他来提,这样就可以避免这种冲突了。这其实是把提议案的工作
* 变为一个单点,而引发的新问题是如果这个Leader出问题了该如何处理,那就需要再选一个Leader出来。
*
* 集群内数据一致性的算法实例:
* 关于集群内数据的一致性,我们通过Quorum和 Vector Clock算法来具体讲解一下。
* Quorum,它是用来权衡分布式系统中数据一致性和可用的,我们引入三个变量,如下:
* N:数据复制节点数量。
* R:成功读操作的最小节点数。
* W:成功写操作的最小节点数。
* 如果W+R>N,是可以保证强一致性的,而如果W+R<=N,是能够保证最终一致性的。
* 根据前面的CAP理论,我们需要在一致性、可用性和分区容忍性方面进行权衡。
* Vector Clock的思路是对同一份数据的每一次修改都加上“<修改者,版本号>”这样一个信息,
* 用于记录修改者的信息及版本号,通过这样的信息来帮助我们解决一些冲突。
* 假设有如下场景:
* Alice、Ben、Catby和Dave四人约定下周要一起聚餐,四个人通过邮件商量聚餐的时间。
* Alice首先建议周三聚餐。
* 之后Dave和Catby商量觉得周四更合适。
* 后来Dave又和Ben商量之后觉得周二也行。
* 最后Alice要汇总大家的意见,得到的反馈如下:
* Catby说,他和Dave商量的时间周四。
* Ben说,他和Dave商量的时间是周三。
* 此时恰好联系不上Dave,而且不知道Catby和Ben分别与Dave确定时间的先后顺序。
* Alice就不能明确到底该定在哪一天了。
* 类似的事情经常会发生。当你向两个或几个人问一些消息时,返回的内容往往不一样,而且你
* 不知道哪个是最新的。Vector Clock就是为了解决这种问题来设计的,简单来说,就是未每一个
* 商议的结果附上一个时间戳,当结果改变时,更新时间戳。加上时间戳后,我们再一次描述上面的
* 场景,如下。
* 当Alice第一次提议将时间定为周三时,可以这样描述这个信息:
* data = Wednesday
* vclock = Alice:1
* vclock就是这条消息的Vector Clock,Alice:1标识这是从Alice发出的第一个版本。
* 接着,Dave和Ben商量将时间改为周二,Ben发给Dave的消息如下:
* data = Tuesday
* vclock = Alice:1,Ben:1
* 注意Ben这条消息保留了Alice的记录,同时加上了自己的记录。Ben:1代表这是Ben第一次修改
* 的记录。接着Dave收到Ben的消息,并同意将时间改为周二,他回给Ben的消息如下:
* data = Tuesday
* vclock = Alice:1,Ben:1,Dave:1
* 这条消息同样保留了原来已有的vclock记录,同时加上了自己的记录。
* 另一方面,Catby收到Alice的消息,打算与Dave商量将时间改为周四,于是他发送如下消息给
* Dave:
* data= Thursday
* vclock = Alice:1,Catby:1
* 看到这里你可能会奇怪,为什么vclock中没有之前的Ben和Dave的记录了?
* 这是因为Ben和Dave商量的时候Catby并不知道这个情况。Catby手中的信息还是Alice最初
* 发出的那份。这样当Dave收到来自Catby的消息时就发现有冲突了。Dave手中的两份信息如下:
* data = Tuesday
* vclock =Alice:1,Ben:1,Dave:1
* data = Thursday
* vclock = Alice:1,Catby:1
* Dave通过对比两份消息的vclock可以发现冲突,这是因为上面两个版本的vclock都不是对方的
* “祖先”。其中Vector clock对祖先的定义是这样的:对于vclock A和 vclock B,当且
* 仅当A中的每一个标记ID都存在于B中,同时A中对应的标记版号要小于等于B时,vclock才是vclockB
* 的祖先,如果标记ID不存在,可以认为,标记版本号为0。
* Dave通过对比vclock发现了版本冲突,于是尝试解决冲突。两个版本中只能选择一个,他选择了
* 时间为周四的,那么这条消息可以表示为:
* data = Thursday
* vclock =Alice:1,Ben:1,Catby:1,Dave:2
* Dave 在vclock中加上了两个消息中的全部标记ID(Alice、Ben、Catby和Dave),同时将自己
* 对应的版本号加1,然后将这条消息发送给Catby。
* 最后,当A从Catby和Ben收集反馈信息时,收到通知如下消息。
* 来自Ben的:
* data = Thursday
* vclock = Alice:1,Ben:1,Dave:1
* 来自Caitby
* data = Thursday
* vclock = Alice:1,Ben:1,Dave:1
* 来自Catby:
* data:=Thursday
* vclock = Alice:1,Catby:1,Ben:1,Dave:2
*
* 这时Alice从Catby的消息就可看,Dave后来改变主意了。
*
* 到这里,我们来介绍一些分布式环境下的事务相关的算法和实践。从工程上来说,
* 如果能够避免分布式事务的引入,那么还是避免为好;如果一定要做引入分布式事务,那么可以
* 考虑最终一致的方法,而不要追求强一致。而且从实现上来说,我们是通过补偿的机制
* 不断重试,让之前因为异常而没有进行到底的操作继续进行,而不是回滚。如果还不能满足需求,
* 那么基于Paxos算法的实现会是一个不错的选择。
*
* 多机的Sequence问题与处理
* 当转变为水平分库时,原来单库中的Sequence及自增Id的做法需要改变。
* Oracle中的Sequence,Mysql中的Auto Increment自增。分库分表后的解决方向:
* 唯一性
* 连续性
* 如果只考虑Id的唯一性,可以参考UUID的生成方式,或者根据自己的业务情况使用各个
* 种子(不同维度的标识,例如IP、MAC、机器名、时间、本机计数器等因素)来生成唯一的Id。
* 这样生成的Id保证了唯一性,但在整个分布式系统中的连续性不好。
* 从连续性考虑。这里的连续是指在整个分布式环境中生成的Id的连续性。在单机环境中,
* 其实就是一个单点来完成这个任务,在分布式系统中,我们可以用一个独立的系统来完成这个工作。
* 实现方案:把所有的Id集中放在一个地方进行管理,对每个Id序列独立管理,每台机器使用Id
* 时都从这个Id生成器上取。这里有以下几个关键问题需要解决。
* 1、性能问题。每次都远程获取Id会有资源损耗。一种改进方案是一次取一段Id,然后缓存在
* 本地,这样就不需要每次都去远程的生成器上取了。这样带来新的问题是:如果应用取了一段id
* 正在用时完全宕机了,那么一些Id号就浪费不可用了。
* 2、生成器的稳定性问题。Id生成器作为一个无状态的集群存在,其可用性要靠整个集群来保证。
* 3、存储的问题。底层存储的选择空间较大,需要根据不同类型进行对应的容灾方案。下面介绍
* 两种方式。
* A、底层使用一个独立的存储来记录每个Id序列当前的最大值,并控制并发更新。
* B、直接把Id生成器舍掉,把相关的逻辑放到需要生成Id的应用本身就行了。去掉应用
* 和存储之间的独立部署的生成器,而在每个应用上完成生成器要做的工作,即读取可用的Id或
* Id段,然后给应用的请求使用。
* 不过这种方式没有中心的控制节点,我们并不希望生成器之间还有通信(这会使系统非常
* 复杂),因此数据的Id并不是严格按照进入数据库的顺序而增大的,在管理上也需要有额外的
* 功能,这些是需要权衡之处。
*
* 应对多机的数据查询
* 跨库Join
* 在分库后,如果需要Join的数据还在一个库里面,那就可以直接进行Join操作。例如,我们
* 根据用户的Id进行用户相关信息表的分库,那么如果查询某个用户在不同表中的一些关联信息,还
* 是可以进行Join操作的。如果需要Join的数据已经分布在多个库中了,那就需要完成跨库的Join
* 操作,这会比较麻烦,解决的思路有如下几种。
* i、在应用层把原来数据库的Join操作分成多次的数据库操作。举个例子,我有用户基本信息
* 的数据表,也有用户出售的商品信息表,需求是查出等级手机号为138XXXXXXXX的用户在售的
* 商品总数。这再单库时用一个sql的Join就解决了,而如果商品信息与用户信息分开了,我们就
* 需要先在应用层根据手机号找到用户Id,然后再根据用户id找到相关的商品总数。
* ii、数据冗余,也就是对一些常用信息进行冗余,这样可以把原来需要Join的操作变为单表
* 查询。这需要结合具体业务场景。
* iii、借助外部系统(例如搜索引擎)解决一些跨库的问题。
*
* 外键约束
* 外键约束的问题比较难解决、不能完全依赖数据库本身来完成之前的功能了。如果要对分库后的单库
* 做外键约束,就要求分库后每个单库的数据都是内聚的,否则就只能靠应用层的判断、容错等方式了。
*
* 跨库查询的问题及解决
* 数据库分库分表的演化
* 来看下合并查询的问题。合并查询问题产生的根源在于我们进行水平分库分表时,把一张逻辑上
* 的表分成了多张物理上的表,假如,我们有一个用户信息表,根据用户id进行分库分表后,物理上就
* 会分成很多用户信息表。
* 根据业务分库分表后,简单的操作不多数,下面看下较为复杂的操作。
* 1、排序,即多个来源的数据查询出来后,在应用层进行排序的工作。如果从数据库中查询的
* 数据已经排好序的,那么在应用层要进行的就是对多路的归并排序;如果查询出的数据未排序,就要
* 进行一个全排序。
* 2、函数处理,即使用Max、Min、Sum、Count等函数对多个数据来源的值进行相应的函数
* 处理。
* 3、求平均值,从多个数据来源进行查询时,需要把SQL改为查询Sum和count,然后对多个
* 数据源的Sum求和、Count求和后,计算平均值,这是需要注意的地方。
* 4、非排序分页,这需要看具体实现所采用的策略,是同等步长地在多个数据源上分页处理,
* 还是同等比例地分页处理。同等步长的意思是,分页的每页中,来自不同数据源的记录数是一样的;
* 同等比例的意思是,分页的每页中,来自不同数据源的数据数占这个数据源符合条件的数据总数的
* 比例是一样的。举例说明如下。
* 5、排序后分页,这是把排序和分页放在一起的情况,也是最复杂的情况,最后需要呈现的
* 结果书数据按照某些条件排序并进行分页显示。我们的数据是来自不同数据源的,因此必须把足够
* 的数据返回给应用,才能得到正确的结果,复杂之处就在于将足够的数据返给应用。
* 第N页的数据获取方式,L表示每页条数,从不同数据源获取N*L条记录,然后再做归并排序。
* 所以越往后翻页,承受的负担越重。
* 在访问很大的系统时,尽量避免这种方式,尤其是排序后需要翻很多页的情况。
*
* 数据访问层的设计与实现
*
* 提供数据访问层的方式:采用数据层专有API方式、采用JDBC方式以及基于ORM/类ORM接
* 口方式(ibatis、hibernate、Spring jdbc)。
* 采用JDBC的方式是兼容性和扩展性最好的,实现成本也是相对比较高的。
* 底层封装了某个ORM框架或类ORM框架的方式具有一定的通用性,实现成本相对JDBC的方式要低。
* 专有API是特殊场景下的选择。
* 具体场景上实现的差别:专有API和对外提供JDBC接口的方式都直接使用了下层数据库提供的JDBC,
* 因此更加灵活,而基于ORM框架或类ORM框架的方式则在数据层和JDBC驱动之间隔了一个第三方的ORM/类
* ORM框架,这在有些场景下会造成一些影响。
*
* 不同提供方式之间在合并查询场景下的对比
* 分库分表排序的场景,直接基于JDBC的优势比较明显。
*
* 数据层流程的顺序看数据层设计
* SQL解析--->规则处理--->SQL改写--->数据源选择--->SQL执行--->结果集返回合并处理
* i、SQL解析阶段。SQL解析主要考虑的问题有两个:
* 1、对SQL支持的程度,是否需要支持所有的SQL,这需要根据具体场景来做决定
* 2、支持多少SQL的方言,对于不同厂商超出标准SQL的部分要支持多少。需要根据实际选择。
* 具体解析时是使用antlr、javacc还是其他工具,看自己的选择,也可自己手写。
* 在进行SQL解析时,对于解析的缓存可以提升解析速度。当然需要注意缓存的容量限制,一般系统中执
* 行的SQL数量相对可控,不过为了安全,解析的缓存需要加上数量上限。
* 通过SQL解析可以得到SQL中的关键信息,例如表名、字段、where条件等。而在数据层中,一个很重
* 要的事情是根据执行的SQL得到被操作的表,根据参数及规则来确定目标数据源连接。
* 这一部分也可以通过提示(hint)的方式实现,该方式会把一些要素之间传进来,而不用去解析整个SQL
* 语句。使用这种方式的一般情况是:
* SQL解析并不完备。
* SQL中不带有分库条件,但实际上是可以明确指定分库的。
* 通过SQL解析或者提示方式得到了相关信息后,下一步就是进行规则处理,从而确定要执行这个SQL的目标库。
*
* ii、规则处理阶段
* 采用固定哈希算法作为规则。固定哈希的方式为,根据某个字段取模,然后将数据分割到不同的数据库和
* 表中,除了根据id取模,还经常会根据时间维度,例如天、星期、月年等来存储数据,这一般用于数据产生后
* 相关日期不进行修改的情况,否则就要涉及数据移动的问题了。根据时间取模多用在日志或者其他与时间维度密
* 切相关的场景。通常将周期性的数据放在一起,这样进行数据备份、迁移或现有数据的清空都会很方便。
*
* 一致性哈希,一致性哈希带来的最大变化是把节点对应的哈希值变成了一个范围,而不再是零散的。在一致性
* 哈希中,我们会把整个哈希值的范围定义的非常大,然后把这个范围分配给现有的节点,如果有节点加入,那么这
* 个新节点会从原有的某个节点上分管一部分范围的哈希值;如果有节点退出,那么这个节点原来管理的哈希值会
* 给它的下一个节点来管理。假设哈希值范围是从0到100,总共四个节点,那么它们管理的范围分别是[0,25)、
* [25,50)、[50,75)、[75,100]。如果第二个节点退出,那么剩下节点管理的范围就变成为[0,25)[25,75)
* [75,100],可以看到,第一个和第四个节点管理的数据没影响,而第三个节点原来所管理的数据也没有影响,
* 只需把第二个节点负责的数据接管过来就行了。如果是增加一个节点,例如在第二个和第三个节点有部分变化
* 数据也没受影响,另一部分要给新增的节点来管理。
* 存在的问题:新增一个节点时,除了新增的节点外,只有一个节点受影响,这个新增节点和受影响的节点的负载
* 是明显比其他节点低的;减少一个节点时,除了减去的节点外,只有一个节点受影响,它要承担自己原来的和减去的
* 节点的工作,压力明显比其他节点要高。这似乎要增加一倍节点或减去一半节点才能保持各个节点的负载均衡。如果
* 真是这样,一致性哈希的优势就不明显了。
*
* 虚拟节点对一致性哈希的改进
* 为了应对上述问题,我们引入虚拟节点的概念。即4个物理节点可以变为很多个虚拟节点,每个虚拟节点支持
* 连续的哈希环上的一段。而这时如果加入一个物理节点,就会相应加入很多虚拟节点,这些新的虚拟节点是相对均匀
* 地插入到整个哈希环上的,这样,就可以很好地分担现有物理节点的压力了;如果减少一个物理节点,对应的很多虚
* 拟节点就会失效,这样,就会有很多剩余的虚拟节点来承担之前虚拟节点的工作,但是对于物理节点来说,增加的负
* 载相对是均衡的,所以可以通过一个物理节点对应非常多的虚拟节点,并且同一个物理节点的虚拟节点尽量均匀分布
* 的方式来解决增加或减少节点时负载不均衡的问题。
*
* 映射表与规则自定义计算方式
* 映射表是根据分库分表字段的值表法来确定数据源的方法,一般用于对热点数据的特殊处理,或者在一些场景下
* 对不完全符合规律的规则进行补充。常见的情况是以前面的方式为基础,映射配合表来用。
* 最后要介绍的规则自定义计算方式是最灵活的方式,它已经不算是以配置的方式来做规则了,而是通过比较复杂
* 的函数计算来解决数据访问的规则问题,可以说是扩展能力最强的一种方式。我们可以通过自定义的函数
* 实现来计算最终的分库。
*
* 为什么要改SQL
* 单库表--->多库表:
* 分布在不同数据库中的表结构一样,但是表名未必一样。把原来的表分布在多苦且每个库都只有一个表的话,
* 那么这些表是可以同名的,但是如果单库中不止一个表,那就不能用同样的名字,一般是在逻辑名后面增加后缀,
* 例如原来的表名为User,那么分库分表后的表就可以命名为User_1、User_2等。
* 在命名表时,不同库的表名都是唯一的,可以避免很多误操作,在进行路由和数据迁移时也比较便利。
* 除了修改表名,SQL的一些提示中用到的索引名等,在分库分表时也需要进行相应的修改,需要从逻辑上的名字
* 变为对应数据库中物理的名字。
* 另外,一个需要修改SQL的地方,就是进行跨库计算平均值的时候,不能从多个数据源取平均值,再计算这些平均值
* 的平均值,而必须修改SQL获取数量、总数后再进行计算。
*
* 如何选择数据源
* 选择数据源的问题。如下图示例,在User经历了分库分表后,我们会给分库后的库都准备备库,也就是原来的一个数据库
* 变为一个数据库的矩阵了。分库是把数据分到了不同的数据分组中。我们决定了数据分组后,还需要决定访问分组中的哪个库。
* 这些库一般是一写多读的(也有多写多读的),根据当前要执行的SQL特点(读、写)、是否在事务中以及各个库的权重规则,
* 计算得到这次SQL请求要访问的数据库。
*
* 执行SQL和结果处理阶段
* 在SQL执行的部分,比较重要的是对异常的处理和判断,需要能够从异常中明确判断出数据库不可用的情况。而
* 关于执行结果的处理,在之前一些特殊情况中都已经提及,这里不再重复了。
*
*
* 实战经验
* 1、复杂的连接管理
* 下面的代码是使用JDBC进行SQL操作的一个简单示例代码,其中没有考虑处理异常的情况。我们在前面看到
* 的从SQL解析开始的执行,其实只相当于这段代码中executeQuery的部分,而在执行前生成的Connection
* 对象、PreparedStatement对象都是数据层自己的实现。这些实现都需要遵守JDBC的规范,具体的实现:
* String sql = "select name from user where id = ?";
* Connection conn = this.getConnection();
* PreparedStatement ps = conn.prepareStatement(sql);
* ps.setInt(1,11);
* ResultSet rs = ps.executeQuery();
* ps.close();
* conn.close();
* 在上面的代码中,我们是直接执行一个PreparedStatement的方法,得到结果后就结束了。而在另一些事务
* 场景下会执行多个PreparedStatement方法,这要求在PreparedStatement具体执行SQL时,需要从
* Connection对象中获取同样的连接,并且如果连接有问题要报错。也就是说需要对异常的情况有全面的考虑,而
* 这些也是我们选择对外暴露JDBC接口的一个代价。
*
* 2、三层数据源的支持和选择
* 对于Java应用引入数据源的情况,我们一般会采用Spring做如下的配置:
*
*
*
*
*
*
*
* 上面是使用Apache BasicDataSource的一个具体例子,数据层是从DataSource接口开始和应用连接
* 的。除了前面的Connection、PreparedStatement、Connection对象外,还需要实现一个DataSource
* 对象。
* DataSource管理了分库以后的整体的数据库,或者说管理了数据库集群。在这个DataSource的实现中,
* 完成了前面介绍的数据层的全部工作。
* 在使用上,DataSource可以通过Spring的方式配置到应用中,替换掉前面代码中的BasicDataSource。
* 从前面的例子看到,配置DataSource需要设置数据库的驱动(决定了数据库的类型),例如,是在Mysql还是
* Oracle或者其他数据库),以及数据库的地址、端号等连接有用信息,此外还要设置用户名和密码。
* 如果使用这个数据层的DataSource,可能就需要如下配置:
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
* 上面的配置信息是DataSource所需的信息。可以看到,配置还是比较多的,而且这里仅配置了两个库,在真实场景
* 中,分库后的数据库节点数量一般都远超两个,配置量会非常大。从工程角度来看,我们可以把上面的driverClassName、
* username和password抽取出来,做成一个公共配置,当然这会要求同一个业务的分库选择同样的数据库(一般都符合,
* 不过在系统迁移或者更换不同类型DB的过渡期,可能会有所不同),设置同样的用户名和密码。
* 即便简化了配置,这样的方式还是有不足的地方,即这个配置在所有用到这个数据库集群的地方都有一份,如果发生变动,
* 更新会比较麻烦。在具体工程实践上,可以把配置集中在一个地方管理,这样使用配置的应用就可以去配置管理中心获取具体
* 配置内容,修改时只需要修改配置中心中的值就可以。配置管理中心的相关内容会在后续的章节中介绍。
* 这个管理了整个业务的数据库集群的DataSource看起来还是比较优雅的,是一个all-in-one的解决方案。但是在
* 具体场景中,可能会比较重(不够轻量级),业务应用没有其他的选择,只能要么使用数据层的所有功能,要么就不用数据层。
* 大家再看看前面数据库集群的图,我们是可以对这个完整的DataSource的功能进行分层的。
*
* 在上图中,我们对原来的6个分库进行了分组,将管理同样的数据分在一个组,User分为了User1和User2,
* 其中User1-M与User1-S1、User1-S2所管理的数据是相同的(这里不考虑数据复制产生的延迟),只是角色不同
* (读/写、主/备的差异)。User2-M、User2-S1、User2-S2是类似的关系。
* 这里我们引入了groupDataSource,也就是分组的DataSource,用于管理整个业务数据库集群中的一组
* 数据库。上图中科院看到,groupDataSource相对于完整的DataSource来说,科院不管理具体的规则,也可以
* 不进行SQL解析。它是作为一个相对基础的数据源提供给业务的,那么groupDataSource重点解决的问题是什么
* 呢?
* 是在要访问这个分组中的数据库时,解决具体访问数据库的选择问题,具体的选择策略是groupDataSource
* 要完成的重点工作,包括根据事务、读/写等特性选择主备,以及根据权重在不同的库间进行选择,我们来看
* groupDataSource的配置。
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
* 从上面的配置可以看到,在Spring中关于 groupDataSource的配置已经不需要配置规则相关的部分了,而对数
* 据库自身的配置与之前是类似的,并且是通过Spring配置了多个groupDataSource给应用,也就是说应用完全知道
* 有几个数据库分组,并且在应用内部决定了数据访问应该走的分组,如果有需要库分组的工作(例如查询合并),是需要应
* 用自己来解决的。
* 看了这两个层面上的DataSource,可以看出,采用完整的DataSource,对于应用来说只会看到一个DataSource,
* 可以少关心很多事情,不过可能受到DataSource本身的限制;如果采用groupDataSource会有更大的自主权。
* 如果采用完整DataSource,对于后端业务的数据库集群的管理会更方便,例如我们可以进行一些扩容、缩容的工作不需要
* 应用太多的感知;而使用groupDataSource就意味着绑定了分组数量,这样要进行扩容、缩容时是需要应用进行较多
* 配合的。虽然使用groupDataSource不能进行整体的扩容、缩容,但是可以进行组内的扩容、缩容、主备切换等工作,
* 这也是groupDataSource最大的价值。在一些活动或者可预期的访问高峰前,可以给每个分组挂载上备库,通过配置
* 管理中心更改配置,就可以让应用使用新的数据库,同样,可以通过配置管理中心的配置更改下线数据库,以及进行主备库
* 的切换。
* 对数据源分组之后,我们再进行数据源功能切分,构建AtomDataSource。从下图中可以清楚地看到,AtomDataSource
* 仅管理一个具体的数据库。很多读者看到这里可能会有疑问,只管理一个具体的数据库使用各种第三方库提供的DataSource
* 的实现不就行了吗,为什么还要自己在数据层的实现中提供一个管理单个库的AtomDataSource呢?
* 像c3p0这样的开源第三方数据源的组件也都可以通过Spring配置或者应用中的代码来创建实例。另外一种数据源的使用方式
* 是在容器里的,例如JBoss中的数据源配置就是一个例子:
*
*
* User1-M
* jdbc:mysql://localhost:3306/sampledb
* com.mysql.jdbc.Driver
* test
* test
* org.jboss.resource.adapter.jdbc.vendor.MySQLException
* Sorter
*
*
*
* 可以看到,在JBoss容器中,配置的基本信息和前面的Spring里面的Apache-Basic-DataSource
* 的配置项是类似的。这两种方式的最大缺点都是不够动态,并且对于进行SQL执行的降级隔离等业务稳定性方面
* 没有很多的支持。
* 而假如我们通过AtomDataSource把单个数据库的数据源的配置集中存储,那么在定期更换密码、进行
* 机房迁移等需要更改ip地址或改变端口时就会非常方便。另外,通过AtomDataSource也可以帮助我们完成
* 在单库上的SQL的连接隔离,以及禁止某些SQL的执行等和稳定性相关的工作。
* 所以我们需要抽象出一个AtomDataSource,关于AtomDataSource的Spring配置方式,处理增加
* 的属性外,与BasicDateSource基本类似,这里不再具体给出。
* 下图所示把整体DataSource分层后为应用提供的三层数据源实现,应用可以根据自己的需求灵活的进行选择。
*
* 独立部署的数据访问层实现方式
* 从数据层的物理部署来说可以分为jar包方式和Proxy方式。
* 采用Proxy方式,客户端与Proxy之间的协议有两种选择:数据库协议和私有协议。
* 应用---->Proxy---->DB (数据库协议)
* 应用(数据库客户端)--->Proxy----->DB(私有协议)
* 采用数据库协议时,应用就会把Proxy看做一个数据库,然后使用数据库本身提供的JDBC
* 的实现就可以连接Proxy。因为应用到Proxy、Proxy到DB采用的都是数据库协议,所以,如
* 果使用的是同样的协议,例如都是Mysql协议,那么在一些场景下就可以减少一次Mysql协议
* 到对象然后再从对象到Mysql协议的转换。不过采用这种方式时Proxy要完全实现一套相关数
* 据库的协议,这个成本是比较高的,此外,应用到Proxy之间也没有办法做到连接复用。
* 采用私有协议时,Proxy对外提供的通信协议是我们自己设计的(这就类似我们在上一章
* 看到的服务框架中使用的协议),并且需要一个独立的数据层客户端,这个协议的好处是,Proxy
* 的实现会相对简单一些,并且应用到Proxy之间的连接是可以复用的。
*
* 读写分离的挑战和应对
* 通过读写分离的方案,可以分担主库(Master)的读的压力。这里面存在一个数据复制的问题,也
* 就是把主库的数据复制到备库(Slave)去。
*
* 主库从库非对称的场景:
* 1、数据结构相同,多从库对应一主库的场景。
* 通过Mysql的Replication可以解决复制的问题,并且延迟也相对较小。在对从库对应
* 一主库的情况下,业务应用只要根据自身的业务 特点把对数据延迟不太敏感的读切换到备库。
* eg.从5.33图看。
* 首先来看Slave。从成本上来说,Slave采用PC Server和Mysql的方案是比较划算
* 的。对于一个主库,需要多台采用Mysql的PC Server来对应,每台PC Server对应原来
* Master中的一部分数据,也就是进行了分库,如
* Master1=Slave1-1 + Slave1-2 +。。。。。。。。
* 如何复制,见下图5-35:
* 从上图可以看到,应用通过数据层访问数据库,通过消息系统就数据库的更新送出消息通知,数据
* 同步服务器获得消息通知后会进行数据的复制工作。分库规则配置则负责在读数据及数据同步服务器更新
* 分库时让数据层知道分库规则。数据同步服务器和DB主库的交互主要是根据被修改或新增的数据主键来
* 获取内容,采用的是行复制的方式。
* 可以说这是一个不优雅但是能够解决问题的方式。比较优雅的方式是基于数据库的日志来进行数据
* 的复制。
*
* 主/备库分库方式不同的数据复制
* 数据库复制在读写分离中是一个比较关键的任务。一般情况下进行的是对称的复制,也就是镜像,
* 但是也会有一些场景进行非对称复制。这里的非对称复制是指源数据和目标数据不是镜像关系,也指
* 源数据库和目标数据库是不同的实现。
*
* 这是一个虚拟的订单的例子。在主库中,我们根据买家id进行了分库,把所有买家的订单分到了
* 4个库中,保证了一个买家查询自己的交易记录时都说在一个数据库上查询的,不过卖家的查询就可
* 能跨多个库了。我们可以做一组备库,在其中按照卖家id进行分库,这样卖家从备库上查询自己的
* 订单时就都是在一个数据库中了。那么,这就需要我们完成这个非对称的复制,需要控制数据的分
* 发,而不是简单的进行镜像复制。
*
* 引入数据变更平台
* 复制到其他数据库是数据变更的一种场景,还有其他场景也会关心数据的变更,例如搜索
* 引擎的索引构建、缓存的失效等。我们可以考虑构建一个通用的平台来管理和控制数据变更。
* 如图,我们引入了Extractor和Applier,Extractor负责把数据源变更的信息加入
* 到数据分发平台中,而Applier的作用是把这些变更应用到相应的目标上,中间的数据分发平台是
* 由多个管道组成。不同的数据变更来源需要有不同的Extractor来进行解析和变更进入数据分发
* 平台的工作。进入到数据分发平台的变更信息就是标准化、结构化的数据了,根据不同的目标不同
* 的Applier把数据落地到目标数据源就可以了。因此,数据分发平台构建好之后,主要的工作就
* 是实现不同类型Extractor和Applier,从而接入更多类型的数据源。
*
* 如何做到数据平滑迁移
* 对于没有状态的应用,扩容和缩容时比较容易的。而对于数据库,扩容和缩容会涉及
* 数据的迁移。如果接受完全停机的扩容或者缩容,就会比较容易处理,停机后进行数据迁移,
* 然后校验并且恢复系统就可以了;但是如果不能接受长时间的停机,那该怎么办呢?
* 对数据库做平滑迁移的最大挑战是,在迁徙的过程中又会有数据的变化。可以考虑的
* 方案是,在开始进行数据迁移时,记录增量的日志,在迁移结束后,再对增量的变化进行处理
* 在最后,可以把要迁移的数据的写暂停,保证增量日志都处理完毕后,再切换规则,放开所有
* 的写,完成迁移工作。
*
*
* 消息中间件
* 消息中间件
* 1、发送一致性的解决方案:
* void fool(){
* //业务操作
* //例如写库,调用服务等
* //发送消息
* }
* 业务操作在前,发送消息在后,如果业务失败了还好,如果成功了,而这时这个应用
* 出问题,那么消息就发不出去了。
* 如果业务成功,应用也没有挂掉,但是消息系统挂掉了,也会导致消息发不出去。
* 另一种做法:
* void fool(){
* //发送消息
* //业务操作
* //写库,调用服务等
* }
* 这种方式更不可靠,在业务还没做时消息就发出了。
* 在具体的实践中,第一种做法丢失消息的比例相对很低,但对于必须保证一致性的场景
* 上面两种方案都不能接受。
* JMS是否可以实现消息的发送一致性
* JMS发送消息部分:
* Destination,是指消息所走通道的目标定义,也就是用来定义消息从发送端发出
* 后要走的通道,而不是最终接收方。Destination属于管理类的对象。
* ConnectionFactory,用于创建连接的对象,属于管理类对象。
* Connection,连接接口,负责Session的创建。
* Session,会话接口,这是一个非常重要的对象,消息的发送者、接收者以及消息
* 对象本身,都是由这个会话对象创建的。
* MessageConsumer,消息的消费者,也就是订阅消息并处理消息的对象。
* MessageProducer,消息的生产者,就是用来发送消息的对象。
* XXXMessage,指各种类型的消息对象,包括BytesMessage、MapMessage、
* ObjectMessage、StreamMessage和TextMessage 5 种。
* 在JMS消息模型中,有Queue和Topic之外,所以,前面的Destination、Connection-
* Factory、Connection、Session、MessageConsumer、MessageProducer都
* 有对应的子接口。表6-1显示了前面各要素在Queue模型(PTP Domain)和Topic模型(Pub/
* Sub Doamein)下的对应关系。
* 在JMS的API中,有很多以XA开头的接口,其实就是支持XA协议的接口。
* 在JMS的API中,有很多以XA开头的接口,其实就是支持XA协议的接口。
* XA系列的接口集中在ConnectionFactory、Connection和Session上,而Message-
* Producer、QueueSender、TopicPublisher、MessageConsumer、QueueReceiver
* 和TopicSubscriber则没有对应的XA对象。这是因为事务的控制是在Session层面上的,而
* Session是通过Connection创建的,Connection是通过ConnectionFactory创建的,
* 所以,这三个接口需要有XA系列对应的接口定义。Session、Connection、ConnectionFactory
* 在Queue模型和Topic模型下对应的各个接口也存在相应的XA系列的对应接口。
* 下面展示了消息最重要的元素(消息、发送者、接收者)与几个基本元素之间的关系。
* ConnectionFactory-->Connection--->Session--->Message
* Destination+Session--->MessageProducer
* Destination+Session--->MessageConsumer
* 在JMS中,如果不使用XA系列的接口实现,那么我们就无法直接得到发送消息给消息中间件及业务操作这两个事情
* 的事务保证,而JMS中定义的XA系列的接口就是为了实现分布式事务的支持(发送消息和业务操作很难做在一个本地事务
* 中,后面会讲到一些变通的做法)。但这会带来如下问题。
* 引入了分布式事务,这会带来一些开销并增加复杂度。
* 对于业务操作有限制,要求业务操作的资源必须支持XA协议,才能够与发送消息一起来做分布式事务。这会成为一
* 个限制,因为并不是所有需要与发送消息一起做成分布式事务的业务操作都支持XA协议。
* JMS可以解决消息发送一致性的问题,但是存在一些限制并且成本相对较高。
* 最终一致性方案的流程如图:
* 1、业务处理应用首先把消息发送给消息中间件,标记消息的状态为待处理。
* 2、消息中间件收到消息后,把消息存储在消息存储中,并不投递该消息。
* 3、消息中间件返回消息处理的结果(仅是入库的结果),结果是成功或者失败。
* 4、业务放收到消息中间件返回的结果并进行处理:
* 如果收到的结果是失败,那么句放弃业务处理,结束。
* 如果收到的结果是成功,则进行业务自身的操作。
* 5、业务操作完成,把业务操作的结果发送给消息中间件。
* 6、消息中间件收到业务操作结果,根据结果进行处理:
* 如果业务失败,则删除消息存储中的消息,结束。
* 如果业务成功,则更新消息存储中的消息状态为可发送,并且进行调度,进行消息的投递。
* 这就是整个流程。在这里读者一定会有一个疑问,即在最简单的版本中,我们只有业务操作和发消息两步,仍然
* 会可能产生很多异常,那么现在这个过程的步骤更多,产生异常的可能点更多,是如何能够保证业务操作和发送消息
* 到消息中间件是一致的呢?
* 我们对每个步骤可能产生的异常情况进行分析。
* 1、业务应用消息给消息中间件。这一步失败,业务操作不处理,消息也不会被存储到消息中间件,状态一致。
* 2、消息中间件把消息入库。这一步失败,可能造成结果有两个。一个是消息中间件失效,业务应用收不到消息
* 中间件的返回结果;二是消息中间件插入消息失败,并且有能力返回结果给应用,这是消息存储中都没有消息。
* 3、业务应用接收消息中间件返回结果异常。网络、消息中间件、业务自身等原因。如果业务应用自身没问题,
* 那么业务应用并不知道消息在消息中间件的处理结果,就会按照消息发送失败来处理,如果这时消息在消息中间件那
* 里入库成功,就会造成不一致。如果是业务应用有问题,那么如果消息在消息中间件中处理成功,也会造成不一致;
* 如果未处理成功,则一致。
* 4、业务应用进行业务操作。这一步不会产生太大问题。
* 5、业务应用发送业务操作结果给消息中间件。如果这一步出现问题,那么消息中间件将不知道该如何处理已经
* 存储在消息存储中的消息,可能造成不一致。
* 6、消息中间件更新消息状态,如果这一步出现问题,与上一步所造成的的结果是类似的。
* 从上面分析可以看出,需要了解的两个主要的控制状态和流程的节点就是业务应用和消息中间件。从业务应用和消息
* 中间件的视角梳理如下:
*
* 异常情况的状态主要有三种:
* 1、业务操作未进行,消息未入存储。
* 2、业务操作未进行,消息存入存储,状态为待处理。
* 3、业务操作成功,消息存入存储,状态为待处理。
* 第一种不需要进行处理,本身一致。2和3都需要了解业务操作的结果,然后来处理已经在消息存储中、状态为待处理
* 的消息。
* 了解业务操作的结果。由消息中间件主动询问业务应用,获取待处理消息所对应的业务操作的结果,然后业务应用
* 需要对业务操作的结果进行检查,并且把结果发送给消息中间件(业务处理结果由失败、成功、等待三种,等待是多出来
* 的一种状态,代表业务操作还在处理中),然后消息中间件根据这个处理结果,更新消息状态。可以说这是发送消息的一
* 个反向的流程。
* 同样这个流程也会出现很多异常,不过这个4步的流程都是为了确认业务处理操作结果,真正的操作只是根据业务
* 处理结果来更改消息的状态,所以,前面3步都与查询相关,如果失败就失败了,而最后一步的更新状态如果失败了,那
* 么就定时重复这个反向流程,重复查询。
* 发送消息的正向流程和检查业务操作结果的反向流程联合起来,就是解决业务操作与发送结果一致性的方案。在
* 大多数情况下,反向流程是不需要工作的。我们来看看正向流程是否带来了额外的负担。
* 伪代码:
* Result postMessage(Message,PostMessageCallback){
* //发送消息给消息中间件
* //获取返回结果
* //如果失败,返回失败
* //进行业务操作
* //获取业务操作结果
* //发送业务操作结果给消息中间件
* //返回处理结果
* }
*
* 解决消息中间件与使用者的强依赖问题
*
* 消息模型对消息接收的影响
* JMS中,Queue(点对点)和Topic(发布/订阅)两种模式
* JMS Queue模型:
* 消息生产者发送消息到JMS,形成队列,消费者从队列中取消息进行消费。也被称为Peer To
* Peer(PTP)方式。
* JMS Topic模型:
* 发送消息部分跟JMS QUeue是一样的,区别在于消息接收部分,Topic模型中,消费者独立接
* 收到达Topic的消息,彼此之间互不影响,并非是收到一个,其他消费者就不能接收了。又被称为Pub/Sub
* 方式。
*
* JMS中客户端连接的处理和带来的限制
* 在使用JMS时,每个Connection都有唯一的ClientId,永远标记连接的唯一性,也就是说刚才对
* Queue和Topic的介绍中,我们是默认一个接收应用只用了一个连接。限制看一下多连接的情况。
*
* 消息订阅者订阅消息的方式
* 持久订阅和非持久订阅
* 非持久订阅,含义是消息接收者和消息中间件直接的消息订阅的关系的存续,与消息接收者自身是否处于运动
* 状态有直接关系。也就是说,当消息接收者应用启动时,就建立了订阅关系,这时可以收到消息;而如果消息接收者
* 应用结束了,那么消息订阅关系也就不存在了,这时的消息是不会为消息接收者保留的;当消息接收者应用再次启动,
* 又会重新建立订阅关系,之后的消息又可以正常收到。
* 持久订阅方式。只要订阅关系建立,即使接收者应用停止,消息也会保留,等等下次应用启动后再投递给接收者。
*
* 保证消息可靠性的做法
* 消息发送端可靠性的保证
* 消息存储的可靠性保证
* 消息从发送者发送到消息中间件后,消息存储是非常重要的一个环节。当消息从发送者端发送出来后,消息的可
* 靠性保证就靠存储了。存储器分为内存和外存,内存断电内容会丢失,外存不受影响,一般消息数据放在外存储器上。
* 持久存储。
* 1、实现基于文件的消息存储
* 场景:要求很高的消息吞吐量,消息的写入速度要很快,并且可以支持对消息的灵活的检索,但是由于
* 消息本身不是特别大(1.5K左右),因此对消息的顺序不十分敏感。选择关系型数据库来进行消息存储,并
* 参考了ActiveMQ中的kaha Persistence的一个实现。当时没有选择分布式文件系统,是因为那时能够选择
* 的分布式文件系统自身的稳定性和性能还有待改进;此外,分布式文件系统对消息灵活的检索是不支持的,需要
* 再进行额外的工作。而没有选择NoSQL的原因是,当时NoSQL不像现在这么成熟和广泛使用;而且在消息的检索
* 方面虽然比分布式文件系统容易一些,但是也不够直接;此外,NoSQL的产品一般都有很好的扩展性,在数据量增大
* 时能够很好地进行数据迁移、扩容,这对于通用系统来说是个很好的特性,但对于我们当时需要的消息系统来说并不
* 重要。另外,参考Active MQ的 Kaha Persistence实现的主要考虑是想把消息直接存储在本地磁盘,而不要
* 额外的独立存储,并且针对机械盘的特点尽量进行顺序写和顺序读。
* 遇到的困难:
* 1、完全重写一个可靠的单机的存储引擎,投入很大。
* 2、各种场景测试没有问题不代表没有问题。保证存储的可靠性挑战比较大。
* 3、由于关注吞吐量不关注消息顺序,会导致原本连续的消息存储的文件中有些消息不需要了,有些需要,
* 形成文件的空洞。
* 4、对消息的检索处理需要考虑索引对内存的消耗,我们必须考虑索引不能完全加载到内存的情况,这涉及
* 了内存和磁盘文件的交换功能,也涉及了如何能够处理过程的高效。
* 可以看到,完全实现消息存储需要解决的问题还很多,也需要较多的投入。而如果要提升单机存储的可靠性,
* 应对断电、程序崩溃等问题,那么久要求我们去实现一些简单数据库存储引擎或者一些NoSQL的单机引擎的工作。
* 因此,我们转向了采用现有的数据库引擎的实现。
*
* 2、采用数据库作为消息存储
* 冗余、宽表。
* 对于消息来说,可以把需要存储的数据分为以下三块。
* 1、消息的Header信息
* 主要是指消息的一些基本信息,例如消息Id、创建时间、投递次数、优先级、自定义的键值对属性等。
* 2、消息的Body
* 消息的具体内容,消息的Body是否与消息的Header信息放在一条记录中是需要考虑的。经过分析和验证,
* 我们选择了把Header和Body放在了一起,其中一个因素是消息体的内容并不大。
* 3、消息的投递对象
* 是指单条消息要投递到的目标集群的ClusterId。
* 3、基于双机内存的消息存储
* 使用文件系统或者数据库来进行消息存储时,因为磁盘IO的原因,系统性能都会受到限制。一个改进方案是用
* 混合方式进行存储的管理。正常情况下,消息持久存储是不工作的,而基于内存来存储消息则能够提供很高的吞吐量。
* 一旦一个机器出现故障,则停止另一台机器的数据写操作,并把当前数据落盘。如下图
* 只要不遇到两台基于内存的消息中间件机器同时出故障,一台出问题,另一台写消息到持久存储的过程不出问题,
* 消息是很安全的。
* 这种方式适合于消息到了消息中间件后大部分消息能够及时被消费掉的情况,它可以很好地提升性能。
*
* 消息系统的扩容处理
* 1、如何扩容,扩容可通过软负载中心完成。
* 在同一个存储中为了区分存储的消息来自哪个消息中间件应用。可以给每条消息增加一个service标识的字段,
* 当有新加入的消息中间件时,会使用新的server标识。这一方案需要应对的问题是,如果有消息中间件应用长期不可用
* 的话,我们就需要加入一个和它具有同样的server标识的机器来代替它,或者把通过这个消息中间件进入到消息系统
* 中但还没有完成投递的消息分给其他机器处理,也就是让另一台机器承载剩余消息的投递工作。
* 2、消息存储的扩容处理
*
* 消息投递的可靠性保证
* 1、消息投递简介
* 显式地收到接收者确认消息处理完毕的信号才能删除消息。决定消息是否删除从应用层的响应入手。
* 2、投递处理的优化
* 投递时采用多线程的方式处理。
* 一种方式是每个线程处理一个消息并且等待处理结束后再进行下一条消息的处理。
* 另一种方式是,把处理消息结果返回的处理工作放到另外的线程池来完成,也就是投递线程完成消息到网络
* 的投递后就可以接着处理下一个消息,保证投递的环节不会被堵死。等待返回结果的消息会先放在内存中,不占用
* 线程资源,等有了最后的结果时,再放入另外的线程池中处理。这种方式把占用线程池的等待方式变为了靠网络
* 收到消息处理结果后的主动响应方式。
* 收到消息的处理结果后,通过数据库的batch来处理消息的更新、删除操作,提高性能。
* 单机多订阅者共享
* 消息只发送一次,然后传到单机的多订阅生成多个实例处理。
*
* 订阅者视角的消息重复的产生和应对
* 消息重复的产生原因
* 一、消息发送端应用的消息重复发送
* 1、消息---》消息中间件,收到并存储,网络异常,应用端未收到发送成功的返回,产生重试。
* 2、超时重试。
* 3、中间件问题,未收到发送成功的返回,产生重试。
* 解决方法,重试发送消息时使用同样的消息Id,不要在消息中间件端产生消息Id,这样可以避免这类情况。
* 二、消息到了消息存储,由消息中间件进行向外的投递时产生重复。
* 1、消息被投递到消息接收者应用进行处理,处理完毕后应用出问题了,消息中间件不知道消息处理结果,
* 会再次投递。
* 2、投递被处理完后,网络出现问题,导致再次投递。
* 3、超时
* 4、消息中间件出问题。
* 5、消息存储障碍
* 一种处理方式是要求消息接收者来处理这种复杂的情况,也就是要求消息接收者的消息处理是幂等操作。
* 幂等是一个数学概念,常见于抽象代数中。有两种主要的定义:
* 在某二元运算下,幂等元素是指被自己重复运算的结果等于它自身的元素。
* 在某一元运算为幂等时,其两次作用在任一元素后会和其作用一次的结果相同。例如,高斯符号便是幂等的。
* 对于消息接收端的情况,幂等的含义是采用同样的输入多次调用处理函数,会得到同样的结果。例如,一个SQL操作:
* update stat_table set count=10 where id=1;
* 这个操作多次执行,id等于1的记录中的count字段的值都为10,这个操作就是幂等的,我们不用担心这个操作被
* 重复。当然,这个SQL中的数字10可以是来自消息体的一个输入。
* 再来看另外一个SQL操作:
* update state_table set count=count +1 where id=1;
* 这样的SQL操作就不是幂等的,一旦重复,结果就会产生变化。
* 因此应对消息重复的办法是,使消息接收端的处理是一个幂等操作。这样的做法降低了消息中间件的整体复杂度,不过
* 也给使用消息中间件的消息接收端应用带来了一定的限制和门槛。
*
* JMS的消息确认方式与消息重复的关系
* 在JMS中,消息接收端对收到消息进行确认,有以下几种选择。
* 1、AUTO_ACKNOWLEDGE
* 这是自动确认的方式,就是说当JMS的消息接收者收到消息后,JMS的客户端会进行确认。但是确认时可能
* 消息还没来得及处理或者尚未处理完成,所以这种确认方式对于消息投递处理来说是不可靠的。
* 2、CLIENT_ACKNOWLEDGE
* 这是客户端自己确认的方式,也就是说客户端如果要确认消息处理成功,告诉服务端确认消息时,需要主动调用
* Message接口的acknowledge()方法以进行消息接收成功的确认。这种方式把控制权完全交给了接收消息的客户端
* 应用。
* 3、DUPS_OK_ACKNOWLEDGE
* 这种方式是在消息接收方的消息处理函数执行后进行确认,一方面保证了消息一定是处理结束后才进行确认,另外
* 一方面也不需要客户端主动调用Message接口的acknowledge()方法了。
* 上述三种确认方式是通过JMS的Connection在创建Queue或者Topic时设置的。
* 从上面可以看出,消息接收者对于消息的接收会出现下面两种情况:
* at least once(至少一次)
* 至少一次,就是说消息被传给消息接收者至少一次,也可能多于一次,这种情况类似前面小节的消息重复
* 处理的情况。采用DUPS_OK_ACKNOWLEDGE或CLIENT_ACKNOWLEDGE没收并且在处理消息前没有确认的
* 话,就可能产生这种现象。
* at most once(至多一次)
* 采用AUTO_ACKNOWLEDGE或CLIENT_ACKNOWLEDGE模式并且在接收到消息后就立刻确认时
* 产生的情况。
*
* 消息投递的其他属性支持
* 消息优先级
* 订阅者消息处理顺序和分级订阅
* 自定义属性
*
* 单机多队列的优化
* 单机多队列的隔离完成了对消息的有序支持。在具体工程中,单机的队列数量特别多,性能就会明显下降,原因是队列数
* 量很多时,消息写入接近于随机写了。一个改进措施是把发送到这台机器的数据进行顺序写入,然后再根据队列做一个索引,每
* 个队列的索引是独立的,其中保持的只是相对于存储数据的物理队列的索引位置。在单机上,物理队列的数量设置与磁盘数有关。
* 这样改进的好处:
* 队列轻量化,单个队列数据量非常少
* 对磁盘的访问串行化,避免磁盘竞争,不会因为队列增加导致IOWAIT增高。
* 采用这个方案可以消除原来大量的数据的随机写,但也有自身的缺点:
* 写虽然是顺序写,但是读却变成了完全的随机读。
* 读一条消息时,会先读逻辑队列,再读物理队列,增加了开销。
* 需要保证物理队列与逻辑队列完全一致,增加了编程的复杂度。
* 对于上述三个缺点需要进一步改进,以克服或降低影响:
* 1、随机读,尽可能让读命中PAGECACHE,减少IO读操作,所以内存越大越好。如果系统中堆积的消息过多,读数据
* 访问磁盘时会不会由于随机读导致系统性能急剧下降呢?答案是否定的。
* >访问PAGECACHE时,即使只访问1KB的消息,系统也会提前预读出更多数据,在下次读时,就可能命中内存。
* >随机访问物理队列磁盘数据时,系统IO调度算法设置为NOOP方式,会在一定程度上将完全的随机读变成顺序
* 跳跃读的方式,而顺序跳跃读会比完全的随机读性能高5倍以上。另外4KB的消息在完全随机访问情况下,仍然可以达
* 到每秒10000次以上的读性能。
* 2、由于逻辑队列存储数量极少,而且是顺序额读,在PAGECACHE预读作用下,逻辑队列的性能几乎与内存一致,
* 所以可以忽略队列对读性能的阻碍。
* 3、物理队列中存储了所有的元信息,类似MySQL的binlog、Oracle的redolog,所以只要有物理队列再,即使
* 逻辑队列数据丢失,仍然可以恢复过来。
*
* 解决本地消息存储的可靠性
* 消息的可靠性永远是一个很重要的话题,在这个方案中我们考虑采用消息同步复制的方式解决可靠性的问题。
* 1、把单个的消息中间件机器变为主(Master)备(Slave)两个节点,Slave节点订阅Master节点上的
* 所有消息,以进行消息的备份。不过需要注意这是一个异步的操作,Slave订阅收到消息总会比Master略少一
* 些,存在着丢失消息的可能。这种方式比较类似于MySQL的replication。
* 2、同样是把单个节点扩展到Master/Slave两个节点,但是采用的是同步复制的方式,而非订阅的方式,
* 也就是说Master收到消息后会主动写往Slave,并且收到了Slave的响应后才向消息发送者返回“成功”的消息。
* 对于消息数据安全性要求非常严格的场景,采用第二种方式更加安全和保险。
*
* 如何支持队列的扩容
* 扩容也是整个系统中一个很重要的环节。在保证顺序的情况下进行扩容的难度会更大。基本的策略是让向一个队列
* 写入数据的消息发送者能够知道应该把消息写入迁移到新的队列中,并且也需要让消息订阅者知道,当前的队列消费完
* 数据后需要迁移到新队列去消费消息。
* 其中有如下几个关键点:
* 1、原队列在开始扩容后需要有一个标志,即便有新消息过来,也不再接收。
* 2、通知消息发送端新的队列的位置。
* 3、对于消息接收端,对原来队列的定位会收到新旧两个位置,当旧队列的数据接收完毕后,则会只关心新队列
* 的位置,完成切换。
*
* Push和Pull方式的对比
* 中间件的两种方式 Push、Pull
*
* 软负载中心与集中配置管理
* 软负载中心有两个最基础的职责:
* 一是聚合地址信息。无论是服务框架中需要用到的服务提供者地址,还是消息中间件系统中的消息中间件应用的地址,
* 都需要由软负载中心去聚合地址列表,形成一个可供服务调用者及消息的发送者、接收者直接使用的列表。
* 二是生命周期感知。软负载中心需要对服务的上下线自动感知,并且根据这个变化去更新服务地址数据,形成新的地址
* 列表后,把数据传给需要数据的调用者或者消息的发送者和接收者。
*
* 软负载中心结构
* 软负载中心包括两个部分,一个是软负载中心的服务端,另一个是软负载中心的客户端。服务端主要负责感知
* 提供服务的机器是否在线,聚合提供者的机器信息,并且负责把数据传送给使用数据的应用。客户端承载了两个角色,
* 作为服务提供者,客户端主要是把服务提供者提供服务的具体信息主动传给服务端,并且随着提供服务的变化去更新
* 数据;而作为服务使用者,客户端主要向服务端告知自己所需要的数据并负责更新数据,还要进行本地的数据缓存,
* 通过本地的数据缓存,使得每次去请求服务获取列表都是一个本地操作,从而提升效率和性能。
*
*
*
*/
}