分布式的基石
分布式共识算法
前文书说过,分布式场景中,强一致性是最先被放弃的。但事务的主要目的就是要达到一致性。放弃强一致性,为了达到最终一致性,于是有了分布式共识算法。
少数服从多数,不强求所有节点都同意,少数默认按照多数的结果来统一结果。来达到所谓的最终一致性。
状态机
并不是共识算法,这也是一种达到一致性的方式,但是需要等消费完所有命令后才可以。依靠这种方式获得状态,有可能不是最终状态。
状态机有一个特性:任何初始状态一样的状态机,如果执行的命令序列一样,则最终达到的状态也一样。如果将此特性应用在多参与者进行协商共识上,可以理解为系统中存在多个具有完全相同的状态机(参与者),这些状态机能最终保持一致的关键就是起始状态完全一致和执行命令序列完全一致。
- 状态复制
根据状态机的特性,要让多台机器的最终状态一致,只要确保它们的初始状态是一致的,并且接收到的操作指令序列也是一致的即可,无论这个操作指令是新增、修改、删除抑或是其他任何可能的程序行为,都可以理解为要将一连串的操作日志正确地广播给各个分布式节点。
广播指令与指令执行期间,允许系统内部状态存在不一致的情况,即并不要求所有节点的每一条指令都是同时开始、同步完成的,只要求在此期间的内部状态不能被外部观察到,且当操作指令序列执行完毕时,所有节点的最终的状态是一致的,这种模型就被称为状态机复制。
Basic Paxos(基础版Paxos)
存在活锁问题
世界上只有一种共识协议,就是Paxos,其他所有共识算法都是Paxos的退化版本。
Paxos没有考虑拜占拜占庭将军问题。 假设信息可能丢失也可能延迟,但不会被错误传递。
角色介绍:
提案节点(Proposer):提出对某个值设置操作的节点,对值的操作被称提案,一旦设置成功,就不会丢失也不可改变。这里的设置值是一个日志记录操作。
Paxos是基于操作转移模型而非状态转移模型来设计的算法。
决策节点(Acceptor):应答提案的节点,决定该节点是否被投票、是否可被接接受。提案一旦有过半数决策节点的接受,即称该提案被批准Accept,被批准的提案意味着该值不能再被更改,也不会丢失,最终所有节点都会接收该操作。
记录节点(leaner):不参与提案,不参与决策,只是单纯的从提案、决策节点中学习已经达成共识的提案,譬如少数派节点从网络分区中恢复时,将进入这种状态。
每个节点都可能成为上面三种角色中的一种或多种,但为了明确多数派,决策节点的数量应该被设置成奇数,系统初始化时,每个节点都知道整个网络所有决策节点的数量、地址等信息。
可能出现的问题:
- 系统内部各个节点通信是不可靠的,上面的所有角色,发出或收到信息,都可能延迟送达、也可能丢失,但不会出现信息传递错误的情况。
- 系统外部各个用户访问可能是并发的。
并发访问可能产生的问题:
由于多个用户可能同时操作同一变量,请求到达的先后顺序可能会直接影响到值的结果。因此应当通过上锁,来使对于同一值的操作为串行化的。这样所有用户都能提前预判到自己本次操作会带来的结果。
但由于分布式系统中,每个节点都是不可靠的,所以传统的互斥锁很有可能会产生死锁。(如:某个节点取得锁后,与其他节点永久失联,此锁无法自动释放)
因此,必须提供一个其他节点能抢占锁的机制,避免因通信问题而出现死锁。
分布式环境中的锁必须是可抢占的
Paxos的操作步骤:
准备(Prepare):抢占锁的过程,如果某个提案节点准备发起提案,必须先向所有的决策节点广播一个许可申请(Prepare请求)。提案节点会在请求中附带一个全局唯一的数字n作为提案ID,决策节点收到请求后,会给提案节点两个承诺与一个应答:
承诺1:承诺不再接收提案ID小于或等于n的Prepare请求(抢锁请求)
承诺2:对于冲突的数据,承诺不再接收提案ID小于n的Accept请求。
应答:在两个承诺的基础上,回复已经批准过的提案中ID最大的那个提案所设定的值和提案ID,如果该值没有被任何提案设定过,就返回空值。(对于来抢锁的节点,返回当前节点的最新值。)
如果收到的提案请求的Id值小于当前值最新通过的id,就直接忽略本次抢锁请求。
批准(Accept):尝试设置值.
没有冲突的情况,如果提案者发现所有响应的决策节点此前都没有批准过该值(最新值为空),说明本次是第一个设置值的节点,可以随意决定要设置的值,将自己选定的值与提案ID,构成二源数组[id,value],再次广播给所有决策节点(Accept请求)。
如果提案者发现收到的影响中,已经有一个或多个节点的应答中含有值了,他就不能随意取值,必须无条件的接受从应答中找出的最大ID的值,构成二元数组,再次广播给所有决策节点(Accept请求)。
提案者虽然发起了抢锁请求,如果抢到锁,尝试按照自己的值来设置值。
如果没有抢到锁,就按照抢锁请求中返回的结果来共同执行广播抢锁返回的结果。
当提案节点收到多数决策节点应答后,协商结束,共识形成,将形成的决策发送给所有记录节点进行学习。

