分布式系统,即是将一个项目中的各个服务分散部署在不同的机器上的,一个服务可能负责几个功能,是一种面向服务的架构(SOA)。分布式系统可以看做是若干个独立计算机的集合,这些服务在不同的计算机上部署,但还是属于同一个项目。
1.说说 CAP 定理、 BASE 理论
CAP
- 一致性(Consistency) : 所有节点访问同一份最新的数据副本
- 可用性(Availability): 非故障的节点在合理的时间内返回合理的响应(不是错误或者超时的响应)。
- 分区容错性(Partition Tolerance) : 分布式系统出现网络分区的时候,仍然能够对外提供服务。
CAP 理论中分区容错性 P 是一定要满足的,在此基础上,只能满足可用性 A 或者一致性 C。
Nacos 不仅支持 CP 也支持 AP。
为啥不可能选择 CA 架构呢?
举个例子:若系统出现“分区”,系统中的某个节点在进行写操作。为了保证 C, 必须要禁止其他节点的读写操作,这就和 A 发生冲突了。如果为了保证 A,其他节点的读写操作正常的话,那就和 C 发生冲突了。
BASE 理论
AP 方案只是在系统发生分区的时候放弃一致性,而不是永远放弃一致性。在分区故障恢复后,系统应该达到最终一致性。这一点其实就是 BASE 理论延伸的地方。
基本可用:
分布式系统在出现不可预知故障的时候,允许损失部分可用性。
什么叫允许损失部分可用性呢?
-
响应时间上的损失: 正常情况下,处理用户请求需要 0.5s 返回结果,但是由于系统出现故障,处理用户请求的时间变为 3 s。
-
系统功能上的损失:正常情况下,用户可以使用系统的全部功能,但是由于系统访问量突然剧增,系统的部分非核心功能无法使用。
软状态:
允许系统中的数据存在中间状态(CAP 理论中的数据不一致),并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。
最终一致性:
强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。不需要实时保证系统数据的强一致性。
分布式一致性的 3 种级别:
- 强一致性:系统写入了什么,读出来的就是什么。
- 弱一致性:不一定可以读取到最新写入的值,也不保证多少时间之后读取到的数据是最新的,只是会尽量保证某个时刻达到数据一致的状态。
- 最终一致性:弱一致性的升级版,系统会保证在一定时间内达到数据一致的状态。
业界比较推崇是最终一致性级别,但是某些对数据一致要求十分严格的场景比如银行转账还是要保证强一致性。
分布式事务(目的是为了保证分布式系统中的数据一致性)
1.什么是分布式事务?
分布式事务其实就是将对同一库事务的概念扩大到了对多个库的事务。目的是为了保证分布式系统中的数据一致性。
- 需要记录事务在任何节点所做的所有动作;
- 事务进行的所有操作要么全部提交,要么全部回滚。
2.你们用什么?能说一下Seata吗?
Seata 的设计目标是对业务无侵入,因此它是从业务无侵入的两阶段提交(全局事务)着手,在传统的两阶段上进行改进,他把一个分布式事务理解成一个包含了若干分支事务的全局事务。而全局事务的职责是协调它管理的分支事务达成一致性,要么一起成功提交,要么一起失败回滚。
Seata 中存在这么几种重要角色:
-
TC(Transaction Coordinator):事务协调者。管理全局的分支事务的状态,用于全局性事务的提交和回滚。
-
TM(Transaction Manager):事务管理者。用于开启、提交或回滚事务。
-
RM(Resource Manager):资源管理器。用于分支事务上的资源管理,向 TC 注册分支事务,上报分支事务的状态,接收 TC 的命令来提交或者回滚分支事务。
Seata整体执行流程:
- 服务A中的 TM 向 TC 申请开启一个全局事务,TC 就会创建一个全局事务并返回一个唯一的 XID
- 服务A中的 RM 向 TC 注册分支事务,然后将这个分支事务纳入 XID 对应的全局事务管辖中
- 服务A开始执行分支事务
- 服务A开始远程调用B服务,此时 XID 会根据调用链传播
- 服务B中的 RM 也向 TC 注册分支事务,然后将这个分支事务纳入 XID 对应的全局事务管辖中
- 服务B开始执行分支事务
- 全局事务调用处理结束后,TM 会根据有误异常情况,向 TC 发起全局事务的提交或回滚
- TC 协调其管辖之下的所有分支事务,决定是提交还是回滚
2.分布式事务常见的实现方案:
分布式常见的实现方案有 2PC、TCC、本地消息表、MQ消息事务、最大努力通知、SAGA事务 等等
2PC
- 准备阶段:事务管理器要求每个涉及到事务的数据库预提交此操作,并反映是否可以提交
- 提交阶段:事务协调器要求每个数据库提交数据,或者回滚数据。
TCC
TCC也是补偿型事务模式,支持两阶段的商业模型。
- Try:尝试待执行的业务。订单系统将当前订单状态设置为支付中,库存系统校验当前剩余库存数量是否大于1,然后将可用库存数量设置为库存剩余数量-1,。
- Confirm:确认执行业务,如果Try阶段执行成功,接着执行Confirm 阶段,将订单状态修改为支付成功,库存剩余数量修改为可用库存数量。
- Cancel:取消待执行的业务,如果Try阶段执行失败,执行Cancel 阶段,将订单状态修改为支付失败,可用库存数量修改为库存剩余数量。
3.分布式限流方法
- 计数器
比如我们要限制1s能够通过的请求数,实现的思路就是从第一个请求进来开始计时,在接下来的1s内,每个请求进来请求数就+1,超过最大请求数的请求会被拒绝,等到1s结束后计数清零,重新开始计数。
- 漏桶算法
就是桶底出水的速度恒定,进水的速度可能快慢不一,但是当进水量大于出水量的时候,水会被装在桶里,不会直接被丢弃
但是桶也是有容量限制的,当桶装满水后溢出的部分还是会被丢弃的。
- 令牌桶算法
令牌桶算法以一个设定的速率产生令牌并放入令牌桶,每次用户请求都得申请令牌,如果令牌不足,则拒绝请求。
分布式设计
1.说说什么是幂等性?
用在接口上就可以理解为:同一个接口,多次发出同一个请求,请求的结果是一致的。
2.什么是幂等性问题?
在系统的运行中,可能会出现这样的问题:
-
用户在填写某些
form表单
时,保存按钮不小心快速点了两次,表中竟然产生了两条重复的数据,只是id不一样。 -
开发人员在项目中为了解决
接口超时
问题,通常会引入了重试机制
。第一次请求接口超时了,请求方没能及时获取返回结果(此时有可能已经成功了),于是会对该请求重试几次,这样也会产生重复的数据。 -
mq消费者在读取消息时,有时候会读取到
重复消息
,也会产生重复的数据。
这些都是常见的幂等性问题。
在分布式系统里,只要下游服务有写(保存、更新)的操作,都有可能会产生幂等性问题。
3.怎么保证接口幂等性呢?
1.insert前先select
在保存数据的接口中,在insert
前,先根据requestId
等字段先select
一下数据。如果该数据已存在,则直接返回,如果不存在,才执行 insert
操作。
2.加唯一索引
加唯一索引是个非常简单但很有效的办法,如果重复插入数据的话,就会抛出异常,为了保证幂等性,一般需要捕获这个异常。
如果是java
程序需要捕获:DuplicateKeyException
异常,如果使用了spring
框架还需要捕获:MySQLIntegrityConstraintViolationException
异常。
3.加悲观锁
更新逻辑,比如更新用户账户余额,可以加悲观锁,把对应用户的哪一行数据锁住。同一时刻只允许一个请求获得锁,其他请求则等待。
select * from user id=123 for update;
这种方式有一个缺点,获取不到锁的请求一般只能报失败,比较难保证接口返回相同值。
4.加乐观锁
更新逻辑,也可以用乐观锁,性能更好。可以在表中增加一个timestamp
或者version
字段,例如version
:
在更新前,先查询一下数据,将version也作为更新的条件,同时也更新version:
update user set amount=amount+100,version=version+1 where id=123 and version=1;
更新成功后,version增加,重复更新请求进来就无法更新了。
6.分布式锁
直接在数据库上加锁的做法性能不够友好,可以使用分布式锁的方式,目前最流行的分布式锁实现是通过Redis,具体实现一般都是使用Redission框架。
分布式锁
1.基于 Redis 实现分布式锁
Redis 实现分布式锁的本质,就是在 Redis 里面占一个“茅坑”,当别的客户端也来占坑时,发现已经有客户端蹲在那里了,就只好放弃或者稍后再试。
可以使用 Redis 的 SET 命令实现分布式锁。SET 命令支持设置键值对的同时添加过期时间,这样可以防止死锁的发生。如果 key 不存在的话,才会设置 key 的值。如果 key 已经存在, SETNX
啥也不做。
2.为什么要给锁设置一个过期时间?
为了避免锁无法被释放,一定要保证设置指定 key 的值和过期时间是一个原子操作!!! 不然的话,依然可能会出现锁无法被释放的问题。