架构设计需要遵循下面的原则:

    1. 平衡的艺术:时间、质量、成本等商业目标
    1. 演进原则:最小闭环、能用、适当冗余设计

如果是代码量多达几十万行的大中型项目,团队里几十个人如果不今年新年更系统拆分,开发测试效率都非常低下,非常难以维护。分布式拆分之后可以大幅度提升开发效率,各个模块可以单独部署。但是分布式系统也带来了一些复杂的技术挑战:

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

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

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

扯深了实在很深,所以这里先给大家举个例子,你自己感受一下,核心意思就是根据情况,先拆分一轮,后面如果系统更复杂了,可以继续分拆。你根据自己负责系统的例子,来考虑一下就好了。

分布式 ID 生成器

数据库自增 ID

插入一条记录,生成一个 ID。优点:1. 复用了 DB 的能力,无需额外代码; 2. 全局唯一;3. 绝对自增;4. ID 步长确定。在业务早期,并发量小,追求快速实现的时候完全够用。

缺点:DB 中数据量太多,生成 ID 的性能完全取决于 DB 的插入性能,高并发 DB 会扛不住。
解决方案:1. DB 只保留 MAX ID; 2. 性能差可以增加 1 层服务层,采用批量生成的方式减低 DB 写压力。

id-service 的具体实现:

  1. SELECT MAX(id) FROM t_id, 例如:max_id = 100
  2. 批量生成一批 id 放入 id-service 内存, 并将 max_id 写回 DB(使用 UPDATE t_id SET id = 200 WHERE id = 100 乐观锁保证只有一个 service 能成功写入,失败的进程需要重试)

分配 ID 的整体性能就提升了 100 倍。

分布式事务

常用的有以下 5 种实现方案:

XA

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

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

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

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

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

image.png

TCC

全称:Try,Confirm,Cancle。

  • Try 阶段:这个阶段说的是对各个服务的资源做检测以及对资源进行锁定或者预留
  • Confirm 阶段:这个阶段说的是在各个服务中执行实际的操作
  • Cancel 阶段:如果任何一个服务的业务方法执行出错,那么这里就需要进行补偿,就是执行已经执行成功的业务逻辑的回滚操作。(把那些执行成功的回滚)

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

比如说我们,一般来说跟相关的,跟钱打交道的,支付、交易相关的场景,我们会用 TCC,严格保证分布式事务要么全部成功,要么全部自动回滚,严格保证资金的正确性,保证在资金上不会出现问题。而且最好是你的各个业务执行的时间都比较短。

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

image.png

可靠消息最终一致性

直接基于 MQ 来实现事务。比如阿里的 RocketMQ 就支持消息事务。大概意思如下:

  1. A 系统先发送一个 prepared 消息到 mq,如果这个 prepared 消息发送失败那么就直接取消操作别执行了;
  2. 如果这个消息发送成功过了,那么接着执行本地事务,如果成功就告诉 mq 发送确认消息,如果失败就告诉 mq 回滚消息;
  3. 如果发送了确认消息,那么此时 B 系统会接收到确认消息,然后执行本地的事务;
  4. mq 会自动定时轮询所有 prepared 消息回调你的接口,问你,这个消息是不是本地事务处理失败了,所有没发送确认的消息,是继续重试还是回滚?一般来说这里你就可以查下数据库看之前本地事务是否执行,如果回滚了,那么这里也回滚吧。这个就是避免可能本地事务执行成功了,而确认消息却发送失败了。
  5. 这个方案里,要是系统 B 的事务失败了咋办?重试咯,自动不断重试直到成功,如果实在是不行,要么就是针对重要的资金类业务进行回滚,比如 B 系统本地回滚后,想办法通知系统 A 也回滚;或者是发送报警由人工来手工回滚和补偿。

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

image.png

最大努力通知

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

  1. 系统 A 本地事务执行完之后,发送个消息到 MQ;
  2. 这里会有个专门消费 MQ 的最大努力通知服务,这个服务会消费 MQ 然后写入数据库中记录下来,或者是放入个内存队列也可以,接着调用系统 B 的接口;
  3. 要是系统 B 执行成功就 ok 了;要是系统 B 执行失败了,那么最大努力通知服务就定时尝试重新调用系统 B,反复 N 次,最后还是不行就放弃。