实例:
- S5初始想设置值为Y,接收到S1已经通过Promise发起的提案值为X。S5放弃自己原来的Y,改为一起通过X值。

X被选定为最终值,并不是必定需要多数派的共同批准,只要S5提案时Promise应答结果中已经包含了批准过X的决策节点,导致S5发起提案的准备阶段,X未获得多数派批准,但由于S3已经批准,最终共识的结果仍为X。
- 未获得多数批准时仍可以赋值成功

一个提案是否通过,不取决于那边获得的Promise(承诺)更多,只取决于哪个提案先通过了Accept。只要有值先通过了Accept,后续再来promise时,天然让新提案结果也共识为之前Accept的结果。
- X已经提案,但并为立刻给出应答,此时Y值的提案到达多数节点,此时达成的共识为Y

s3接收到X值提案后,又接收了新的Y提案(且Y提案版本号领先X),此时X的通过请求被放弃,集群达成的共识为Y。
- 活锁极端情况导致的活锁

不断使用新的版本号来提案,导致准备阶段无限成功,之前通过他的提案全部作废。形成活锁。
解决方案:可以加入随机超时时间,来避免活锁产生。
- Basic Paxos的缺点:
- 只能对单个值形成决议
- 决议的形成至少需要两次网络请求和应答(准备和批准)高并发情况下将产生较大的网络开销
- 极端情况下甚至可能形成活锁
Multi Paxos
选主的Paxos,在Basic Paxos的基础上增加了“选主” 的过程。
zookeeper的ZAB算法,Raft算法都属于本共识算法的派生
通过Basic Paxos选出主节点,后续所有操作都路由给主节点决策,相当于只有主节点有决策权,所有的提案节点接收到请求后,都要把请求路由给主节点,主节点通过接收到请求的先后顺序来决策发起Accepted请求。
如果主节点宕机后(心跳检测发现与主节点失联),则每个节点都会发起提案,尝试让自己为主。

节点只有主(Leader)和从(Follower)的区别,没有提案者、决策者和记录者

