八股文-分布式微服务篇
分布式
CAP理论
【问题】: 什么是CAP?
【答案】: CAP 理论是对分布式系统的特性做了一个高度的抽象,变成了三大指标:
- 一致性(Consistency):一致性强调的是数据正确,每次读取节点中的数据都是最新写入的数据。
- 可用性(Availability):每个节点使用本地数据来响应客户端的请求。另外当节点不可用时,可以使用快速失败策略,至少不能让服务长时间不能响应。可用性强调的是服务可用,不保证数据正确。
- 分区容错性(Partition Tolerance):分区容错性的含义就是节点间出现任意数量的消息丢失或高延迟的时候,系统仍然在继续工作。分布式系统告诉客户端,我的内部不论出现什么样的数据同步问题,我会一直运行。强调的是集群对分区故障的容错能力。
对于分布式系统,CAP 三个指标只能选择其中两个。
【大白话解释】: CAP 就像你和朋友合租,你们要决定三件事:账本数据必须一模一样(一致性)、随时都能查账(可用性)、两人不在家也能各查各的(分区容错)。但网络一旦出问题,你要么保证数据对但可能查不了(CP),要么随时能查但数据可能不是最新的(AP),没法全都要。
【扩展知识讲解】: CA:保证一致性和可用性。当分布式系统正常运行时(大部分时候所处的状态),这个时候不需要 P,那么 C 和 A 能够同时保证。只有在发生分区故障时,才需要 P,这个时候就只能在 C 和 A 之间做出选择。典型应用:单机版部署的 MySQL。
CP:保证数据的一致性和分区容错性,比如配置信息,必须保证每个节点存的都是最新的,正确的数据。比如 Raft 的强一致性系统,会导致无法执行读操作和写操作。典型应用:Etcd、Consul、Hbase。
AP:保证分布式系统的可用性和分区容错性。用户访问系统,都能得到相应数据,不会出现响应错误,但是可能会读到旧的数据。典型应用:Cassandra 和 DynamoDB。
BASE理论
【问题】: 什么是BASE理论?
【答案】: BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent(最终一致性)三个短语的缩写。BASE 理论是对 CAP 中 AP 的一个扩展,通过牺牲强一致性来获得可用性,当出现故障允许部分不可用但要保证核心功能可用,允许数据在一段时间内是不一致的,但最终达到一致状态。满足 BASE 理论的事务,我们称之为柔性事务。
【大白话解释】: BASE 就是 “差不多就行了” 理论——不用时刻保持数据一模一样,只要最终能对上就行。就像你转账,可能中间有个 “处理中” 的状态,但最后钱一定是对的。
【扩展知识讲解】: 基本可用:分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。如电商网站交易付款出现问题了,商品依然可以正常浏览。
软状态:由于不要求强一致性,所以BASE允许系统中存在中间状态(也叫软状态),这个状态不影响系统可用性,如订单中的”支付中”、”数据同步中”等状态,待数据最终一致后状态改为”成功”状态。
最终一致性:最终一致是指经过一段时间后,所有节点数据都将会达到一致。如订单的”支付中”状态,最终会变为”支付成功”或者”支付失败”,使订单状态与实际交易结果达成一致,但需要一定时间的延迟、等待。
负载均衡
【问题】: 常用的负载均衡算法有哪几个?
【答案】:
- 轮询:请求依次按顺序分发到不同的可用服务器执行,循环分发请求。
- 最小连接:分发请求到连接数最少的服务器。场景:处理请求用时较长的场景。
- 散列:根据用户请求的IP地址的散列(hash)来选择要转发的服务器。场景:需要处理状态而要求用户能连接到相同服务器。
【大白话解释】: 轮询就是排号叫人,一人一个轮流来;最小连接就是谁闲给谁;散列就是按你身份证尾号分窗口,保证你每次都去同一个窗口。
分布式事务
【问题】: 什么是分布式事务?有哪些实现方案?
【答案】: 事务是一个程序执行单元,里面的所有操作要么全部执行成功,要么全部执行失败。
分布式的理论基础是CAP,由于P(分区容错)是必选项,所以只能在AP或者CP中选择:
- 分布式理论的CP → 刚性事务,遵循ACID,对数据要求强一致性
- 分布式理论的AP+BASE → 柔性事务,遵循BASE,允许一定时间内不同节点的数据不一致,但要求最终一致
分布式事务的实现主要有以下6种方案:XA方案、TCC方案、SAGA方案、本地消息表、可靠消息最终一致性方案、最大努力通知方案。
【大白话解释】: 分布式事务就是跨多个服务的事务,比如你下单要同时扣库存、扣余额、加积分,要么全成功要么全回滚。因为网络分区不可避免,所以只能在”数据一定要对”和”服务一定要可用”之间二选一,于是就有了刚性事务和柔性事务两种思路。
【问题】: 介绍下XA两阶段提交方案
【答案】: 第一阶段询问,第二阶段提交执行。有一个事务管理器的概念,负责协调多个数据库(资源管理器)的事务,事务管理器先问问各个数据库你准备好了吗?如果每个数据库都回复 ok,那么就正式提交事务,在各个数据库上执行操作;如果任何其中一个数据库回答不 ok,那么就回滚事务。
这种分布式事务方案,比较适合单块应用里,跨多个库的分布式事务,而且因为严重依赖于数据库层面来搞定复杂的事务,效率很低,绝对不适合高并发的场景。基于Spring + JTA 就可以搞定。
【大白话解释】: XA就像开会表决:主持人先问大家”能不能干?”,所有人都说能,才正式动手;只要有一个人说不行,大家就都别干了。问题是一直锁着资源等表决结果,效率很低,人一多就卡死。
【问题】: 介绍下TCC三阶段提交方案
【答案】: TCC的全称是 Try、Confirm、Cancel。金融核心等业务可能会选择 TCC 方案,以追求强一致性和更高的并发量。
- Try 阶段:对各个服务的资源做检测以及对资源进行锁定或者预留(冻结银行B资金,类似于上锁)
- Confirm 阶段:在各个服务中执行实际的操作(在自己库插数据,调用银行B扣款接口,调用银行C转账接口)
- Cancel 阶段:如果任何一个服务的业务方法执行出错,那么就需要进行补偿,就是执行已经执行成功的业务逻辑的回滚操作(回滚,将银行B的扣款加回去)
这种方案几乎很少人使用,因为事务回滚严重依赖于自己写代码来回滚和补偿,会造成补偿代码巨大。跟钱打交道的,支付、交易相关的场景,会用 TCC,严格保证分布式事务要么全部成功,要么全部自动回滚,严格保证资金的正确性,而且最好是各个业务执行的时间都比较短。一般尽量别这么搞,自己手写回滚逻辑,或者是补偿逻辑,实在太恶心,业务代码是很难维护的。
【大白话解释】: TCC 就是先冻结(Try)再确认(Confirm),出问题就撤销(Cancel)。比如转账:先冻结你要扣的钱,确认没问题就真扣,有问题就把冻结的钱解冻还回去。缺点是每个操作都要写三套代码,累死人。
【问题】: 介绍下SAGA方案
【答案】: 目前业界比较公认的是采用 Saga 作为长事务的解决方案。业务流程中每个参与者都提交本地事务,若某一个参与者失败,则补偿前面已经成功的参与者。
正常事务流程是 T1 → T2 → T3,当执行到 T3 时发生了错误,那开始从 T3 开始反向执行补偿服务 C3 → C2 → C1,把 T1、T2、T3 已经修改的数据补偿掉。
Saga 模式的适用场景是:业务流程长、业务流程多;参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口。
优势:
- 一阶段提交本地事务,无锁,高性能
- 参与者可异步执行,高吞吐
- 补偿服务易于实现,比如一个更新操作的反向操作是比较容易理解的
缺点:
- 不保证事务的隔离性
【扩展知识讲解】: 很多金融核心以上的业务(渠道层、产品层、系统集成层),这些系统的特点是最终一致即可。流程多、流程长、还可能要调用其它公司的服务,这种情况如果选择 TCC 方案开发的话,一来成本高,二来无法要求其它公司的服务也遵循 TCC 模式。同时流程长,事务边界太长,加锁时间长,也会影响并发性能。
【大白话解释】: Saga 就是 “干一步提交一步,出错就倒着撤”。比如出差报销:填单子→领导签字→财务打钱,走到财务那被打回来了,那就撤销领导签字→撕掉单子,一步一步往回退。好处是不用一直锁着资源,坏处是中间状态别人能看到。
【问题】: 介绍下本地消息表方案
【答案】:
- A 系统在自己本地一个事务里操作同时,插入一条数据到消息表
- 接着 A 系统将这个消息发送到 MQ 中去
- B 系统接收到消息之后,在一个事务里,往自己本地消息表里插入一条数据,同时执行其他的业务操作,如果这个消息已经被处理过了,那么此时这个事务会回滚,这样保证不会重复处理消息
- B 系统执行成功之后,就会更新自己本地消息表的状态以及 A 系统消息表的状态
- 如果 B 系统处理失败了,那么就不会更新消息表状态,那么此时 A 系统会定时扫描自己的消息表,如果有未处理的消息,会再次发送到 MQ 中去,让 B 再次处理
- 这个方案保证了最终一致性,哪怕 B 事务失败了,但是 A 会不断重发消息,直到 B 那边成功为止
这个方案最大的问题就在于严重依赖于数据库的消息表来管理事务,如果是高并发场景咋办呢?咋扩展呢?所以一般很少用。
【大白话解释】: 本地消息表就像寄快递:你做完自己的事同时写个发货单(消息表),然后把货发出去(MQ),对方收到后确认签收。对方要是没收着,你就定时翻翻发货单,发现没签收的就再发一次。问题是要在数据库里多存一张表,高并发时数据库顶不住。
【问题】: 介绍下可靠消息最终一致性方案
【答案】: 直接基于 MQ 来实现事务。比如阿里的RocketMQ 就支持消息事务。
- A 系统先发送一个 prepared 消息到 mq,如果这个 prepared 消息发送失败那么就直接取消操作别执行了
- 如果这个消息发送成功过了,那么接着执行本地事务,如果成功就告诉 mq 发送确认消息,如果失败就告诉 mq 回滚消息
- 如果发送了确认消息,那么此时 B 系统会接收到确认消息,然后执行本地的事务
- mq 会自动定时轮询所有 prepared 消息回调你的接口,询问这个消息是不是本地事务处理失败了,所有没发送确认的消息是继续重试还是回滚?一般来说这里就可以查下数据库看之前本地事务是否执行,如果回滚了那么这里也回滚。这个就是避免可能本地事务执行成功了,而确认消息却发送失败了
- 要是系统 B 的事务失败了咋办?重试,自动不断重试直到成功,如果实在是不行,要么就是针对重要的资金类业务进行回滚,比如 B 系统本地回滚后,想办法通知系统 A 也回滚;或者是发送报警由人工来手工回滚和补偿
目前国内互联网公司大都是这么玩儿的,要不你就用 RocketMQ 支持的,要不你就自己基于类似 ActiveMQ、RabbitMQ 自己封装一套类似的逻辑出来,总之思路就是这样子的。
【大白话解释】: 这个方案就像发挂号信:先贴个半成品邮票(prepared),本地事办完了才盖正式邮戳(confirm),如果本地事没办成就把信撤回(rollback)。邮局还会定期问你这信到底寄不寄,防止你事办完了但忘了确认。对方收不到确认信就一直重试,实在不行就人工处理。
【问题】: 介绍下最大努力通知方案
【答案】:
- 系统 A 本地事务执行完之后,发送个消息到 MQ
- 有个专门消费 MQ 的最大努力通知服务,这个服务会消费 MQ 然后写入数据库中记录下来,或者是放入个内存队列也可以,接着调用系统 B 的接口
- 要是系统 B 执行成功就 ok 了;要是系统 B 执行失败了,那么最大努力通知服务就定时尝试重新调用系统 B,反复 N 次,最后还是不行就放弃
【大白话解释】: 最大努力通知就是 “我尽力了”——我办完我的事就通知你,你要是没收到我就多通知几次,但通知 N 次你还不行我就不管了,放弃。适合对一致性要求不高的场景,比如短信通知、邮件提醒。
限流
【问题】: 什么是限流?常用的限流算法有哪些?
【答案】: 限流就是:当高并发或者瞬时高并发时,为了保证系统的稳定性、可用性,系统以牺牲部分请求为代价或者延迟处理请求为代价,保证系统整体服务可用。
令牌桶(Token Bucket)、漏桶(Leaky Bucket)和计数器算法是最常用的三种限流算法。
【大白话解释】: 令牌桶就像游乐园发手环,桶里匀速往里扔手环,有手环才能玩,桶满了手环就不加了,桶空了就得等;漏桶就像漏斗倒水,不管你倒多快,下面都是匀速流出来;计数器就是数人头,一分钟内超过N个人就不让进了。
【扩展知识讲解】: 分布式限流和接入层限流可以进行全局限流,常用实现方式:
- redis+lua 实现中的 lua 脚本
- 使用 Nginx+Lua 实现的 Lua 脚本
- 使用 OpenResty 开源的限流方案
- 限流框架,比如 Sentinel 实现降级限流熔断
服务熔断降级
【问题】: 什么是雪崩?熔断?降级?
【答案】: 雪崩效应:
- 第一次滚雪球:库存服务不可用(如响应超时等),库存服务收到的很多请求都未处理完,库存服务将无法处理更多请求。
- 第二次滚雪球:因商品服务的请求都在等库存服务返回结果,导致商品服务调用库存服务的很多请求未处理完,商品服务将无法处理其他请求,导致商品服务不可用。
- 第三次滚雪球:因商品服务不可用,订单服务调用商品服务的其他请求无法处理,导致订单服务不可用。
- 第四次滚雪球:因订单服务不可用,客户端将不能下单,更多客户将重试下单,将导致更多下单请求不可用。
熔断:设置服务的超时,当被调用的服务某段时间内失败率达到某个阈值,则对该服务开启短路保护,后来的请求不调用这个服务,直接返回默认的数据。
降级:对非核心业务降级运行,某些服务不处理,或者简单处理(抛异常、返回Null、返回Mock数据)。
【大白话解释】: 雪崩就是一个服务挂了,拖着依赖它的服务也挂,一个接一个全倒;熔断就像保险丝,发现某个服务老出问题就断开它,别让它拖累别人,直接返回个兜底值;降级就是 “非核心业务先停一停”,比如双11把推荐功能关了,保住下单功能就行。
幂等
【问题】: 分布式系统中,幂等性如何设计?
【答案】: 在高并发场景的架构里,比如支付场景,幂等性是必须得保证的。常见设计方案:
建唯一索引:唯一索引或唯一组合索引来防止新增数据存在脏数据(当表存在唯一索引,并发时新增异常时,再查询一次就可以了,数据应该已经存在了,返回结果即可)。
token机制:由于重复点击或者网络重发,或者nginx重发等情况会导致数据被重复提交。前端在数据提交前要向后端服务的申请token,token放到 Redis 或 JVM 内存,token有效时间。提交后后台校验token,同时删除token,生成新的token返回。redis要用删除操作来判断token,删除成功代表token校验通过,如果用select+delete来校验token,存在并发问题,不建议使用。
悲观锁:select id, name from table_# where id='##' for update; 悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,根据实际情况选用(另外还要考虑id是否为主键,如果id不是主键或者不是 InnoDB 存储引擎,那么就会出现锁全表)。
乐观锁:给数据库表增加一个version字段,可以通过这个字段来判断是否已经被修改了:update table_xxx set name=#name#, version=version+1 where version=#version#
分布式锁:比如 Redis、Zookeeper 的分布式锁。单号为key,然后给Key设置有效期(防止支付失败后,锁一直不释放),来一个请求使用订单号生成一把锁,业务代码执行完成后再释放锁。
【大白话解释】: 幂等就是 “同一个操作做一次和做多次效果一样”。比如你点支付按钮手抖连点三次,钱只能扣一次。建唯一索引就是给数据上 “身份证号”,重复的进不来;token机制就是发号排队,一个号只能用一次;悲观锁就是 “我先锁着,你们等着”;乐观锁就是 “大家都能改,但改的时候看版本号对不对”;分布式锁就是全系统只认一把锁,谁拿到谁干活。
三高架构
【问题】: 什么是三高架构?
【答案】: 高并发性能能够保证系统在面对大量并发请求时能够高效处理;高可用性能够保证系统在面对故障和异常时能够持续提供服务;高性能能够保证系统在有限资源下能够以较快的速度完成任务。这三者相辅相成,构成了一个健壮、可靠和高效的软件系统。
【扩展知识讲解】:
高并发性:
- 多线程与并发库:java.util.concurrent包,synchronized关键字,volatile关键字。
- 线程池:Executors,ThreadPoolExecutor。
- 异步处理:CompletableFuture,RxJava。
- 消息队列:RabbitMQ,Kafka,ActiveMQ。
- 负载均衡:Nginx,Apache Load Balancer,Zuul。
高可用性:
- 集群:通过多实例部署实现负载均衡和故障转移。
- 服务发现与注册:Eureka,Consul,Zookeeper。
- 断路器模式:Hystrix,Resilience4j。
- 配置中心:Spring Cloud Config。
- 微服务架构:Spring Boot,Spring Cloud。
- 容器化与编排:Docker,Kubernetes。
高性能:
- JVM调优:垃圾回收算法(G1,CMS,ZGC),JVM参数调优。
- 缓存:Redis,Memcached,Caffeine。
- 数据库优化:
- SQL调优:索引,查询优化。
- 数据库连接池:HikariCP,DBCP,Tomcat JDBC。
- 分库分表:ShardingSphere,MyCAT。
- 静态资源处理:使用CDN,静态资源服务器(如Nginx)。
- 代码优化:算法优化,数据结构选择。
- I/O优化:使用NIO,AIO。
- 代码生成和编译:GraalVM,JIT编译。
其他相关技术:
- API网关:Spring Cloud Gateway,Zuul。
- 限流:Guava的RateLimiter,Sentinel。
- 监控和跟踪:Prometheus,Grafana,Zipkin,SkyWalking。
- 日志管理:ELK Stack(Elasticsearch,Logstash,Kibana)。
- 测试:单元测试(JUnit),性能测试(JMeter,Gatling)。
- 安全:OAuth 2.0,JWT,Spring Security。