Java分布式相关知识-4.分布式系统Dubbo

1.什么是分布式系统

Java分布式相关知识-4.分布式系统Dubbo_第1张图片
01_什么是最简单的分布式系统.png

2.为什么将系统拆分成分布式

  • 拆分系统的好处
    1)要是不拆分,一个大系统几十万行代码,20个人维护一份代码,简直是悲剧啊。代码经常改着改着就冲突了,各种代码冲突和合并要处理,非常耗费时间;经常我改动了我的代码,你调用了我,导致你的代码也得重新测试,麻烦的要死;然后每次发布都是几十万行代码的系统一起发布,大家得一起提心吊胆准备上线,几十万行代码的上线,可能每次上线都要做很多的检查,很多异常问题的处理,简直是又麻烦又痛苦;而且如果我现在打算把技术升级到最新的spring版本,还不行,因为这可能导致你的代码报错,我不敢随意乱改技术。
    
    假设一个系统是20万行代码,其中小A在里面改了1000行代码,但是此时发布的时候是这个20万行代码的大系统一块儿发布。就意味着20万上代码在线上就可能出现各种变化,20个人,每个人都要紧张地等在电脑面前,上线之后,检查日志,看自己负责的那一块儿有没有什么问题。
    
    小A就检查了自己负责的1万行代码对应的功能,确保ok就闪人了;结果不巧的是,小A上线的时候不小心修改了线上机器的某个配置,导致另外小B和小C负责的2万行代码对应的一些功能,出错了
    
    几十个人负责维护一个几十万行代码的单块应用,每次上线,准备几个礼拜,上线 -> 部署 -> 检查自己负责的功能
    
    最近从2013年到现在,5年的时间里,2013年以前,基本上都是BAT的天下;2013年开始,有几个小巨头开始快速的发展,上市,几百亿美金,估值都几百亿美金;2015年,出现了除了BAT以外,又有几个互联网行业的小巨头出现。
    
    BAT工作,在市值几百亿美金的小巨头工作
    
    有某一个小巨头,现在估值几百亿美金的小巨头,5年前刚开始搞的时候,核心的业务,几十个人,维护一个单块的应用
    
    维护单块的应用,在从0到1的环节里,是很合适的,因为那个时候,是系统都没上线,没什么技术挑战,大家有条不紊的开发。ssh + mysql + tomcat,可能会部署几台机器吧。
    
    结果不行了,后来系统上线了,业务快速发展,10万用户 -> 100万用户 -> 1000万用户 -> 上亿用户了。
    
    2)拆分了以后,整个世界清爽了,几十万行代码的系统,拆分成20个服务,平均每个服务就1~2万行代码,每个服务部署到单独的机器上。20个工程,20个git代码仓库里,20个码农,每个人维护自己的那个服务就可以了,是自己独立的代码,跟别人没关系。再也没有代码冲突了,爽。每次就测试我自己的代码就可以了,爽。每次就发布我自己的一个小服务就可以了,爽。技术上想怎么升级就怎么升级,保持接口不变就可以了,爽。
    
    所以简单来说,一句话总结,如果是那种代码量多达几十万行的中大型项目,团队里有几十个人,那么如果不拆分系统,开发效率极其低下,问题很多。但是拆分系统之后,每个人就负责自己的一小部分就好了,可以随便玩儿随便弄。分布式系统拆分之后,可以大幅度提升复杂系统大型团队的开发效率。
    
    但是同时,也要提醒的一点是,系统拆分成分布式系统之后,大量的分布式系统面临的问题也是接踵而来,所以后面的问题都是在围绕分布式系统带来的复杂技术挑战在说。
    
  • 如何拆分系统
    这个问题说大可以很大,可以扯到领域驱动模型设计上去,说小了也很小,我不太想给大家太过于学术的说法,因为你也不可能背这个答案,过去了直接说吧。还是说的简单一点,大家自己到时候知道怎么回答就行了。
    
    系统拆分分布式系统,拆成多个服务,拆成微服务的架构,拆很多轮的。上来一个架构师第一轮就给拆好了,第一轮;团队继续扩大,拆好的某个服务,刚开始是1个人维护1万行代码,后来业务系统越来越复杂,这个服务是10万行代码,5个人;第二轮,1个服务 -> 5个服务,每个服务2万行代码,每人负责一个服务
    
    如果是多人维护一个服务,<=3个人维护这个服务;最理想的情况下,几十个人,1个人负责1个或2~3个服务;某个服务工作量变大了,代码量越来越多,某个同学,负责一个服务,代码量变成了10万行了,他自己不堪重负,他现在一个人拆开,5个服务,1个人顶着,负责5个人,接着招人,2个人,给那个同学带着,3个人负责5个服务,其中2个人每个人负责2个服务,1个人负责1个服务
    
    我个人建议,一个服务的代码不要太多,1万行左右,两三万撑死了吧
    
    大部分的系统,是要进行多轮拆分的,第一次拆分,可能就是将以前的多个模块该拆分开来了,比如说将电商系统拆分成订单系统、商品系统、采购系统、仓储系统、用户系统,等等吧。
    
    但是后面可能每个系统又变得越来越复杂了,比如说采购系统里面又分成了供应商管理系统、采购单管理系统,订单系统又拆分成了购物车系统、价格系统、订单管理系统。
    
    扯深了实在很深,所以这里先给大家举个例子,你自己感受一下,核心意思就是根据情况,先拆分一轮,后面如果系统更复杂了,可以继续分拆。你根据自己负责系统的例子,来考虑一下就好了。
    
  • 拆分后不用dubbo可以吗?
    当然可以了,大不了最次,就是各个系统之间,直接基于spring mvc,就纯http接口互相通信呗,还能咋样。但是这个肯定是有问题的,因为http接口通信维护起来成本很高,你要考虑超时重试、负载均衡等等各种乱七八糟的问题,比如说你的订单系统调用商品系统,商品系统部署了5台机器,你怎么把请求均匀地甩给那5台机器?这不就是负载均衡?你要是都自己搞那是可以的,但是确实很痛苦。
    
    所以dubbo说白了,是一种rpc框架,就是本地就是进行接口调用,但是dubbo会代理这个调用请求,跟远程机器网络通信,给你处理掉负载均衡了、服务实例上下线自动感知了、超时重试了,等等乱七八糟的问题。那你就不用自己做了,用dubbo就可以了。
    