分布式场景下,当以下三个问题全部解决时,就相当于达成了分布式的共识。
证明论文:这篇以《一种可以让人理解的共识算法》(In Search of an Understandable Consensus Algorithm)
- 如何选主
所有节点通过心跳检测来判断主节点是否失效
如果失效,开始通过基础Paxos来让自己成为主节点。
- 如何把数据复制到各个节点上
主节点接收变更,并记录日志。提出将某个值设置为X,此时主节点将X写入到自己的变更日志,先不提交,然后将变更成X的信息在下一次心跳广播中广播给所有的从节点,并要求从节点恢复确认收到的消息。
主节点发起变更,给从节点记录日志。从节点收到消息后,将操作写入自己的变更日志中,然后给主节点发送确认签收的消息。主节点收到过半的签收消息后,提交自己的变更、应答客户端并给从节点广播可以提交的消息。
主节点收到半数日志记录完成,提交变更并广播从节点确认日志。从节点收到提交消息,变更自己记录的日志,数据复制完成。
网络产生分区的异常情况:
- S1、S2和S3、S4、S5之间彼此无法通信,形成网络分区。
- 一段时间后,S3、S4、S5中某个节点达到心跳检测阈值,得知当前分区中没有主节点存在,开始发起竞选自己为新主节点。S3、S4、S5选择S3为新主节点。此时分区中存在三个节点,超过半数(5/2=2)。S3成功当选。S1、S2不知道有S3新主的存在。此时集群中出现了两个主节点,S1和S3。
- 此时客户端发起操作请求:
- S1、S2分区接收到请求:请求由S1处理,但S1只能收到两个节点的响应,无法超过半数。S1、S2分区的所有请求都无法成功提交。
- 如果客户端连接到了S3、S4、S5分区,请求由S3处理,S3最多可以收到3个响应,超过半数,修改有效。集群可以正常提供服务。
- 只要系统中仍有半数以上的节点在线,系统就可以正常提供服务。
- 假设网络阻塞被打通,两个分区重新恢复了通信
- S1和S3都向所有节点发送心跳包,S3中的任期编号更大,此时5个节点都只承认S3是唯一的主节点。
- S1、S2回滚它们所有未提交的变更。
- S1、S2从主节点(S3)发送的心跳包中获得它们失联期间发生的所有变更,将变更提交写入到本地。
- 此时分布式系统各节点状态达成最终一致。
如何保证过程的安全
协定性(Safety):所有的坏事都不会发生。
选主的结果一定是有且只有唯一的一个主节点,不可能同时出现两个主节点
终止性(Liveness):所有的好事都终将发生,但不知道啥时候。
选主过程是一定可以在某个时刻能够结束的。
Gossip 协议
Paxos、Raft、ZAB为强一致性的分布式共识协议,虽然系统内部可能会出现不同值,但在外部只能观察到一致的值。
Gossip为最终一致性的分布式共识协议,系统中不一致的状态可能被外部观察到。
病毒式传播。
某个信息需要传播,从信息源开始,按照固定周期(1秒),随机选择它相连接的K个节点来传播消息。
每个节点收到消息后,将在下一个周期内,选择给其他相邻的k个节点发送相同的消息,直到最终网络中的所有节点有收到消息为止。

Gossip 传播示意图(图片来源)
能够容忍网络上节点的随意增加或者减少,随意宕机或者重启,新增加的节点状态最终都会和其他同步达成一致。
完全去中心化,没有主节点的概念
- 缺点
- 消息要通过多个轮次才能到达全网
- 全网的各个节点必定存在不一致的情况
- 对于个体,无法准确的预测消息传播到全网的时间
- 消息冗余,同样的消息重复发送给同一个节点,增加了网络压力
从类库到服务
将构成软件服务的组件,拆分成一个个服务。
采用服务来当组件,而不是类库
类库在程序编译期间静态链接到程序中的,通过调用本地方法来使用其中的功能。
服务是进程外组件,通过远程调用方法来使用其中的功能。
服务发现
如何确定目标方法的确切位置
所有远程服务调用都是使用全限定名、端口号与服务标识所构成的三元组来确定一个远程服务的精确坐标。
- 服务发现三个必须的过程
- 服务的注册:当服务启动时,将自己的坐标信息通知到服务注册中心
- 自注册模式: Spring Cloud 的@EnableEurekaClient 注解,程序主动来完成
- 第三方注册: Kubernetes 和 Registrator,由容器编排框架或第三方注册工具来完成
- 服务的维护(心跳检测):服务是不可靠的,随时可能因为各种意外而被迫下线。服务发现框架必须要自己区保证所维护的服务列表的正确性。
- 维护方式:长连接、心跳、探针、进程状态等
- 服务的发现:消费者从服务发现框架中,把一个服务转换为服务实际坐标的过程。
- Eureka 中的 ServiceID、Nacos 中的服务名、或者通用的 FQDN
- Kubernetes 也支持注入环境变量来做服务发现。
注册中心的地位是特殊的,注册中心不依赖其他服务,但被所有其他服务共同依赖,是系统中最基础的服务,几乎没有可能在业务层面进行容错。注册中心一旦崩溃,整个系统都不再可用。必须尽最大努力保证服务发现的可用性。

