分布式系统常见问题

/   前言   /

目录

  • 为什么要进行系统拆分

  • 如果进行系统拆分

  • 分布式服务框架

    • Dubbo工作原理

    • Dubbo支持的序列化协议

    • Hessian数据结构

    • 几个常用的CAP框架对比

  • 分布式锁

    • Redis分布式锁

    • zk分布式锁

  • 分布式事务

    • 两阶段提交方案/XA方案

  • 分布式会话

/   正文   /

为什么要进行系统拆分?

先想象一下不拆分是什么样子:

  • 要是不拆分,一个大系统几十万行代码,一堆人维护同一个项目、一份代码,改一处就要整体重新上线,代码也得重新测试,上线发布都是几十万行代码的系统一起发布,可能每次上线都要做很多的检查,风险性极高。

  • 多分支、多版本开发也是问题,接口版本不兼容,业务冲突、代码冲突和合并要处理,非常耗费时间;

  • 技术升级几乎无法升级,一旦升级,可能很多地方出问题,排查。

  • 开发效率极其低下,历史遗留问题很多,先行者到处埋坑,后来者要承受巨大的技术债务

    分布式系统常见问题_第1张图片

拆分了以后:

  • 系统耦合度降低,系统性风险降低由多个服务组成,开发、部署、测试互相之间不影响,测试单系统代码就可以,每次发布单系统服务,一切以微服务接口、mq等关联。

  • 多工程,多 git 代码仓库,开发人员各自维护自己的服务,减少版本冲突、代码冲突了。

  • 技术升级负债低,不影响其他系统,保持接口不变就可以了,各业务技术栈可百花齐放

  • 服务扩容也很简单,新业务新增服务即可

    分布式系统常见问题_第2张图片

也要提醒的一点是,系统拆分成分布式系统之后,大量的分布式系统面临的问题也是接踵而来,所以后面的问题都是在围绕分布式系统带来的复杂技术挑战在说。

如何进行系统拆分?

系统拆分为分布式系统,拆成多个服务,拆成微服务的架构,是需要拆很多轮的。并不是说上来一个架构师一次就给拆好了,而以后都不用拆。

第一轮;团队继续扩大,拆好的某个服务,刚开始是 1 个人维护 1 万行代码,后来业务系统越来越复杂,这个服务是 10 万行代码,5 个人;第二轮,1 个服务 -> 5 个服务,每个服务 2 万行代码,每人负责一个服务。

如果是多人维护一个服务,最理想的情况下,几十个人,1 个人负责 1 个或 2~3 个服务;某个服务工作量变大了,代码量越来越多,某个同学,负责一个服务,代码量变成了 10 万行了,他自己不堪重负,他现在一个人拆开,5 个服务,1 个人顶着,负责 5 个人,接着招人,2 个人,给那个同学带着,3 个人负责 5 个服务,其中 2 个人每个人负责 2 个服务,1 个人负责 1 个服务。

个人建议,一个服务的代码不要太多,1 万行左右,两三万撑死了吧。

大部分的系统,是要进行多轮拆分的,第一次拆分,可能就是将以前的多个模块该拆分开来了,比如说将电商系统拆分成订单系统、商品系统、采购系统、仓储系统、用户系统等。

但是后面可能每个系统又变得越来越复杂了,比如说采购系统里面又分成了供应商管理系统、采购单管理系统,订单系统又拆分成了购物车系统、价格系统、订单管理系统。

核心意思就是根据情况,先拆分一轮,后面如果系统更复杂了,可以继续分拆。你根据自己负责系统的实际情况来考虑。


分布式服务框架

什么是微服务

通过将功能分解到各个离散的服务中以实现对解决方案的解耦

Dubbo工作原理

dubbo 工作原理

  • 第一层:service 层,接口层,给服务提供者和消费者来实现的

  • 第二层:config 层,配置层,主要是对 dubbo 进行各种配置的

  • 第三层:proxy 层,服务代理层,无论是 consumer 还是 provider,dubbo 都会给你生成代理,代理之间进行网络通信

  • 第四层:registry 层,服务注册层,负责服务的注册与发现

  • 第五层:cluster 层,集群层,封装多个服务提供者的路由以及负载均衡,将多个实例组合成一个服务

  • 第六层:monitor 层,监控层,对 rpc 接口的调用次数和调用时间进行监控

  • 第七层:protocal 层,远程调用层,封装 rpc 调用

  • 第八层:exchange 层,信息交换层,封装请求响应模式,同步转异步

  • 第九层:transport 层,网络传输层,抽象 mina 和 netty 为统一接口

  • 第十层:serialize 层,数据序列化层