3.dubbo的工作原理

  • dubbo的工作原理

    Java分布式相关知识-4.分布式系统Dubbo_第2张图片
    01_dubbo的工作原理.png
    第一层:service层,接口层,给服务提供者和消费者来实现的
    第二层:config层,配置层,主要是对dubbo进行各种配置的
    第三层:proxy层,服务代理层,透明生成客户端的stub和服务单的skeleton
    第四层:registry层,服务注册层,负责服务的注册与发现
    第五层:cluster层,集群层,封装多个服务提供者的路由以及负载均衡,将多个实例组合成一个服务
    第六层:monitor层,监控层,对rpc接口的调用次数和调用时间进行监控
    第七层:protocol层,远程调用层,封装rpc调用
    第八层:exchange层,信息交换层,封装请求响应模式,同步转异步
    第九层:transport层,网络传输层,抽象mina和netty为统一接口
    第十层:serialize层,数据序列化层
    
    工作流程:
    
    1)第一步,provider向注册中心去注册
    2)第二步,consumer从注册中心订阅服务,注册中心会通知consumer注册好的服务
    3)第三步,consumer调用provider
    4)第四步,consumer和provider都异步的通知监控中心
    
  • 注册中心挂了可以继续通信吗?

    • 可以,consumer第一次访问从注册中心获取服务信息会缓存在本地。

4.dubbo支持的通信协议

