-
这个术有一个特别厉害的地方,过程和心得:多个分身的感受和经历都是相通的。比如 A 分身去找卡卡西(鸣人的老师)请教问题,那么其他分身也会知道 A 分身问的什么问题。
-
漩涡鸣人有另外一个超级厉害的忍术,需要由几个影分身完成:风遁·螺旋手里剑。这个忍术是靠三个鸣人一起协作完成的。
-
分布在不同地方的系统或服务,是彼此相互关联的。
-
分布式系统是分工合作的。
-
比如 Redis 的哨兵机制,可以知道集群环境下哪台 Redis 节点挂了。Kafka的 Leader 选举机制,如果某个节点挂了,会从 follower 中重新选举一个 leader 出来。(leader 作为写数据的入口,follower 作为读的入口)
-
那多重影分身之术有什么缺点?
-
是一种工作方式
-
若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统
-
将不同的业务分布在不同的地方
-
宏观层面:多个功能模块糅合在一起的系统进行服务拆分,来解耦服务间的调用。
-
微观层面:将模块提供的服务分布到不同的机器或容器里,来扩大服务力度。
-
任何事物有阴必有阳,那分布式又会带来哪些问题呢
-
需要更多优质人才懂分布式,人力成本增加
-
架构设计变得异常复杂,学习成本高
-
运维部署和维护成本显著增加
-
多服务间链路变长,开发排查问题难度加大
-
环境高可靠性问题
-
数据幂等性问题
-
数据的顺序问题
-
等等
-
一致性(Consistency),所有节点访问同一份最新的数据副本。
-
可用性(Availability),每次请求都能获取到非错的响应,但不保证获取的数据为最新数据
-
分区容错性(Partition tolerance),不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在 C 和 A 之间做出选择
-
基本可用 :分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。如电商网址交易付款出现问题来,商品依然可以正常浏览。
-
软状态:由于不要求强一致性,所以BASE允许系统中存在中间状态(也叫软状态),这个状态不影响系统可用性,如订单中的“支付中”、“数据同步中”等状态,待数据最终一致后状态改为“成功”状态。
-
最终一致性:最终一致是指的经过一段时间后,所有节点数据都将会达到一致。如订单的“支付中”状态,最终会变为“支付成功”或者“支付失败”,使订单状态与实际交易结果达成一致,但需要一定时间的延迟、等待。
-
生产者发送每条数据时,增加一个全局唯一 id,类似订单 id。每次消费时,先去 Redis 查下是否有这个 id,如果没有,则进行正常处理消息,且将 id 存到 Redis。如果查到有这个 id,说明之前消费过,则不要进行重复处理这条消息。
-
不同业务场景,可能会有不同的幂等性方案,大家选择合适的即可,上面的几种方案只是提供常见的解决思路。
-
事务机制(不推荐,异步方式),对于 RabbitMQ 来说,生产者发送数据之前开启 RabbitMQ 的事务机制channel.txselect ,如果消息没有进队列,则生产者受到异常报错,并进行回滚 channel.txRollback,然后重试发送消息;如果收到了消息,则可以提交事务 channel.txCommit。但这是一个同步的操作,会影响性能。
-
confirm 机制(推荐,异步方式),我们可以采用另外一种模式:confirm 模式来解决同步机制的性能问题。每次生产者发送的消息都会分配一个唯一的 id,如果写入到了 RabbitMQ 队列中,则 RabbitMQ 会回传一个 ack 消息,说明这个消息接收成功。如果 RabbitMQ 没能处理这个消息,则回调 nack 接口。说明需要重试发送消息;也可以自定义超时时间 + 消息 id 来实现超时等待后重试机制。但可能出现的问题是调用 ack 接口时失败了,所以会出现消息被发送两次的问题,这个时候就需要保证消费者消费消息的幂等性。
-
事务机制是同步的,提交事务后悔被阻塞直到提交事务完成后。
-
confirm 模式异步接收通知,但可能接收不到通知。需要考虑接收不到通知的场景。
-
创建 Queue 的时候将其设置为持久化。这个地方没搞懂,欢迎探讨解答。
-
发送消息的时候将消息的 deliveryMode 设置为 2。
-
开启生产者 confirm 模式,可以重试发送消息。
-
关闭 RabbitMQ 的自动 ack,每次生产者将消息写入消息队列后,就自动回传一个 ack 给生产者。
-
消费者处理完消息再主动 ack,告诉消息队列我处理完了。
-
给 topic 设置 replication.factor 参数,值必须大于 1,要求每个 partition 必须有至少 2 个副本。
-
给 kafka 服务端设置 min.insyc.replicas 必须大于 1,表示一个 leader 至少一个 follower 还跟自己保持联系。
-
生产者向消息队列按照顺序发送了 2 条消息,消息1:增加数据 A,消息2:删除数据 A。
-
期望结果:数据 A 被删除。
-
但是如果有两个消费者,消费顺序是:消息2、消息 1。则最后结果是增加了数据 A。
-
将 Queue 进行拆分,创建多个内存 Queue,消息 1 和 消息 2 进入同一个 Queue。
-
创建多个消费者,每一个消费者对应一个 Queue。
-
创建了 topic,有 3 个 partition。
-
创建一条订单记录,订单 id 作为 key,订单相关的消息都丢到同一个 partition 中,同一个生产者创建的消息,顺序是正确的。
-
为了快速消费消息,会创建多个消费者去处理消息,而为了提高效率,每个消费者可能会创建多个线程来并行的去拿消息及处理消息,处理消息的顺序可能就乱序了。
-
解决方案和 RabbitMQ 类似,利用多个 内存 Queue,每个线程消费 1个 Queue。
-
具有相同 key 的消息 进同一个 Queue。
-
修复代码层面消费者的问题,确保后续消费速度恢复或尽可能加快消费的速度。
-
停掉现有的消费者。
-
临时建立好原先 5 倍的 Queue 数量。
-
临时建立好原先 5 倍数量的 消费者。
-
将堆积的消息全部转入临时的 Queue,消费者来消费这些 Queue。
-
准备好批量重导的程序
-
手动将消息闲时批量重导
-
判断哪些是无用的消息,RabbitMQ 可以进行 Purge Message 操作。
-
如果是有用的消息,则需要将消息快速消费,将消息里面的内容转存到数据库。
-
准备好程序将转存在数据库中的消息再次重导到消息队列。
-
闲时重导消息到消息队列。
-
英文名:sentinel,中文名:哨兵。
-
集群监控:负责主副进程的正常工作。
-
消息通知:负责将故障信息报警给运维人员。
-
故障转移:负责将主节点转移到备用节点上。
-
配置中心:通知客户端更新主节点地址。
-
分布式:有多个哨兵分布在每个主备节点上,互相协同工作。
-
分布式选举:需要大部分哨兵都同意,才能进行主备切换。
-
高可用:即使部分哨兵节点宕机了,哨兵集群还是能正常工作。
-
配置 min-slaves-to-write 1,表示至少有一个备用节点。
-
配置 min-slaves-max-lag 10,表示数据复制和同步的延迟不能超过 10 秒。最多丢失 10 秒的数据。
-
分库:因一个数据库支持的最高并发访问数是有限的,可以将一个数据库的数据拆分到多个库中,来增加最高并发访问数。
-
分表:因一张表的数据量太大,用索引来查询数据都搞不定了,所以可以将一张表的数据拆分到多张表,查询时,只用查拆分后的某一张表,SQL 语句的查询性能得到提升。
-
水平拆分:把一个表的数据拆分到多个数据库,每个数据库中的表结构不变。用多个库抗更高的并发。比如订单表每个月有500万条数据累计,每个月都可以进行水平拆分,将上个月的数据放到另外一个数据库。
-
垂直拆分:把一个有很多字段的表,拆分成多张表到同一个库或多个库上面。高频访问字段放到一张表,低频访问的字段放到另外一张表。利用数据库缓存来缓存高频访问的行数据。比如将一张很多字段的订单表拆分成几张表分别存不同的字段(可以有冗余字段)。
-
根据租户来分库、分表。
-
利用时间范围来分库、分表。
-
利用 ID 取模来分库、分表。
-
双写迁移方案:迁移时,新数据的增删改操作在新库和老库都做一遍。
-
使用分库分表工具 Sharding-jdbc 来完成分库分表的累活。
-
使用程序来对比两个库的数据是否一致,直到数据一致。
-
依然存在单表数据量过大的问题。
-
部分表无法关联查询,只能通过接口聚合方式解决,提升了开发的复杂度。
-
分布式事处理复杂。
-
跨库的关联查询性能差。
-
数据多次扩容和维护量大。
-
跨分片的事务一致性难以保证。
-
如果要做分库分表,则必须得考虑表主键 ID 是全局唯一的,比如有一张订单表,被分到 A 库和 B 库。如果 两张订单表都是从 1 开始递增,那查询订单数据时就错乱了,很多订单 ID 都是重复的,而这些订单其实不是同一个订单。
-
分库的一个期望结果就是将访问数据的次数分摊到其他库,有些场景是需要均匀分摊的,那么数据插入到多个数据库的时候就需要交替生成唯一的 ID 来保证请求均匀分摊到所有数据库。
-
全局唯一性
-
趋势递增
-
单调递增
-
信息安全
-
数据库自增 ID。每个数据库每增加一条记录,自己的 ID 自增 1。
缺点:多个库的 ID 可能重复,这个方案可以直接否掉了,不适合分库分表后的 ID 生成;信息不安全。
-
适用 UUID 唯一 ID。
缺点:UUID 太长、占用空间大;不具有有序性,作为主键时,在写入数据时,不能产生有顺序的 append 操作,只能进行 insert 操作,导致读取整个 B+ 树节点到内存,插入记录后将整个节点写回磁盘,当记录占用空间很大的时候,性能很差。
-
获取系统当前时间作为唯一 ID。
缺点:高并发时,1 ms内可能有多个相同的 ID;信息不安全。
-
Twitter 的 snowflake(雪花算法):Twitter 开源的分布式 id 生成算法,64 位的 long 型的 id,分为 4 部分:
-
1 bit:不用,统一为 0
-
41 bits:毫秒时间戳,可以表示 69 年的时间。
-
10 bits:5 bits 代表机房 id,5 个 bits 代表机器 id。最多代表 32 个机房,每个机房最多代表 32 台机器。
-
12 bits:同一毫秒内的 id,最多 4096 个不同 id,自增模式
-
百度的 UIDGenerator 算法。
-
基于 Snowflake 的优化算法。
-
借用未来时间和双 Buffer 来解决时间回拨与生成性能等问题,同时结合 MySQL 进行 ID 分配。
优点:毫秒数在高位,自增序列在低位,整个ID都是趋势递增的;不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的;可以根据自身业务特性分配bit位,非常灵活。
缺点:强依赖机器时钟,如果机器上时钟回拨(可以搜索 2017 年闰秒 7:59:60),会导致发号重复或者服务会处于不可用状态。
-
美团的 Leaf-Snowflake 算法。
-
为什么叫 Leaf(叶子):来自数学家莱布尼茨的一句话:“世界上没有两片相同的树叶”,也就是说这个算法生成的 ID 是唯一的。
-
获取 id 是通过代理服务访问数据库获取一批 id(号段)。
-
双缓冲:当前一批的 id 使用 10% 时,再访问数据库获取新的一批 id 缓存起来,等上批的 id 用完后直接用。
优点:Leaf 服务可以很方便的线性扩展,性能完全能够支撑大多数业务场景;ID 号码是趋势递增的 8byte 的 64 位数字,满足上述数据库存储的主键要求;容灾性高:Leaf 服务内部有号段缓存,即使 DB 宕机,短时间内 Leaf 仍能正常对外提供服务;可以自定义 max_id 的大小,非常方便业务从原有的 ID 方式上迁移过来;即使 DB 宕机,Leaf 仍能持续发号一段时间。偶尔的网络抖动不会影响下个号段的更新。
缺点:ID号码不够随机,能够泄露发号数量的信息,不太安全。
-
XA 方案(两阶段提交方案)
-
TCC 方案(try、confirm、cancel)
-
SAGA 方案
-
可靠消息最终一致性方案
-
最大努力通知方案
-
事务管理器负责协调多个数据库的事务,先问问各个数据库准备好了吗?如果准备好了,则在数据库执行操作,如果任一数据库没有准备,则回滚事务。
-
适合单体应用,不适合微服务架构。因为每个服务只能访问自己的数据库,不允许交叉访问其他微服务的数据库。
-
Try 阶段:对各个服务的资源做检测以及对资源进行锁定或者预留。
-
Confirm 阶段:各个服务中执行实际的操作。
-
Cancel 阶段:如果任何一个服务的业务方法执行出错,需要将之前操作成功的步骤进行回滚。
-
跟支付、交易打交道,必须保证资金正确的场景。
-
对于一致性要求高。
-
但因为要写很多补偿逻辑的代码,且不易维护,所以其他场景建议不要这么做。
-
业务流程中的每个步骤若有一个失败了,则补偿前面操作成功的步骤。
-
业务流程长、业务流程多。
-
参与者包含其他公司或遗留系统服务。
-
第一个阶段提交本地事务、无锁、高性能。
-
参与者可异步执行、高吞吐。
-
补偿服务易于实现。
-
不保证事务的隔离性。
-
利用消息中间件 RocketMQ 来实现消息事务。
-
第一步:A 系统发送一个消息到 MQ,MQ 将消息状态标记为 prepared(预备状态,半消息),该消息无法被订阅。
-
第二步:MQ 响应 A 系统,告诉 A 系统已经接收到消息了。
-
第三步:A 系统执行本地事务。
-
第四步:若 A 系统执行本地事务成功,将 prepared 消息改为 commit(提交事务消息),B 系统就可以订阅到消息了。
-
第五步:MQ 也会定时轮询所有 prepared的消息,回调 A 系统,让 A 系统告诉 MQ 本地事务处理得怎么样了,是继续等待还是回滚。
-
第六步:A 系统检查本地事务的执行结果。
-
第七步:若 A 系统执行本地事务失败,则 MQ 收到 Rollback 信号,丢弃消息。若执行本地事务成功,则 MQ 收到 Commit 信号。
-
B 系统收到消息后,开始执行本地事务,如果执行失败,则自动不断重试直到成功。或 B 系统采取回滚的方式,同时要通过其他方式通知 A 系统也进行回滚。
-
B 系统需要保证幂等性。
-
系统 A 本地事务执行完之后,发送消息到 MQ。
-
MQ 将消息持久化。
-
系统 B 如果执行本地事务失败,则最大努力服务会定时尝试重新调用系统 B,尽自己最大的努力让系统 B 重试,重试多次后,还是不行就只能放弃了。转到开发人员去排查以及后续人工补偿。
-
跟支付、交易打交道,优先 TCC。
-
大型系统,但要求不那么严格,考虑 消息事务或 SAGA 方案。
-
单体应用,建议 XA 两阶段提交就可以了。
-
最大努力通知方案建议都加上,毕竟不可能一出问题就交给开发排查,先重试几次看能不能成功。
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论