工作流程

  • 第一步:provider 向注册中心去注册

  • 第二步:consumer 从注册中心订阅服务,注册中心会通知 consumer 注册好的服务

  • 第三步:consumer 调用 provider

  • 第四步:consumer 和 provider 都异步通知监控中心

注册中心挂了可以继续通信吗?注册中心挂了可以继续通信吗?

  • 可以,因为刚开始初始化的时候,消费者会将提供者的地址等信息拉取到本地缓存,所以注册中心挂了可以继续通信。

Dubbo 支持的序列化协议

序列化,就是把数据结构或者是一些对象,转换为二进制串的过程,而反序列化是将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程。

dubbo 支持 hession、Java 二进制序列化、json、SOAP 文本序列化多种序列化协议。但是 hessian 是其默认的序列化协议。

  • dubbo 协议

    • 默认就是走 dubbo 协议,单一长连接,进行的是 NIO 异步通信,基于 hessian 作为序列化协议。使用的场景是:传输数据量小(每次请求在 100kb 以内),但是并发量很高,以及服务消费者机器数远大于服务提供者机器数的情况。

    • 为了要支持高并发场景,一般是服务提供者就几台机器,但是服务消费者有上百台,可能每天调用量达到上亿次!此时用长连接是最合适的,就是跟每个服务消费者维持一个长连接就可以,可能总共就 100 个连接。然后后面直接基于长连接 NIO 异步通信,可以支撑高并发请求。

    • 长连接,通俗点说,就是建立连接过后可以持续发送请求,无须再建立连接。

  • rmi 协议

    • RMI 协议采用 JDK 标准的 java.rmi.* 实现,采用阻塞式短连接和 JDK 标准序列化方式。多个短连接,适合消费者和提供者数量差不多的情况,适用于文件的传输,一般较少用。

  • hessian 协议

    • Hessian 1 协议用于集成 Hessian 的服务,Hessian 底层采用 Http 通讯,采用 Servlet 暴露服务,Dubbo 缺省内嵌 Jetty 作为服务器实现。走 hessian 序列化协议,多个短连接,适用于提供者数量比消费者数量还多的情况,适用于文件的传输,一般较少用。

  • http 协议

    • 基于 HTTP 表单的远程调用协议,采用 Spring 的 HttpInvoker 实现。走表单序列化。

  • thrift 协议

    • 当前 dubbo 支持的 thrift 协议是对 thrift 原生协议的扩展,在原生协议的基础上添加了一些额外的头信息,比如 service name,magic number 等。

  • webservice

    • 基于 WebService 的远程调用协议,基于 Apache CXF 的 frontend-simple 和 transports-http 实现。走 SOAP 文本序列化。

  • memcached 协议

    • 基于 memcached 实现的 RPC 协议。

  • redis 协议

    • 基于 Redis 实现的 RPC 协议。

  • rest 协议

    • 基于标准的 Java REST API——JAX-RS 2.0(Java API for RESTful Web Services 的简写)实现的 REST 调用支持。

  • gPRC 协议

    • Dubbo 自 2.7.5 版本开始支持 gRPC 协议,对于计划使用 HTTP/2 通信,或者想利用 gRPC 带来的 Stream、反压、Reactive 编程等能力的开发者来说, 都可以考虑启用 gRPC 协议。

Hessian 的数据结构

Hessian 的对象序列化机制有 8 种原始类型:

  • 原始二进制数据

  • boolean

  • 64-bit date(64 位毫秒值的日期)

  • 64-bit double

  • 32-bit int

  • 64-bit long

  • null

  • UTF-8 编码的 string

另外还包括 3 种递归类型:

  • list for lists and arrays

  • map for maps and dictionaries

  • object for objects

还有一种特殊的类型:

  • ref:用来表示对共享对象的引用。

Protocol Buffer为什么效率高