Java分布式相关知识-4.分布式系统Dubbo_第3张图片
01_dubbo的网络通信协议.png
  • dubbo支持不同的通信协议
    1)dubbo协议
    
    dubbo://192.168.0.1:20188
    
    默认就是走dubbo协议的,单一长连接,NIO异步通信,基于hessian作为序列化协议
    
    适用的场景就是:传输数据量很小(每次请求在100kb以内),但是并发量很高
    
    为了要支持高并发场景,一般是服务提供者就几台机器,但是服务消费者有上百台,可能每天调用量达到上亿次!此时用长连接是最合适的,就是跟每个服务消费者维持一个长连接就可以,可能总共就100个连接。然后后面直接基于长连接NIO异步通信,可以支撑高并发请求。
    
    否则如果上亿次请求每次都是短连接的话,服务提供者会扛不住。
    
    而且因为走的是单一长连接,所以传输数据量太大的话,会导致并发能力降低。所以一般建议是传输数据量很小,支撑高并发访问。
    
    2)rmi协议
    
    走java二进制序列化,多个短连接,适合消费者和提供者数量差不多,适用于文件的传输,一般较少用
    
    3)hessian协议
    
    走hessian序列化协议,多个短连接,适用于提供者数量比消费者数量还多,适用于文件的传输,一般较少用
    
    4)http协议
    
    走json序列化
    
    5)webservice
    
    走SOAP文本序列化
    
  • dubbo支持的序列化协议
    所以dubbo实际基于不同的通信协议,支持hessian、java二进制序列化、json、SOAP文本序列化多种序列化协议。但是hessian是其默认的序列化协议。
    
  • 5.dubbo的负载均衡

  • dubbo相关知识点
    (1)dubbo工作原理:服务注册,注册中心,消费者,代理通信,负载均衡
    (2)网络通信、序列化:dubbo协议,长连接,NIO,hessian序列化协议
    (3)负载均衡策略,集群容错策略,动态代理策略:dubbo跑起来的时候一些功能是如何运转的,怎么做负载均衡?怎么做集群容错?怎么生成动态代理?
    (4)dubbo SPI机制:你了解不了解dubbo的SPI机制?如何基于SPI机制对dubbo进行扩展?
    
  • dubbo负载均衡策略
    Java分布式相关知识-4.分布式系统Dubbo_第4张图片
    01_dubbo负载均衡.png
    1)random loadbalance
    
    默认情况下,dubbo是random load balance随机调用实现负载均衡,可以对provider不同实例设置不同的权重,会按照权重来负载均衡,权重越大分配流量越高,一般就用这个默认的就可以了。
    
    2)roundrobin loadbalance
    
    还有roundrobin loadbalance,这个的话默认就是均匀地将流量打到各个机器上去,但是如果各个机器的性能不一样,容易导致性能差的机器负载过高。所以此时需要调整权重,让性能差的机器承载权重小一些,流量少一些。
    
    跟运维同学申请机器,有的时候,我们运气,正好公司资源比较充足,刚刚有一批热气腾腾,刚刚做好的一批虚拟机新鲜出炉,配置都比较高。8核+16g,机器,2台。过了一段时间,我感觉2台机器有点不太够,我去找运维同学,哥儿们,你能不能再给我1台机器,4核+8G的机器。我还是得要。
    
    3)leastactive loadbalance
    
    这个就是自动感知一下,如果某个机器性能越差,那么接收的请求越少,越不活跃,此时就会给不活跃的性能差的机器更少的请求
    
    4)consistanthash loadbalance
    
    一致性Hash算法,相同参数的请求一定分发到一个provider上去,provider挂掉的时候,会基于虚拟节点均匀分配剩余的流量,抖动不会太大。如果你需要的不是随机负载均衡,是要一类请求都到一个节点,那就走这个一致性hash策略。
    
  • dubbo集群容错策略
    1)failover cluster模式
    
    失败自动切换,自动重试其他机器,默认就是这个,常见于读操作
    
    2)failfast cluster模式
    
    一次调用失败就立即失败,常见于写操作
    
    3)failsafe cluster模式
    
    出现异常时忽略掉,常用于不重要的接口调用,比如记录日志
    
    4)failbackc cluster模式
    
    失败了后台自动记录请求,然后定时重发,比较适合于写消息队列这种
    
    5)forking cluster
    
    并行调用多个provider,只要一个成功就立即返回
    
    6)broadcacst cluster
    
    逐个调用所有的provider
    
  • dubbo动态代理策略
    默认使用javassist动态字节码生成,创建代理类
    
    但是可以通过spi扩展机制配置自己的动态代理策略
    

6.dubbo的SPI原理

Java分布式相关知识-4.分布式系统Dubbo_第5张图片
01_dubbo的SPI原理.png