- Eureka(AP)
优先保证高可用性,牺牲了系统中服务状态的一致性。
注册:
Eureka的各个节点采用异步复制来交换服务注册信息,当有新服务注册进来时,并不需要等待信息在其他节点注册复制完成,而是马上在该节点宣布服务可见,只是不保证在其他节点上多长时间后才会可见。
维护:
当旧服务下线或者断网,只由超时机制来控制从哪个服务注册表中移除,变动信息不会实时的同步给所有服务端和客户端。
客户端或服务端都维护着一份服务注册表缓存,并以TTL(Time to Live)机制来进行更新。哪怕注册中心崩溃,客户端依然可以维持有限的可用。
总结:
Eureka更适合节点关系相对固定,服务不会频繁的上下线,以较小的代价换取最高的可用性。
万一客户端拿到了已经发生变动的错误地址,也能够通过Ribbon和Hystrix模块配合来兜底,实现故障转移或者快速失败。
- Consul(CP)
采用Raft算法,要求多数派写入成功后,服务的注册或变动才算成功。严格保证集群外读取到的结果必定是一致的。同时采用Gossip协议,支持多数据中心之间更大规模的服务同步。
如果从注册中心中取到了错误的地址,就没有其他兜底方案了。
- CP模型:ZooKeeper、Doozerd、Etcd
Etcd采用Raft算法,ZooKeeper采用ZAB算法,都是主从 Multi Paxos的派生算法。他们都是CP的。
只提供CRUD和Watch等少量API,完整的服务发现,健康检查等,都必须自己手动实现。
- 利用基础设施(主要指DNS服务器)实现的服务发现,SkyDNS,CoreDNS
K8S的服务注册发现机制,是 CP 还是 AP 就取决于后端采用何种存储,如果是基于 Etcd 实现的,那自然是 CP 的,如果是基于内存异步复制的方案实现的,那就是 AP 的(仅针对 DNS 服务器本身,不考虑本地 DNS 缓存的 TTL 刷新)。
优点:对应用透明,任何语言、框架、工具肯定都支持HTTP、DNS的,完全不受程序技术选型的约束。
缺点:必须自己考虑如何做负载均衡、如何调用远程方法等。
- 专门用于服务发现的框架和工具,代表是 Eureka、Consul 和 Nacos。
CP 的 Consul、AP 的 Eureka,还有同时支持 CP 和 AP 的 Nacos(Nacos 采用类 Raft 协议做的 CP,采用自研的 Distro 协议做的 AP,这里“同时”是“都支持”的意思,它们必须二取其一,不是说 CAP 全能满足)。
网关路由(Gateway)
微服务中网关的首要职责就是作为统一的出口对外提供服务,将外部访问网关地址的流量,根据适当的规则路由到内部集群中正确的服务节点之上。
关还可以根据需要作为流量过滤器来使用,提供某些额外的可选的功能,譬如安全、认证、授权、限流、监控、缓存,等等
网关 = 路由器(基础职能) + 过滤器(可选职能)
- 五种网络IO模型
异步IO(Asynchronous I/O):异步IO中数据到达缓冲区后,不需要调用进程主动进行从缓冲区复制数据的操作,而是复制完成后,由操作系统向线程发送信号。
阻塞IO(Blocking I/O):由调用进程执行操作,如果遇到阻塞,调用进程被阻塞。
节省CPU资源。缺点是线程休眠所带来的上下文切换,需要从用户态切换到内核态的重负载操作。
非阻塞IO(Non-Blocking I/O):调用线程执行,如果发现资源没有就绪,就等待固定时间轮询,直到操作结束。可以避免CPU休眠,节省切换上下文的消耗,但如果需要等待时间较长的返回,非阻塞IO浪费了CPU资源。非常不推荐
多路复用IO(Multiplexing IO):可以在同一条阻塞线程上处理多个不同端口的监听。哪个端口资源准备好,就先去处理哪个端口的操作。
目前最主流的网络IO应用,有select、epoll、kqueue 等不同实现。
信号驱动IO(Signal-Driven I/O):有固定线程监听缓冲区,当资源准备完成,此线程把数据复制结束后,通知其他线程执行操作。
客户端负载均衡
- 客户端负载均衡(Ribbon、Spring Cloud Load Balancer)
客户端负载均衡
在服务内部实现的负载均衡,每个服务都有自己对应的负载均衡器,负载均衡器和服务在同一个进程内,互相调用不会出现网络开销。
由于请求的来源可能是来自集群中任意一个服务节点,而不再是统一来自集中式均衡器,这就使得内部网络安全和信任关系变得复杂,当攻破任何一个服务时,更容易通过该服务突破集群中的其他部分。
服务集群的拓扑关系是动态的,每一个客户端均衡器必须持续跟踪其他服务的健康状况,以实现上线新服务、下线旧服务、自动剔除失败的服务、自动重连恢复的服务等均衡器必须具备的功能。由于这些操作都需要通过访问服务注册中心来完成,数量庞大的客户端均衡器一直持续轮询服务注册中心,也会为它带来不小的负担。
- 代理负载均衡
服务网格(Service Mesh)基于代理,类似VPN实现的负载均衡器。
原本嵌入服务进程中的均衡器提取出来,同一个Pod之内的一个特殊服务。(边车代理)