最佳实践:特别严格的场景,用的是 TCC 来保证强一致性;然后其他的一些场景基于阿里的 RocketMQ 来实现分布式事务。

分布式 Session

常用的解决方案有 2 种:

  • 基于 redis 的 session 共享
  • 使用 JWT 存储用户身份,再从 DB 或者 cache 中获取其他信息。这样无论请求分配到哪个服务器都无所谓。

如何实现一个 RPC 框架

  1. 服务要去注册中心注册,所以要有个服务注册中心,保留各个服务的信息,可以用 ZK
  2. 消费者需要去注册中心上拿到对应的服务信息
  3. 向机器发起请求可以采用负载均衡算法,比如最简单的轮询
  4. 服务器接收到序列化请求后调用对应的代码返回数据并序列化数据发给客户端
  5. 客户端反序列化得到 RPC 的结果

跨公网调用,第三方接口超时,怎么优化

异步代理法

异步代理法

如上图所示:增加 async-proxy 向业务方屏蔽“本地实时”和“远程”调用。由 async-proxy 定期维护远程结果缓存到本地。典型的场景就是:通过 openid 获取用户基本信息。优点是:公网抖动、第三方接口超时并不影响内部接口。不足:本地的不是最新数据(很多业务是可以接受数据延迟的)。

主备切换法

非独享的第三方服务可以有多个供应商备份。

主备切换法

例如:发送短信可以同时接入阿里云和腾讯云。

异步调用法

先直接返回第三方处理成功,业务成功后进行通知。

如何保证 DB 和 Cache 的缓存双写一致

解决缓存和 DB 一致性的问题,常见的解决方案有下面的 4 种:

  1. 先修改 DB,后更新缓存
  2. 先更新缓存,后修改 DB
  3. 先修改 DB,后删除缓存
  4. 先删除缓存,后修改 DB

1-3 这 3 种方案都有同一个问题:当第一步成功执行,第二步未执行的时候(例如:停电),DB 和缓存的数据就不一致了!方案 4 相比起来更好 —— 但是依然存在并发保存旧值的情况

先删缓存,后改DB的并发保存旧值情况

最终解决方案是:消息队列 + 延迟双删。

MQ 有消息确认机制,可以保证我们在执行完第一步之后,即使出现掉电重启的情况,依然可以执行后续的流程。

延迟双删的具体流程如下:

  1. 删除缓存
  2. 更新 DB
  3. 延迟一会(500ms-1s)再删除缓存

最后一次延迟删除缓存的原因是,为了避免上面因为并发问题导致保存旧值的情况发生,所以会延迟一段时间之后再进行删除操作。这样即使有并发问题,也能最大限度的解决保存旧值的情况,因为是延迟之后删除的,所以即使因为并发问题保存了旧值,但延迟一段时间之后旧值就会被删除,那么这样就自然而然的保证了数据库和缓存的最终一致性。

如何解决时钟回拨问题

雪花算法要求系统时钟必须是单调递增的,如果系统时钟发生回拨(时间逆流),可能导致生成的 ID 重复。百度 UidGenerator 框架中解决了时间回拨的问题,并且解决方案比较经典:

UidGenerator 的每个实例中,都维护一个本地时钟缓存,用于记录当前时间戳。这个本地时钟会定期与系统时钟进行同步,如果检测到系统时钟往前走了(出现了时钟回拨),则将本地时钟调整为系统时钟。

UidGenerator 是百度开源的一个分布式唯一 ID 生成器,它是基于 Snowflake 算法的改进版本。与传统的 Snowflake 算法相比,UidGenerator 在高并发场景下具有更好的性能和可用性。

分布式共识算法

Paxos 基于“少数服从多数”(Quorum 机制),通过“请求阶段”和“批准阶段”在不确定的环境下,解决了单个“提案”的共识问题。

Raft 算法属于“强领导者”(Strong Leader) 模型,领导者负责所有的写入操作,它是整个系统的写瓶颈。解决方案是使用哈希算法将数据划分成多个独立的分片。

Raft 是 Re{liable|picated|dundant} And Fault-Tolerant 的缩写。即:可靠、复制、冗余和容错。

Paxos 算法理解起来十分难懂,理论描述和实际工程实践之间存在巨大鸿沟,最终实现的系统往往建立在一个尚未完全证明的算法基础之上。

Raft 算法是分布式系统领域的首选共识算法。