7.基于dubbo进行服务治理、服务降级、失败重试以及超时重试

  • 服务治理
    1)调用链路自动生成
    
    一个大型的分布式系统,或者说是用现在流行的微服务架构来说吧,分布式系统由大量的服务组成。那么这些服务之间互相是如何调用的?调用链路是啥?说实话,几乎到后面没人搞的清楚了,因为服务实在太多了,可能几百个甚至几千个服务。
    
    那就需要基于dubbo做的分布式系统中,对各个服务之间的调用自动记录下来,然后自动将各个服务之间的依赖关系和调用链路生成出来,做成一张图,显示出来,大家才可以看到对吧。
    
    服务A -> 服务B -> 服务C
                  -> 服务E
          -> 服务D
                  -> 服务F
          -> 服务W
    
    2)服务访问压力以及时长统计
    
    需要自动统计各个接口和服务之间的调用次数以及访问延时,而且要分成两个级别。一个级别是接口粒度,就是每个服务的每个接口每天被调用多少次,TP50,TP90,TP99,三个档次的请求延时分别是多少;第二个级别是从源头入口开始,一个完整的请求链路经过几十个服务之后,完成一次请求,每天全链路走多少次,全链路请求延时的TP50,TP90,TP99,分别是多少。
    
    这些东西都搞定了之后,后面才可以来看当前系统的压力主要在哪里,如何来扩容和优化啊
    
    3)其他的
    
    服务分层(避免循环依赖),调用链路失败监控和报警,服务鉴权,每个服务的可用性的监控(接口调用成功率?几个9?)99.99%,99.9%,99%
    
  • 服务降级
    比如说服务A调用服务B,结果服务B挂掉了,服务A重试几次调用服务B,还是不行,直接降级,走一个备用的逻辑,给用户返回响应
    
  • 失败重试和超时重试
    所谓失败重试,就是consumer调用provider要是失败了,比如抛异常了,此时应该是可以重试的,或者调用超时了也可以重试。
    
    
    
    某个服务的接口,要耗费5s,你这边不能干等着,你这边配置了timeout之后,我等待2s,还没返回,我直接就撤了,不能干等你
    
    如果是超时了,timeout就会设置超时时间;如果是调用失败了自动就会重试指定的次数
    
    你就结合你们公司的具体的场景来说说你是怎么设置这些参数的,timeout,一般设置为200ms,我们认为不能超过200ms还没返回
    
    retries,3次,设置retries,还一般是在读请求的时候,比如你要查询个数据,你可以设置个retries,如果第一次没读到,报错,重试指定的次数,尝试再次读取2次
    

8.分布式系统的幂等性问题

  • 场景

    假如你有个服务提供一个接口,结果这服务部署在了5台机器上,接着有个接口就是付款接口。然后人家用户在前端上操作的时候,不知道为啥,总之就是一个订单不小心发起了两次支付请求,然后这俩请求分散在了这个服务部署的不同的机器上,好了,结果一个订单扣款扣两次?尴尬了。。。
    
    或者是订单系统调用支付系统进行支付,结果不小心因为网络超时了,然后订单系统走了前面我们看到的那个重试机制,咔嚓给你重试了一把,好,支付系统收到一个支付请求两次,而且因为负载均衡算法落在了不同的机器上,尴尬了。。。
    
    Java分布式相关知识-4.分布式系统Dubbo_第6张图片
    01_分布式系统接口的幂等性问题.png
  • 保证幂等性主要是三点

    (1)对于每个请求必须有一个唯一的标识,举个例子:订单支付请求,肯定得包含订单id,一个订单id最多支付一次,对吧
    
    (2)每次处理完请求之后,必须有一个记录标识这个请求处理过了,比如说常见的方案是在mysql中记录个状态啥的,比如支付之前记录一条这个订单的支付流水,而且支付流水采
    
    (3)每次接收请求需要进行判断之前是否处理过的逻辑处理,比如说,如果有一个订单已经支付了,就已经有了一条支付流水,那么如果重复发送这个请求,则此时先插入支付流水,orderId已经存在了,唯一键约束生效,报错插入不进去的。然后你就不用再扣款了。
    
    (4)上面只是给大家举个例子,实际运作过程中,你要结合自己的业务来,比如说用redis用orderId作为唯一键。只有成功插入这个支付流水,才可以执行实际的支付扣款。
    
    要求是支付一个订单,必须插入一条支付流水,order_id建一个唯一键,unique key
    
    所以你在支付一个订单之前,先插入一条支付流水,order_id就已经进去了
    
    你就可以写一个标识到redis里面去,set order_id payed,下一次重复请求过来了,先查redis的order_id对应的value,如果是payed就说明已经支付过了,你就别重复支付了
    
    然后呢,你再重复支付这个订单的时候,你写尝试插入一条支付流水,数据库给你报错了,说unique key冲突了,整个事务回滚就可以了
    
    来保存一个是否处理过的标识也可以,服务的不同实例可以一起操作redis。
    