常见数据存储格式JSON or XML,对于 Protocol Buffer 还比较陌生。Protocol Buffer 其实是 Google 出品的一种轻量并且高效的结构化数据存储格式,性能比 JSON、XML 要高很多。

其实 PB 之所以性能如此好,主要得益于两个:第一,它使用 proto 编译器,自动进行序列化和反序列化,速度非常快,应该比 XML 和 JSON 快上了 20~100 倍;第二,它的数据压缩效果好,就是说它序列化后的数据量体积小。因为体积小,传输起来带宽和速度上会有优化。

dubbo与http区别

  • Dubbo 接口:

    • Dubbo 接口是阿里巴巴开源的致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案。

    • dubbo框架告别了传统的web service的服务模式,进而改用provider和consumer模式进行服务。

  • 为什么是高性能的呢?

    • 可以在某个服务器集群中提供单一专注的服务,这样不与其他服务混杂,同时dubbo接口有SOA调度通过监控每台服务器而实现负载均衡。

    • consumer端无需关注provider端如何实现,只需在注册中心订阅即可到相应服务器请求服务,这样就实现了高性能和透明化。

    • 说到底,Dubbo接口就是一个分布式服务框架。

  • 为什么要用Dubbo 接口:

    • 互联网应用及用户数据规模变大,传统的垂直架构满足不了需求,因此急需分布式服务架构以及流动计算架构。

区别:

  1. 协议层区别

    • HTTP ,HTTPS 使用的是 应用层协议 应用层协议:定义了用于在网络中进行通信和传输数据的接口

    • DUBBO接口使用的是 TCP/IP是传输层协议 传输层协议:管理着网络中的端到端的数据传输;因此要比 HTTP协议快

  2. socket 层区别

    • dubbo默认使用socket长连接,即首次访问建立连接以后,后续网络请求使用相同的网络通道

    • http1.1协议默认使用短连接,每次请求均需要进行三次握手,而http2.0协议开始将默认socket连接改为了长连接(keep-alive)

rpc与http区别

  • rpc是一种网络请求通信方式

  • http是应用层协议

  • 常见Controller层接口基本是基于http协议的rpc通信

  • socket API是基于TCP协议的rpc通信

附:RMI其实就是一种RPC的实现, 很早在jdk1.1实现。RMI的通信方式是把Java对象序列化为二进制格式,接收方收到以后再进行反序列化,局限java编程。

Dubbo 负载均衡策略和高可用策略都有哪些?动态代理策略呢?

  • dubbo 工作原理:服务注册、注册中心、消费者、代理通信、负载均衡;

  • 网络通信、序列化:dubbo 协议、长连接、NIO、hessian 序列化协议;

  • 负载均衡策略、集群容错策略、动态代理策略

  • dubbo SPI 机制:你了解不了解 dubbo 的 SPI 机制?如何基于 SPI 机制对 dubbo 进行扩展?

dubbo 负载均衡策略

  • RandomLoadBalance :默认情况下,dubbo 是 RandomLoadBalance ,即随机调用实现负载均衡,可以对 provider 不同实例设置不同的权重,会按照权重来负载均衡,权重越大分配流量越高,一般就用这个默认的就可以了。

  • RoundRobinLoadBalance :这个的话默认就是均匀地将流量打到各个机器上去,但是如果各个机器的性能不一样,容易导致性能差的机器负载过高。所以此时需要调整权重,让性能差的机器承载权重小一些,流量少一些。

  • LeastActiveLoadBalance :官网对 LeastActiveLoadBalance 的解释是“最小活跃数负载均衡”,活跃调用数越小,表明该服务提供者效率越高,单位时间内可处理更多的请求,那么此时请求会优先分配给该服务提供者。

  • ConsistentHashLoadBalance :一致性 Hash 算法,相同参数的请求一定分发到一个 provider 上去,provider 挂掉的时候,会基于虚拟节点均匀分配剩余的流量,抖动不会太大。如果你需要的不是随机负载均衡,是要一类请求都到一个节点,那就走这个一致性 Hash 策略。