Kubernetes 严格保证了同一个 Pod 中的容器不会跨越不同的节点,这些容器共享着同一个网络名称空间,因此代理均衡器与服务实例的交互,实质上是对本机回环设备的访问,仍然要比真正的网络交互高效且稳定得多。代理均衡器付出的代价较小。
好处:
- 代理均衡器不受编程语言限制
- 服务拓扑感知方面更有优势,控制平台K8S会同一管控服务上下线状态。
- 地域或区域的负载均衡
云计算领域常用的负载均衡,基于地理位置把请求全部分发给不同的机房去处理。尽量保证一个地方的请求,全部都只会在同一个区域的机房内部处理完成,不要出现跨区域调用的情况。
流量治理
容错性设计:失败检查,自动恢复
服务容错
在分布式场景中,错误是必然存在的,当服务出现错误时,与其有关联的服务应当如何处理
常见的容错策略:
故障转移:多数服务均部署了多个副本,每个副本可能在不同的机器上,当某台机器宕机时,故障转移只如果调用的服务出现故障,系统不会立刻向调用者返回失败,而是自动切换到其他服务副本。(Feign的原理)从而保证服务的高可用。
故障转移存在调用次数的限制,如果服务调用超时时间为100毫秒,但失败的请求就花费了60毫秒返回数据,就算转移调用可以正常返回结果,但也会超时,这样的调用也就没意义了。
快速失败:有些业务不允许做故障转移,故障转移策略可以实施的前提是必须要保证接口的幂等性,对于非幂等的接口,重复调用就可能会产生脏数据。
如果服务出现异常。就尽快抛出异常,由调用者处理。
安全失败:服务调用过程中,有主路和旁路之分,(旁路调用不重要,主路调用重要),只要主路调用正确,有部分服务失败了也不影响核心业务的正确性。
沉默失败:当请求失败后,就默认服务提供者一定时间内无法再对外提供服务。
故障恢复:通常以快速失败+故障恢复的策略同时出现,服务调用出错之后,将该调用的失败信息存入一个消息队列,然后由系统自动开始异步重试调用。
例如,核心放款Confirm阶段,调用支付放款失败后,记录流水,等待流水同步。
并行调用:在调用开始之前,就想尽最大可能拿到返回值。一开始就同时向多个服务发起调用,只要其中任何一个返回成功,调用就悬挂成功。
广播调用:调用所有的副本,要求所有请求都返回成功后,才算成功。
如刷新分布式缓存。
| 容错策略 | 优点 | 缺点 | 应用场景 |
|---|---|---|---|
| 故障转移 | 系统自动处理,调用者对失败的信息不可见 | 增加调用时间,额外的资源开销 | 调用幂等服务 对调用时间不敏感的场景 |
| 快速失败 | 调用者有对失败的处理完全控制权 不依赖服务的幂等性 | 调用者必须正确处理失败逻辑,如果一味只是对外抛异常,容易引起雪崩 | 调用非幂等的服务 超时阈值较低的场景 |
| 安全失败 | 不影响主路逻辑 | 只适用于旁路调用 | 调用链中的旁路服务 |
| 沉默失败 | 控制错误不影响全局 | 出错的地方将在一段时间内不可用 | 频繁超时的服务 |
| 故障恢复 | 调用失败后自动重试,也不影响主路逻辑 | 重试任务可能产生堆积,重试仍然可能失败 | 调用链中的旁路服务 对实时性要求不高的主路逻辑也可以使用 |
| 并行调用 | 尽可能在最短时间内获得最高的成功率 | 额外消耗机器资源,大部分调用可能都是无用功 | 资源充足且对失败容忍度低的场景 |
| 广播调用 | 支持同时对批量的服务提供者发起调用 | 资源消耗大,失败概率高 | 只适用于批量操作的场景 |
- 断路器模式
通过代理(断路器对象),来一对一的接管服务调用者的远程请求。
熔断:
断路器来统计调用返回的各种结果,当出现故障的次数达到断路器的阈值,断路器open,窗口内,断路器不再进行远程访问。等待后续恢复。
这样可以避免因持续的失败导致的资源堆积和消耗,避免了雪崩效应。