9.分布式系统接口调用顺序性

  • 场景

    Java分布式相关知识-4.分布式系统Dubbo_第7张图片
    01_分布式系统接口调用顺序性.png
    其实分布式系统接口的调用顺序,也是个问题,一般来说是不用保证顺序的。但是有的时候可能确实是需要严格的顺序保证。给大家举个例子,你服务A调用服务B,先插入再删除。好,结果俩请求过去了,落在不同机器上,可能插入请求因为某些原因执行慢了一些,导致删除请求先执行了,此时因为没数据所以啥效果也没有;结果这个时候插入请求过来了,好,数据插入进去了,那就尴尬了。
    
    本来应该是先插入 -> 再删除,这条数据应该没了,结果现在先删除 -> 再插入,数据还存在,最后你死都想不明白是怎么回事。
    
  • 建议

    首先,一般来说,我个人给你的建议是,你们从业务逻辑上最好设计的这个系统不需要这种顺序性的保证,因为一旦引入顺序性保障,会导致系统复杂度上升,而且会带来效率低下,热点数据压力过大,等问题。
    
    下面我给个我们用过的方案吧,简单来说,首先你得用dubbo的一致性hash负载均衡策略,将比如某一个订单id对应的请求都给分发到某个机器上去,接着就是在那个机器上因为可能还是多线程并发执行的,你可能得立即将某个订单id对应的请求扔一个内存队列里去,强制排队,这样来确保他们的顺序性。
    
    但是这样引发的后续问题就很多,比如说要是某个订单对应的请求特别多,造成某台机器成热点怎么办?解决这些问题又要开启后续一连串的复杂技术方案。。。曾经这类问题弄的我们头疼不已,所以,还是建议什么呢?
    
    最好是比如说刚才那种,一个订单的插入和删除操作,能不能合并成一个操作,就是一个删除,或者是什么,避免这种问题的产生。
    

10.设计一个类似dubbo的rpc框架

(1)上来你的服务就得去注册中心注册吧,你是不是得有个注册中心,保留各个服务的信心,可以用zookeeper来做,对吧
(2)然后你的消费者需要去注册中心拿对应的服务信息吧,对吧,而且每个服务可能会存在于多台机器上
(3)接着你就该发起一次请求了,咋发起?蒙圈了是吧。当然是基于动态代理了,你面向接口获取到一个动态代理,这个动态代理就是接口在本地的一个代理,然后这个代理会找到服务对应的机器地址
(4)然后找哪个机器发送请求?那肯定得有个负载均衡算法了,比如最简单的可以随机轮询是不是
(5)接着找到一台机器,就可以跟他发送请求了,第一个问题咋发送?你可以说用netty了,nio方式;第二个问题发送啥格式数据?你可以说用hessian序列化协议了,或者是别的,对吧。然后请求过去了。。
(6)服务器那边一样的,需要针对你自己的服务生成一个动态代理,监听某个网络端口了,然后代理你本地的服务代码。接收到请求的时候,就调用对应的服务代码,对吧。

11.ZK的使用场景

  • 场景
    Java分布式相关知识-4.分布式系统Dubbo_第8张图片
    01_zookeeper的分布式协调场景.png

    Java分布式相关知识-4.分布式系统Dubbo_第9张图片
    02_zookeeper的分布式锁场景.png

    Java分布式相关知识-4.分布式系统Dubbo_第10张图片
    03_zookeeper的元数据_配置管理场景.png

    Java分布式相关知识-4.分布式系统Dubbo_第11张图片
    04_zookeeper的HA高可用性场景.png
    (1)分布式协调:这个其实是zk很经典的一个用法,简单来说,就好比,你A系统发送个请求到mq,然后B消息消费之后处理了。那A系统如何知道B系统的处理结果?用zk就可以实现分布式系统之间的协调工作。A系统发送请求之后可以在zk上对某个节点的值注册个监听器,一旦B系统处理完了就修改zk那个节点的值,A立马就可以收到通知,完美解决。
    
    (2)分布式锁:对某一个数据连续发出两个修改操作,两台机器同时收到了请求,但是只能一台机器先执行另外一个机器再执行。那么此时就可以使用zk分布式锁,一个机器接收到了请求之后先获取zk上的一把分布式锁,就是可以去创建一个znode,接着执行操作;然后另外一个机器也尝试去创建那个znode,结果发现自己创建不了,因为被别人创建了。。。。那只能等着,等第一个机器执行完了自己再执行。
    
    (3)元数据/配置信息管理:zk可以用作很多系统的配置信息的管理,比如kafka、storm等等很多分布式系统都会选用zk来做一些元数据、配置信息的管理,包括dubbo注册中心不也支持zk么
    
    (4)HA高可用性:这个应该是很常见的,比如hadoop、hdfs、yarn等很多大数据系统,都选择基于zk来开发HA高可用机制,就是一个重要进程一般会做主备两个,主进程挂了立马通过zk感知到切换到备用进程
    