dubbo 集群容错策略

  • Failover Cluster 模式 :失败自动切换,自动重试其他机器,默认就是这个,常见于读操作。(失败重试其它机器)

  • Failfast Cluster 模式:一次调用失败就立即失败,常见于非幂等性的写操作,比如新增一条记录(调用失败就立即失败)

  • Failsafe Cluster 模式 :出现异常时忽略掉,常用于不重要的接口调用,比如记录日志。

  • Failback Cluster 模式 :失败了后台自动记录请求,然后定时重发,比较适合于写消息队列这种。

  • Forking Cluster 模式:并行调用多个 provider,只要一个成功就立即返回。常用于实时性要求比较高的读操作,但是会浪费更多的服务资源,可通过 forks="2" 来设置最大并行数。

  • Broadcast Cluster 模式 :逐个调用所有的 provider。任何一个 provider 出错则报错(从 2.1.0 版本开始支持)。通常用于通知所有提供者更新缓存或日志等本地资源信息。

  • dubbo 动态代理策略 :默认使用 javassist 动态字节码生成,创建代理类。但是可以通过 spi 扩展机制配置自己的动态代理策略。

Dubbo 的 SPI 思想是什么?

spi,简单来说,就是 service provider interface ,说白了是什么意思呢,比如你有个接口,现在这个接口有 3 个实现类,那么在系统运行的时候对这个接口到底选择哪个实现类呢?这就需要 spi 了,需要根据指定的配置或者是默认的配置,去找到对应的实现类加载进来,然后用这个实现类的实例对象。

举个栗子。

你有一个接口 A。A1/A2/A3 分别是接口 A 的不同实现。你通过配置 接口 A = 实现 A2 ,那么在系统实际运行的时候,会加载你的配置,用实现 A2 实例化一个对象来提供服务。

spi 机制一般用在哪儿?插件扩展的场景,比如说你开发了一个给别人使用的开源框架,如果你想让别人自己写个插件,插到你的开源框架里面,从而扩展某个功能,这个时候 spi 思想就用上了。

Java spi 思想的体现 spi 经典的思想体现,大家平时都在用,比如说 jdbc。Java 定义了一套 jdbc 的接口,但是 Java 并没有提供 jdbc 的实现类。

但是实际上项目跑的时候,要使用 jdbc 接口的哪些实现类呢?

一般来说,我们要根据自己使用的数据库,比如 mysql,你就将 mysql-jdbc-connector.jar 引入进来;oracle,你就将 oracle-jdbc-connector.jar 引入进来。在系统跑的时候,碰到你使用 jdbc 的接口,他会在底层使用你引入的那个 jar 中提供的实现类。

dubbo 的 spi 思想 

dubbo 也用了 spi 思想,不过没有用 jdk 的 spi 机制,是自己实现的一套 spi 机制。

Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

Protocol 接口,在系统运行的时候,,dubbo 会判断一下应该选用这个 Protocol 接口的哪个实现类来实例化对象来使用。它会去找一个你配置的 Protocol,将你配置的 Protocol 实现类,加载到 jvm 中来,然后实例化对象,就用你的那个 Protocol 实现类就可以了。

上面那行代码就是 dubbo 里大量使用的,就是对很多组件,都是保留一个接口和多个实现,然后在系统运行的时候动态根据配置去找到对应的实现类。如果你没配置,那就走默认的实现好了,没问题。

如何基于 Dubbo 进行服务治理、服务降级、失败重试以及超时重试?

服务降级,这个是涉及到复杂分布式系统中必备的一个话题,因为分布式系统互相来回调用,任何一个系统故障了,你不降级,直接就全盘崩溃?那就太坑爹了吧。

失败重试,分布式系统中网络请求如此频繁,要是因为网络问题不小心失败了一次,是不是要重试?

超时重试,跟上面一样,如果不小心网络慢一点,超时了,如何重试?

分布式服务接口的幂等性如何设计(比如不能重复扣款)?

这个没有通用的一个方法,这个应该结合业务来保证幂等性。所谓幂等性,就是说一个接口,多次发起同一个请求,你这个接口得保证结果是准确的,比如不能多扣款、不能多插入一条数据、不能将统计值多加了 1。这就是幂等性。