服务熔断和服务降级的区别:
服务熔断:当服务不可用时,通过上面提到的短路器方式,不再访问出错的服务。调用者来直接处理此异常。不再被调用的服务相当于被熔断了。
服务降级:调用者调用下游,当下游无法工作,或负载太高,需要调用者想其他方式来处理的过程,为服务降级。
服务降级不一定必须是服务熔断时出现,也可能是负载均衡,或流量控制的一种手段。
- 舱壁隔离模式
调用外部服务的故障大致可以分为“失败”(如 400 Bad Request、500 Internal Server Error 等错误)、“拒绝”(如 401 Unauthorized、403 Forbidden 等错误)以及“超时”(如 408 Request Timeout、504 Gateway Timeout 等错误)三大类。
超时场景中,只要请求一直不结束,就会一直占用着某个线程不能释放,如果某个请求大量超时,导致tomcat的全部线程都被占用,导致整个机器上所有的java服务全部瘫痪。

解决方案:
为每个服务单独设置线程池,每个服务接收到的任务,只会在本服务中独有的线程池中运行,如果发生大量超时,也只会导致单个服务崩溃,不会对其他服务造成影响。

根据 Netflix 官方给出的数据,一旦启用 Hystrix 线程池来进行服务隔离,大概会为每次服务调用增加约 3 毫秒至 10 毫秒的延时,如果调用链中有 20 次远程服务调用,那每次请求就要多付出 60 毫秒至 200 毫秒的代价来换取服务隔离的安全保障。
一般来说,我们会选择将服务层面的隔离实现在服务调用端或者边车代理上,将系统层面的隔离实现在 DNS 或者网关处。
- 重试模式
对失败的服务进行重试。
流量控制
场景应用题
已知条件:
- 系统中一个业务操作需要调用 10 个服务协作来完成
- 该业务操作的总超时时间是 10 秒
- 每个服务的处理时间平均是 0.5 秒
- 集群中每个服务均部署了 20 个实例 副本
求解以下问题:
- 流量统计指标
通过哪些数据来反应系统的流量压力大小。
- 每秒事务数(TPS):衡量系统吞吐量的最终标准。
- 每秒请求数(HPS):每秒从客户端发向服务端的请求数,一次业务可能需要多次请求才能完成。
- 每秒查询数(QPS):一台服务器能够响应的查询次数。
- 流量计数器模式
不严谨,设置一个计算器,统计一段时间内的总流量,然后除以时间。
可能出现前面几秒流量极大,后面流量极小。流量统计不严谨。
- 滑动窗口
利用双指针,统计一段时间内的流量数,可以平滑的计算一段时间内的流量数。
可以保证流量不会超过系统设定的最大值。但不能填谷。
- 漏桶模式
通过缓存区来缓冲流量,然后把缓冲区中的流量,较为平稳的发送给服务。
比如搞一个FIFO队列,然后让服务消费队列 中的请求。
可以填谷,且保证流量平稳。但无法动态调整流出速度,流出速度一般是固定值。
- 令牌桶模式
假设x秒内最大请求数不超过Y,则每隔X/Y时间就向桶中添加一个令牌(记得给桶中加入令牌数量的上限,到达上限不再添加令牌)。请求进来时,需先从桶中取到令牌,才能进入系统。一旦桶中没有令牌可取,就尝试让调用服务执行降级逻辑。
- 让系统以一个由限流目标决定的速率向桶中注入令牌,譬如要控制系统的访问不超过 100 次每秒,速率即设定为 100 个令牌每秒,每个令牌注入间隔为 1/100=10 毫秒。
- 桶中最多可以存放 N 个令牌,N 的具体数量是由超时时间和服务处理能力共同决定的。如果桶已满,第 N+1 个进入的令牌会被丢弃掉。
- 请求到时先从桶中取走 1 个令牌,如果桶已空就进入降级逻辑。
- 通过集中缓存还限流(非常不推荐)
通过redis等集中缓存,在缓存中统计每个接口的流量使用情况,然后根据缓存中统计的数量+分布式锁来实现限流。
限流往往只有在高流量的情况下才会使用,但修改redis数量,获取分布式锁等为了限流准备的操作。需要增加至少两次网络IO,反而大大消耗了资源,可能进一步拖累了系统的处理速度。
- 在令牌桶基础上增加货币属性
不把令牌看作简单的准入通行证,同时也给令牌添加货币额度。不同等级的用户,添加不同的额度,每次服务调用,都给令牌消耗一定额度。当额度归零时,让用户重新获取令牌。
只有在获取令牌时需要网络请求,其他情况下都不在需要额外的网络请求。
但可能会导致用户额度归零后,无法获取令牌而使业务中断。
DDD领域驱动设计
传统MVC架构
主要包括M-mode对象层,封装到domain里。V-view展示层,前后端分离,几乎没有JSP文件了。C-Controller控制层,对外提供接口实现类。


DDD
DDD是一种软件设计方法。DDD是指导我们做软件工程设计的一种手段,主要用来切割工程模型:领域、界限上下文、实体、值对象、聚合、工厂、仓储等。通过DDD的指导思想,我们可以在前期投入更多的时间,更加合理的规划出可持续迭代的工程设计。
DDD用来解决什么问题
战略设计
主要以应对复杂的业务需求,通过抽象、分治的过程,合理的拆分出多个微服务,分而治之。少数几个中等规模的单体应用,周围环绕着一个服务生态系统,这更有意义。你实际上并没有构建微服务 @贾斯汀·埃瑟里奇
战术设计
如何基于面向对象思维,运用领域模型来表达业务概念。传统MVC三层架构,会让Service扁平的、大量的,平铺出非常复杂的业务逻辑代码。系统会不断的增加复杂度,直到难以维护的程度。