12.redis与ZK分布式锁对比

  • redis分布式锁

    Java分布式相关知识-4.分布式系统Dubbo_第12张图片
    01_redis最普通的分布式锁的实现原理.png
    官方叫做RedLock算法,是redis官方支持的分布式锁算法。
    
    这个分布式锁有3个重要的考量点,互斥(只能有一个客户端获取锁),不能死锁,容错(大部分redis节点或者这个锁就可以加可以释放)
    
    第一个最普通的实现方式,如果就是在redis里创建一个key算加锁
    
    SET my:lock 随机值 NX PX 30000,这个命令就ok,这个的NX的意思就是只有key不存在的时候才会设置成功,PX 30000的意思是30秒后锁自动释放。别人创建的时候如果发现已经有了就不能加锁了。
    
    释放锁就是删除key,但是一般可以用lua脚本删除,判断value一样才删除:
    
    关于redis如何执行lua脚本,自行百度
    
    if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
    else
        return 0
    end
    
    为啥要用随机值呢?因为如果某个客户端获取到了锁,但是阻塞了很长时间才执行完,此时可能已经自动释放锁了,此时可能别的客户端已经获取到了这个锁,要是你这个时候直接删除key的话会有问题,所以得用随机值加上面的lua脚本来释放锁。
    
    但是这样是肯定不行的。因为如果是普通的redis单实例,那就是单点故障。或者是redis普通主从,那redis主从异步复制,如果主节点挂了,key还没同步到从节点,此时从节点切换为主节点,别人就会拿到锁。
    
  • RedLock算法

    Java分布式相关知识-4.分布式系统Dubbo_第13张图片
    02_RedLock算法.png
    这个场景是假设有一个redis cluster,有5个redis master实例。然后执行如下步骤获取一把锁:
    
    1)获取当前时间戳,单位是毫秒
    2)跟上面类似,轮流尝试在每个master节点上创建锁,过期时间较短,一般就几十毫秒
    3)尝试在大多数节点上建立一个锁,比如5个节点就要求是3个节点(n / 2 +1)
    4)客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了
    5)要是锁建立失败了,那么就依次删除这个锁
    6)只要别人建立了一把分布式锁,你就得不断轮询去尝试获取锁
    
  • zk分布式锁

    Java分布式相关知识-4.分布式系统Dubbo_第14张图片
    03_zookeeper的分布式锁原理.png
    zk分布式锁,其实可以做的比较简单,就是某个节点尝试创建临时znode,此时创建成功了就获取了这个锁;这个时候别的客户端来创建锁会失败,只能注册个监听器监听这个锁。释放锁就是删除这个znode,一旦释放掉就会通知客户端,然后有一个等待着的客户端就可以再次重新枷锁。
    

13.分布式会话

  • 分布式会话

    Java分布式相关知识-4.分布式系统Dubbo_第15张图片
    01_分布式会话是什么.png
    session是啥?浏览器有个cookie,在一段时间内这个cookie都存在,然后每次发请求过来都带上一个特殊的jsessionid cookie,就根据这个东西,在服务端可以维护一个对应的session域,里面可以放点儿数据。
    
    一般只要你没关掉浏览器,cookie还在,那么对应的那个session就在,但是cookie没了,session就没了。常见于什么购物车之类的东西,还有登录状态保存之类的。
    
    这个不多说了,懂java的都该知道这个。
    
    但是你单块系统的时候这么玩儿session没问题啊,但是你要是分布式系统了呢,那么多的服务,session状态在哪儿维护啊?
    
  • tomcat + redis

    这个其实还挺方便的,就是使用session的代码跟以前一样,还是基于tomcat原生的session支持即可,然后就是用一个叫做Tomcat RedisSessionManager的东西,让所有我们部署的tomcat都将session数据存储到redis即可。
    
  • spring session + redis

    分布式会话的这个东西重耦合在tomcat中,如果我要将web容器迁移成jetty,难道你重新把jetty都配置一遍吗?
    
    因为上面那种tomcat + redis的方式好用,但是会严重依赖于web容器,不好将代码移植到其他web容器上去,尤其是你要是换了技术栈咋整?比如换成了spring cloud或者是spring boot之类的。还得好好思忖思忖。
    
    所以现在比较好的还是基于java一站式解决方案,spring了。人家spring基本上包掉了大部分的我们需要使用的框架了,spirng cloud做微服务了,spring boot做脚手架了,所以用sping session是一个很好的选择。
    