其实保证幂等性主要是三点:

  • 对于每个请求必须有一个唯一的标识,举个栗子:订单支付请求,肯定得包含订单 id,一个订单 id 最多支付一次,对吧。

  • 每次处理完请求之后,必须有一个记录标识这个请求处理过了。常见的方案是在 mysql 中记录个状态啥的,比如支付之前记录一条这个订单的支付流水。

  • 每次接收请求需要进行判断,判断之前是否处理过。比如说,如果有一个订单已经支付了,就已经有了一条支付流水,那么如果重复发送这个请求,则此时先插入支付流水,orderId 已经存在了,唯一键约束生效,报错插入不进去的。然后你就不用再扣款了。

实际运作过程中,你要结合自己的业务来,比如说利用 Redis,用 orderId 作为唯一键。只有成功插入这个支付流水,才可以执行实际的支付扣款。

要求是支付一个订单,必须插入一条支付流水,order_id 建一个唯一键 unique key 。你在支付一个订单之前,先插入一条支付流水,order_id 就已经进去了。你就可以写一个标识到 Redis 里面去, set order_id payed ,下一次重复请求过来了,先查 Redis 的 order_id 对应的 value,如果是 payed 就说明已经支付过了,你就别重复支付了。

分布式服务接口请求的顺序性如何保证?

一般来说,个人建议是,你们从业务逻辑上设计的这个系统最好是不需要这种顺序性的保证,因为一旦引入顺序性保障,比如使用分布式锁,会导致系统复杂度上升,而且会带来效率低下,热点数据压力过大等问题。

简单来说,首先你得用 Dubbo 的一致性 hash 负载均衡策略,将比如某一个订单 id 对应的请求都给分发到某个机器上去,接着就是在那个机器上,因为可能还是多线程并发执行的,你可能得立即将某个订单 id 对应的请求扔一个内存队列里去,强制排队,这样来确保他们的顺序性。

但是这样引发的后续问题就很多,比如说要是某个订单对应的请求特别多,造成某台机器成热点怎么办?解决这些问题又要开启后续一连串的复杂技术方案...... 曾经这类问题弄的我们头疼不已,所以,还是建议什么呢?

最好是比如说刚才那种,一个订单的插入和删除操作,能不能合并成一个操作,就是一个删除,或者是其它什么,避免这种问题的产生。

如何自己设计一个类似 Dubbo 的 RPC 框架?

最基本的结构:

  • 注册中心,得有个注册中心,保留各个服务的信息,可以用 zookeeper 来做,对吧。

  • 然后你的消费者需要去注册中心拿对应的服务信息吧,对吧,而且每个服务可能会存在于多台机器上。

  • 接着你就该发起一次请求了,咋发起?当然是基于动态代理了,你面向接口获取到一个动态代理,这个动态代理就是接口在本地的一个代理,然后这个代理会找到服务对应的机器地址。

  • 然后找哪个机器发送请求?那肯定得有个负载均衡算法了,比如最简单的可以随机轮询是不是。

  • 接着找到一台机器,就可以跟它发送请求了,第一个问题咋发送?你可以说用 netty 了,nio 方式;第二个问题发送啥格式数据?你可以说用 hessian 序列化协议了,或者是别的,对吧。然后请求过去了。

  • 服务器那边一样的,需要针对你自己的服务生成一个动态代理,监听某个网络端口了,然后代理你本地的服务代码。接收到请求的时候,就调用对应的服务代码,对吧。

CAP 定理