14.分布式事务

  • 单系统事务
    Java分布式相关知识-4.分布式系统Dubbo_第16张图片
    01_单块系统里的事务.png
  • 分布式系统事务
    Java分布式相关知识-4.分布式系统Dubbo_第17张图片
    02_分布式系统里的事务.png
  • 两阶段提交方案/XA方案
    Java分布式相关知识-4.分布式系统Dubbo_第18张图片
    03_两阶段提交方案.png
    也叫做两阶段提交事务方案,这个举个例子,比如说咱们公司里经常tb是吧(就是团建),然后一般会有个tb主席(就是负责组织团建的那个人)。
    
    tb,team building,团建
    
    第一个阶段,一般tb主席会提前一周问一下团队里的每个人,说,大家伙,下周六我们去滑雪+烧烤,去吗?这个时候tb主席开始等待每个人的回答,如果所有人都说ok,那么就可以决定一起去这次tb。如果这个阶段里,任何一个人回答说,我有事不去了,那么tb主席就会取消这次活动。
    
    第二个阶段,那下周六大家就一起去滑雪+烧烤了
    
    所以这个就是所谓的XA事务,两阶段提交,有一个事务管理器的概念,负责协调多个数据库(资源管理器)的事务,事务管理器先问问各个数据库你准备好了吗?如果每个数据库都回复ok,那么就正式提交事务,在各个数据库上执行操作;如果任何一个数据库回答不ok,那么就回滚事务。
    
    这种分布式事务方案,比较适合单块应用里,跨多个库的分布式事务,而且因为严重依赖于数据库层面来搞定复杂的事务,效率很低,绝对不适合高并发的场景。如果要玩儿,那么基于spring + JTA就可以搞定,自己随便搜个demo看看就知道了。
    
    这个方案,我们很少用,一般来说某个系统内部如果出现跨多个库的这么一个操作,是不合规的。我可以给大家介绍一下, 现在微服务,一个大的系统分成几百个服务,几十个服务。一般来说,我们的规定和规范,是要求说每个服务只能操作自己对应的一个数据库。
    
    如果你要操作别的服务对应的库,不允许直连别的服务的库,违反微服务架构的规范,你随便交叉胡乱访问,几百个服务的话,全体乱套,这样的一套服务是没法管理的,没法治理的,经常数据被别人改错,自己的库被别人写挂。
    
    如果你要操作别人的服务的库,你必须是通过调用别的服务的接口来实现,绝对不允许你交叉访问别人的数据库!
    
  • TCC方案
    Java分布式相关知识-4.分布式系统Dubbo_第19张图片
    04_TCC方案.png
    TCC的全程是:Try、Confirm、Cancel。
    
    这个其实是用到了补偿的概念,分为了三个阶段:
    
    1)Try阶段:这个阶段说的是对各个服务的资源做检测以及对资源进行锁定或者预留
    2)Confirm阶段:这个阶段说的是在各个服务中执行实际的操作
    3)Cancel阶段:如果任何一个服务的业务方法执行出错,那么这里就需要进行补偿,就是执行已经执行成功的业务逻辑的回滚操作
    
    给大家举个例子吧,比如说跨银行转账的时候,要涉及到两个银行的分布式事务,如果用TCC方案来实现,思路是这样的:
    
    1)Try阶段:先把两个银行账户中的资金给它冻结住就不让操作了
    2)Confirm阶段:执行实际的转账操作,A银行账户的资金扣减,B银行账户的资金增加
    3)Cancel阶段:如果任何一个银行的操作执行失败,那么就需要回滚进行补偿,就是比如A银行账户如果已经扣减了,但是B银行账户资金增加失败了,那么就得把A银行账户资金给加回去
    
    这种方案说实话几乎很少用人使用,我们用的也比较少,但是也有使用的场景。因为这个事务回滚实际上是严重依赖于你自己写代码来回滚和补偿了,会造成补偿代码巨大,非常之恶心。
    
    比如说我们,一般来说跟钱相关的,跟钱打交道的,支付、交易相关的场景,我们会用TCC,严格严格保证分布式事务要么全部成功,要么全部自动回滚,严格保证资金的正确性,在资金上出现问题
    
    比较适合的场景:这个就是除非你是真的一致性要求太高,是你系统中核心之核心的场景,比如常见的就是资金类的场景,那你可以用TCC方案了,自己编写大量的业务逻辑,自己判断一个事务中的各个环节是否ok,不ok就执行补偿/回滚代码。
    
    而且最好是你的各个业务执行的时间都比较短。
    
    但是说实话,一般尽量别这么搞,自己手写回滚逻辑,或者是补偿逻辑,实在太恶心了,那个业务代码很难维护。
    
  • 本地消息表
    Java分布式相关知识-4.分布式系统Dubbo_第20张图片
    05_本地消息表方案.png
    国外的ebay搞出来的这么一套思想
    
    这个大概意思是这样的
    
    1)A系统在自己本地一个事务里操作同时,插入一条数据到消息表
    2)接着A系统将这个消息发送到MQ中去
    3)B系统接收到消息之后,在一个事务里,往自己本地消息表里插入一条数据,同时执行其他的业务操作,如果这个消息已经被处理过了,那么此时这个事务会回滚,这样保证不会重复处理消息
    4)B系统执行成功之后,就会更新自己本地消息表的状态以及A系统消息表的状态
    5)如果B系统处理失败了,那么就不会更新消息表状态,那么此时A系统会定时扫描自己的消息表,如果有没处理的消息,会再次发送到MQ中去,让B再次处理
    6)这个方案保证了最终一致性,哪怕B事务失败了,但是A会不断重发消息,直到B那边成功为止
    
    这个方案说实话最大的问题就在于严重依赖于数据库的消息表来管理事务啥的???这个会导致如果是高并发场景咋办呢?咋扩展呢?所以一般确实很少用
    
  • 可靠消息最终一致性方案
    Java分布式相关知识-4.分布式系统Dubbo_第21张图片
    06_可靠消息最终一致性方案.png
    这个的意思,就是干脆不要用本地的消息表了,直接基于MQ来实现事务。比如阿里的RocketMQ就支持消息事务。
    
    大概的意思就是:
    1)A系统先发送一个prepared消息到mq,如果这个prepared消息发送失败那么就直接取消操作别执行了
    2)如果这个消息发送成功过了,那么接着执行本地事务,如果成功就告诉mq发送确认消息,如果失败就告诉mq回滚消息
    3)如果发送了确认消息,那么此时B系统会接收到确认消息,然后执行本地的事务
    4)mq会自动定时轮询所有prepared消息回调你的接口,问你,这个消息是不是本地事务处理失败了,所有没发送确认消息?那是继续重试还是回滚?一般来说这里你就可以查下数据库看之前本地事务是否执行,如果回滚了,那么这里也回滚吧。这个就是避免可能本地事务执行成功了,别确认消息发送失败了。
    5)这个方案里,要是系统B的事务失败了咋办?重试咯,自动不断重试直到成功,如果实在是不行,要么就是针对重要的资金类业务进行回滚,比如B系统本地回滚后,想办法通知系统A也回滚;或者是发送报警由人工来手工回滚和补偿
    
    这个还是比较合适的,目前国内互联网公司大都是这么玩儿的,要不你举用RocketMQ支持的,要不你就自己基于类似ActiveMQ?RabbitMQ?自己封装一套类似的逻辑出来,总之思路就是这样子的
    
  • 最大努力通知方案
    Java分布式相关知识-4.分布式系统Dubbo_第22张图片
    07_最大努力通知方案.png
    这个方案的大致意思就是:
    
    1)系统A本地事务执行完之后,发送个消息到MQ
    2)这里会有个专门消费MQ的最大努力通知服务,这个服务会消费MQ然后写入数据库中记录下来,或者是放入个内存队列也可以,接着调用系统B的接口
    3)要是系统B执行成功就ok了;要是系统B执行失败了,那么最大努力通知服务就定时尝试重新调用系统B,反复N次,最后还是不行就放弃
    

15.高并发系统架构组成

Java分布式相关知识-4.分布式系统Dubbo_第23张图片
01_高并发系统的架构组成.png

你可能感兴趣的:(Java分布式相关知识-4.分布式系统Dubbo)