在理论计算机科学中,CAP 定理(CAP theorem),又被称作布鲁尔定理(Brewer's theorem),它指出对于一个分布式计算系统来说,不可能同时满足以下三点:

  • 一致性(Consistency) (等同于所有节点访问同一份最新的数据副本)

  • 可用性(Availability)(每次请求都能获取到非错的响应——但是不保证获取的数据为最新数据)

  • 分区容错性(Partition tolerance)(以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在 C 和 A 之间做出选择。)

几个常用的CAP框架对比

框架 所属
Eureka AP
Zookeeper CP
Consul CP

Eureka

Eureka 保证了可用性,实现最终一致性。

Eureka 所有节点都是平等的所有数据都是相同的,且 Eureka 可以相互交叉注册。


Eureka client 使用内置轮询负载均衡器去注册,有一个检测间隔时间,如果在一定时间内没有收到心跳,才会移除该节点注册信息;如果客户端发现当前 Eureka 不可用,会切换到其他的节点,如果所有的 Eureka 都跪了,Eureka client 会使用最后一次数据作为本地缓存;所以以上的每种设计都是他不具备一致性的特性。

注意:因为 EurekaAP 的特性和请求间隔同步机制,在服务更新时候一般会手动通过 Eureka 的 api 把当前服务状态设置为offline,并等待 2 个同步间隔后重新启动,这样就能保证服务更新节点对整体系统的影响

Zookeeper

强一致性

Zookeeper 在选举 leader 时会停止服务,只有成功选举 leader 成功后才能提供服务,选举时间较长;

内部使用 paxos 选举投票机制,只有获取半数以上的投票才能成为 leader,否则重新投票,所以部署的时候最好集群节点不小于 3 的奇数个(但是谁能保证跪掉后节点也是基数个呢);

Zookeeper 健康检查一般是使用 tcp 长链接,在内部网络抖动时或者对应节点阻塞时候都会变成不可用,这里还是比较危险的;

Consul

和 Zookeeper 一样数据 CP

Consul 注册时候只有过半的节点都写入成功才认为注册成功;leader 挂掉时,重新选举期间整个 Consul 不可用,保证了强一致性但牺牲了可用性
有很多 blog 说 Consul 属于 ap,官方已经确认他为 CP 机制


分布式锁

  • 使用 Redis 如何设计分布式锁?使用 zk 来设计分布式锁可以吗?这两种分布式锁的实现方式哪种效率比较高?

Redis分布式锁

官方叫做 RedLock 算法,是 Redis 官方支持的分布式锁算法。

这个分布式锁有 3 个重要的考量点:

  • 互斥(只能有一个客户端获取锁)

  • 不能死锁

  • 容错(只要大部分 Redis 节点创建了这把锁就可以)

Redis最普通的分布式锁

第一个最普通的实现方式,就是在 Redis 里使用 SET key value [EX seconds] [PX milliseconds] NX 创建一个 key,这样就算加锁。其中:

  • NX:表示只有 key 不存在的时候才会设置成功,如果此时 redis 中存在这个 key,那么设置失败,返回 nil

  • EX seconds:设置 key 的过期时间,精确到秒级。意思是 seconds 秒后锁自动释放,别人创建的时候如果发现已经有了就不能加锁了。

  • PX milliseconds:同样是设置 key 的过期时间,精确到毫秒级。

比如执行以下命令:

SET resource_name my_random_value PX 30000 NX

释放锁就是删除 key ,但是一般可以用 lua 脚本删除,判断 value 一样才删除

为啥要用 random_value 随机值呢?因为如果某个客户端获取到了锁,但是阻塞了很长时间才执行完,比如说超过了 30s,此时可能已经自动释放锁了,此时可能别的客户端已经获取到了这个锁,要是你这个时候直接删除 key 的话会有问题,所以得用随机值加上面的 lua 脚本来释放锁。

但是这样是肯定不行的。因为如果是普通的 Redis 单实例,那就是单点故障。或者是 Redis 普通主从,那 Redis 主从异步复制,如果主节点挂了(key 就没有了),key 还没同步到从节点,此时从节点切换为主节点,别人就可以 set key,从而拿到锁。

RedLock 算法

这个场景是假设有一个 Redis cluster,有 5 个 Redis master 实例。然后执行如下步骤获取一把锁:

  1. 获取当前时间戳,单位是毫秒;

  2. 跟上面类似,轮流尝试在每个 master 节点上创建锁,超时时间较短,一般就几十毫秒(客户端为了获取锁而使用的超时时间比自动释放锁的总时间要小。例如,如果自动释放时间是 10 秒,那么超时时间可能在 5~50 毫秒范围内);

  3. 尝试在大多数节点上建立一个锁,比如 5 个节点就要求是 3 个节点 n / 2 + 1 ;

  4. 客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了;

  5. 要是锁建立失败了,那么就依次之前建立过的锁删除;

  6. 只要别人建立了一把分布式锁,你就得不断轮询去尝试获取锁

zk分布式锁

zk 分布式锁,其实可以做的比较简单,就是某个节点尝试创建临时 znode,此时创建成功了就获取了这个锁;这个时候别的客户端来创建锁会失败,只能注册个监听器监听这个锁。释放锁就是删除这个 znode,一旦释放掉就会通知客户端,然后有一个等待着的客户端就可以再次重新加锁。

redis 分布式锁和 zk 分布式锁的对比

  • redis 分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能。

  • zk 分布式锁,获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较小。

另外一点就是,如果是 Redis 获取锁的那个客户端 出现 bug 挂了,那么只能等待超时时间之后才能释放锁;而 zk 的话,因为创建的是临时 znode,只要客户端挂了,znode 就没了,此时就自动释放锁。

Redis 分布式锁大家没发现好麻烦吗?遍历上锁,计算时间等等......zk 的分布式锁语义清晰实现简单。

所以先不分析太多的东西,就说这两点,我个人实践认为 zk 的分布式锁比 Redis 的分布式锁牢靠、而且模型简单易用。


分布式事务

  • 分布式事务了解吗?你们如何解决分布式事务问题的?TCC 如果出现网络连不通怎么办?XA 的一致性如何保证?

分布式事务的实现主要有以下 6 种方案:

  • XA 方案

  • TCC 方案

  • SAGA 方案

  • 本地消息表

  • 可靠消息最终一致性方案

  • 最大努力通知方案

两阶段提交方案/XA方案

所谓的 XA 方案,即:两阶段提交,有一个事务管理器的概念,负责协调多个数据库(资源管理器)的事务,事务管理器先问问各个数据库你准备好了吗?如果每个数据库都回复 ok,那么就正式提交事务,在各个数据库上执行操作;如果任何其中一个数据库回答不 ok,那么就回滚事务。

这种分布式事务方案,比较适合单块应用里,跨多个库的分布式事务,而且因为严重依赖于数据库层面来搞定复杂的事务,效率很低,绝对不适合高并发的场景。如果要玩儿,那么基于 Spring + JTA 就可以搞定,自己随便搜个 demo 看看就知道了。

这个方案,我们很少用,一般来说某个系统内部如果出现跨多个库的这么一个操作,是不合规的。我可以给大家介绍一下, 现在微服务,一个大的系统分成几十个甚至几百个服务。一般来说,我们的规定和规范,是要求每个服务只能操作自己对应的一个数据库

如果你要操作别的服务对应的库,不允许直连别的服务的库,违反微服务架构的规范,你随便交叉胡乱访问,几百个服务的话,全体乱套,这样的一套服务是没法管理的,没法治理的,可能会出现数据被别人改错,自己的库被别人写挂等情况。

如果你要操作别人的服务的库,你必须是通过调用别的服务的接口来实现,绝对不允许交叉访问别人的数据库。

TCC 方案

TCC 的全称是:Try 、 Confirm 、 Cancel 。

  • Try 阶段:这个阶段说的是对各个服务的资源做检测以及对资源进行锁定或者预留

  • Confirm 阶段:这个阶段说的是在各个服务中执行实际的操作

  • Cancel 阶段:如果任何一个服务的业务方法执行出错,那么这里就需要进行补偿,就是执行已经执行成功的业务逻辑的回滚操作。(把那些执行成功的回滚)

这种方案说实话几乎很少人使用,我们用的也比较少,但是也有使用的场景。因为这个事务回滚实际上是严重依赖于你自己写代码来回滚和补偿了,会造成补偿代码巨大,非常之恶心。

比如说我们,一般来说跟相关的,跟钱打交道的,支付交易相关的场景,我们会用 TCC,严格保证分布式事务要么全部成功,要么全部自动回滚,严格保证资金的正确性,保证在资金上不会出现问题。

而且最好是你的各个业务执行的时间都比较短。

但是说实话,一般尽量别这么搞,自己手写回滚逻辑,或者是补偿逻辑,实在太恶心了,那个业务代码是很难维护的。

Saga 方案

金融核心等业务可能会选择 TCC 方案,以追求强一致性和更高的并发量,而对于更多的金融核心以上的业务系统 往往会选择补偿事务,补偿事务处理在 30 多年前就提出了 Saga 理论,随着微服务的发展,近些年才逐步受到大家的关注。目前业界比较公认的是采用 Saga 作为长事务的解决方案。

本地消息表

本地消息表其实是国外的 ebay 搞出来的这么一套思想。

这个大概意思是这样的:

  1. A 系统在自己本地一个事务里操作同时,插入一条数据到消息表;

  2. 接着 A 系统将这个消息发送到 MQ 中去;

  3. B 系统接收到消息之后,在一个事务里,往自己本地消息表里插入一条数据,同时执行其他的业务操作,如果这个消息已经被处理过了,那么此时这个事务会回滚,这样保证不会重复处理消息

  4. B 系统执行成功之后,就会更新自己本地消息表的状态以及 A 系统消息表的状态;

  5. 如果 B 系统处理失败了,那么就不会更新消息表状态,那么此时 A 系统会定时扫描自己的消息表,如果有未处理的消息,会再次发送到 MQ 中去,让 B 再次处理;

  6. 这个方案保证了最终一致性,哪怕 B 事务失败了,但是 A 会不断重发消息,直到 B 那边成功为止。

这个方案说实话最大的问题就在于严重依赖于数据库的消息表来管理事务啥的,如果是高并发场景咋办呢?咋扩展呢?所以一般确实很少用。

可靠消息最终一致性方案

这个的意思,就是干脆不要用本地的消息表了,直接基于 MQ 来实现事务。比如阿里的 RocketMQ 就支持消息事务。

大概的意思就是:

  1. A 系统先发送一个 prepared 消息到 mq,如果这个 prepared 消息发送失败那么就直接取消操作别执行了;

  2. 如果这个消息发送成功过了,那么接着执行本地事务,如果成功就告诉 mq 发送确认消息,如果失败就告诉 mq 回滚消息;

  3. 如果发送了确认消息,那么此时 B 系统会接收到确认消息,然后执行本地的事务;

  4. mq 会自动定时轮询所有 prepared 消息回调你的接口,问你,这个消息是不是本地事务处理失败了,所有没发送确认的消息,是继续重试还是回滚?一般来说这里你就可以查下数据库看之前本地事务是否执行,如果回滚了,那么这里也回滚吧。这个就是避免可能本地事务执行成功了,而确认消息却发送失败了。

  5. 这个方案里,要是系统 B 的事务失败了咋办?重试咯,自动不断重试直到成功,如果实在是不行,要么就是针对重要的资金类业务进行回滚,比如 B 系统本地回滚后,想办法通知系统 A 也回滚;或者是发送报警由人工来手工回滚和补偿。

  6. 这个还是比较合适的,目前国内互联网公司大都是这么玩儿的,要不你就用 RocketMQ 支持的,要不你就自己基于类似 ActiveMQ?RabbitMQ?自己封装一套类似的逻辑出来,总之思路就是这样子的。

最大努力通知方案

这个方案的大致意思就是:

  1. 系统 A 本地事务执行完之后,发送个消息到 MQ;

  2. 这里会有个专门消费 MQ 的最大努力通知服务,这个服务会消费 MQ 然后写入数据库中记录下来,或者是放入个内存队列也可以,接着调用系统 B 的接口;

  3. 要是系统 B 执行成功就 ok 了;要是系统 B 执行失败了,那么最大努力通知服务就定时尝试重新调用系统 B,反复 N 次,最后还是不行就放弃。


分布式会话

  • 集群部署时的分布式 Session 如何实现?

Session 是啥?浏览器有个 Cookie,在一段时间内这个 Cookie 都存在,然后每次发请求过来都带上一个特殊的 jsessionid cookie ,就根据这个东西,在服务端可以维护一个对应的 Session 域,里面可以放点数据。

一般的话只要你没关掉浏览器,Cookie 还在,那么对应的那个 Session 就在,但是如果 Cookie 没了,Session 也就没了。常见于什么购物车之类的东西,还有登录状态保存之类的。

这个不多说了,懂 Java 的都该知道这个。

单块系统的时候这么玩儿 Session 没问题,但是你要是分布式系统呢,那么多的服务,Session 状态在哪儿维护啊?

其实方法很多,但是常见常用的是以下几种:

完全不用 Session

使用 JWT Token 储存用户身份,然后再从数据库或者 cache 中获取其他的信息。这样无论请求分配到哪个服务器都无所谓。

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 做脚手架,所以用 Spring Session 是一个很好的选择。

关注我的公众号,学习技术或投稿

分布式系统常见问题_第3张图片

长按上图,识别图中二维码即可关注

你可能感兴趣的:(分布式,分布式系统)