分布式之坑

分布式的理解

  • 是一种工作方式
  • 若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统
  • 将不同的业务分不到不同的地方

分布式的优势

  • 宏观层面:使得多个模块糅合在一起的系统进行服务拆分,解耦服务间的调用。
  • 微观层面:将模块提供的服务分布到不同的机器或容器中,扩大服务力度。

分布式的问题

  • 使得成本增加
  • 多服务间链路变长,开发排查难度加大
  • 环境高可靠性问题
  • 数据幂等性问题
  • 数据的顺序问题

1、分布式的基础

1.1 CAP定理

  • 一致性(consistency):在分布式系统的所有数据备份,在同一时刻是否同样的值。(所有节点访问同一份最新的数据备份)

  • 可用性(Availability):在集群中一部分节点故障后,集群整体能否还能响应客户端的读写请求。

  • 分区容忍性(Partition tolerance):分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性就意味着发生了分区,必须就当前操作在CA之间做出选择。

  • 由于当前的网络硬件肯定会出现延迟丢包等问题,所以分区容忍性是必须要实现的。只能在一致性和可用性之间做出选择。

1.2 BASE理论

  • Basically Available(基本可用):分布式系统出现故障时,允许损失部分可用功能,保证核心功能可用。如电商网址交易付款出现问题时,商品依然可以正常浏览。
  • soft state(软状态):由于不要求强一致性,所以BASE允许系统中存在中间状态(也叫做软状态),这个状态不影响系统可用性。如订单中的“支付中”、“数据同步中”等状态,带数据最终一致后状态改为“成功状态”。
  • eventually consistent(最终一致性):经过一段时间后,所有节点数据都将会达到一致。如订单的“支付中状态”,最终会变为“支付成功”或“支付失败”,使订单状态与实际交易结果达成一致,但需要一定时间的延迟、等待。
  • BASE理论是对CAP理论中AP的一个扩展。通过牺牲强一致性来获得可用性,当出现故障允许部分不可用但要保证核心功能可用,允许数据在一段时间内是不一致的,但最终达到一致状态。

2、分布式消息队列的坑

消息队列如何做到分布式
将消息队列里面的消息分摊到多个节点,所有节点的消息队列之和就包含了所有消息。

2.1 非幂等的坑

幂等性的概念

幂等性就是无论操作多少次都和第一次的操作结果一致。如果消息被消费多次,很有可能造成数据的不一致。要是消息不可避免地被消费多次,但是我们开发人员能通过技术手段保证数据的前后一致性,也是可以接受的。如Java的ABA问题(可以参考这里)。

场景分析
RabbitMQ、RocketMQ、Kafka消息队列中间件都可能出现重复消费问题。这种问题需要开发人员来保证。

kafka是怎么保证消息队列的幂等性

Kafka 有一个 偏移量 的概念,代表着消息的序号,每条消息写到消息队列都会有一个偏移量,消费者消费了数据之后,每过一段固定的时间,就会把消费过的消息的偏移量提交一下,表示已经消费过了,下次消费就从偏移量后面开始消费。

坑:当消费完消息后,还没来得及提交偏移量,系统就被关机了,那么未提交偏移量的消息则会再次被消费。

如下图所示,队列中的数据 A、B、C,对应的偏移量分别为 100、101、102,都被消费者消费了,但是只有数据 A 的偏移量 100 提交成功,另外 2 个偏移量因系统重启而导致未及时提交。
在这里插入图片描述
重启后,消费者又是拿偏移量 100 以后的数据,从偏移量 101 开始拿消息。所以数据 B 和数据 C 被重复消息。

在这里插入图片描述
避坑指南

  • 微信支付结果通知场景
    微信官方文档上提到微信支付通知结果可能会推送多次,需要开发者自行保证幂等性。第一次我们可以直接修改订单状态(如支付中 -> 支付成功),第二次就根据订单状态来判断,如果不是支付中,则不进行订单处理逻辑。
  • 插入数据库场景
    每次插入数据时,先检查下数据库中是否有这条数据的主键 id,如果有,则进行更新操作。
  • 写 Redis 场景
    Redis 的 Set 操作天然幂等性,所以不用考虑 Redis 写数据的问题。
  • 其他场景方案
    生产者发送每条数据时,增加一个全局唯一 id,类似订单 id。每次消费时,先去 Redis 查下是否有这个 id,如果没有,则进行正常处理消息,且将 id 存到 Redis。如果查到有这个 id,说明之前消费过,则不要进行重复处理这条消息。
    不同业务场景,可能会有不同的幂等性方案,大家选择合适的即可,上面的几种方案只是提供常见的解决思路。

2.2 消息丢失的坑

生产者存放消息的过程中丢失消息

  • 事务机制(不推荐,同步方式):对于 RabbitMQ 来说,生产者发送数据之前开启 RabbitMQ 的事务机制channel.txselect ,如果消息没有进队列,则生产者受到异常报错,并进行回滚 channel.txRollback,然后重试发送消息;如果收到了消息,则可以提交事务 channel.txCommit。但这是一个同步的操作,会影响性能。
  • confirm机制(推荐,异步方式):confirm 模式来解决同步机制的性能问题。每次生产者发送的消息都会分配一个唯一的 id,如果写入到了 RabbitMQ 队列中,则 RabbitMQ 会回传一个 ack 消息,说明这个消息接收成功。如果 RabbitMQ 没能处理这个消息,则回调 nack 接口。说明需要重试发送消息。

消息队列丢失消息
消息队列的消息可以放到内存中,或将内存中的消息转到硬盘(比如数据库)中,一般都是内存和硬盘中都存有消息。如果只是放在内存中,那么当机器重启了,消息就全部丢失了。如果是硬盘中,则可能存在一种极端情况,就是将内存中的数据转换到硬盘的期间中,消息队列出问题了,未能将消息持久化到硬盘。

  • 创建 Queue 的时候将其设置为持久化。这个地方没搞懂,欢迎探讨解答。
  • 发送消息的时候将消息的 deliveryMode 设置为 2 。
  • 开启生产者 confirm 模式,可以重试发送消息。

消费者丢失消息
消费者刚拿到数据,还没开始处理消息,结果进程因为异常退出了,消费者没有机会再次拿到消息。

  • 关闭RabbitMQ的自动ack,消费者处理完消息再主动ACK。
  • 主动ACK的时候挂了?可能会被再次消费,这个时候就需要幂等处理了。
  • 一直被重复消费?加上重试次数的监测,如果超过一定次数将消息丢失,记录异常。

kafka消息丢失
Kafka 的某个 broker(节点)宕机了,重新选举 leader (写入的节点)。如果 leader 挂了,follower 还有些数据未同步完,则 follower 成为 leader 后,消息队列会丢失一部分数据。

  • 每个分片至少有2个副本
  • 保证一个leader至少一个follower还跟自己保持联系。

2.3 消息乱序

生产者向消息队列按照顺序发送了 2 条消息,消息1:增加数据 A,消息2:删除数据 A。
期望结果:数据 A 被删除。
但是如果有两个消费者,消费顺序是:消息2、消息 1。则最后结果是增加了数据 A
RabbitMQ 解决方案:
将 Queue 进行拆分,创建多个内存 Queue,消息 1 和 消息 2 进入同一个 Queue。创建多个消费者,每一个消费者对应一个 Queue。
在这里插入图片描述
Kafka 场景

  • 创建了 topic,有 3 个 partition。
  • 创建一条订单记录,订单 id 作为 key,订单相关的消息都丢到同一个 partition 中,同一个生产者创建的消息,顺序是正确的。
  • 为了快速消费消息,会创建多个消费者去处理消息,而为了提高效率,每个消费者可能会创建多个线程来并行的去拿消息及处理消息,处理消息的顺序可能就乱序了。

在这里插入图片描述
解决方案和 RabbitMQ 类似,利用多个 内存 Queue,每个线程消费 1个 Queue。具有相同 key 的消息 进同一个 Queue。
在这里插入图片描述

2.4 消息积压

场景1:消费端出了问题,消费者都挂了,没有消费者来消费了。
场景2:消费端出了问题,消费者消费的速度太慢了,导致消息不断积压。

比如线上正在做订单活动,下单全部走消息队列,如果消息不断积压,订单都没有下单成功,那么将会损失很多交易。

  1. 修复代码层面消费者问题,确保后续消费速度恢复。
  2. 停掉现在的消费者。
  3. 临时建立原先5倍的Queue数量。
  4. 临时建立原先5倍的消费者数量
  5. 将堆积的消息全部转入临时Queue,消费者来消费这些queue。

2.5 消息过期失效

RabbitMQ 可以设置过期时间,如果消息超过一定的时间还没有被消费,则会被 RabbitMQ 给清理掉。消息就丢失了。

  • 准备好批量重导的程序。
  • 手动将消息闲时批量重导。

2.6 队列写满

当消息队列因消息积压导致的队列快写满,所以不能接收更多的消息了。生产者生产的消息被丢弃。

  1. 判断哪些是无用的消息
  2. 如果是有用的消息,需要将消息快速消费,将消息里面的内容转存到数据库。
  3. 准备好程序将数据库的消息再次重导到消息队列。
  4. 闲时重导消息到消息队列。

3、分布式缓存的坑

在高频访问数据库的场景中,我们会在业务层和数据层之间加入一套缓存机制,来分担数据库的访问压力,毕竟访问磁盘 I/O 的速度是很慢的。比如利用缓存来查数据,可能5ms就能搞定,而去查数据库可能需要 50 ms,差了一个数量级。而在高并发的情况下,数据库还有可能对数据进行加锁,导致访问数据库的速度更慢。
分布式缓存我们用的最多的就是 Redis了,它可以提供分布式缓存服务。

3.1 redis数据丢失的坑

异步复制数据导致的数据丢失
主节点使用异步方式将数据同步给备用节点的过程中,主节点宕机了,导致部分数据未同步到备用节点。而这个从节点又被选举为主节点,这时候就有部分数据丢失了。

脑裂导致的数据丢失
主节点所在机器脱离了集群网络,实际上自身还是运行着的。但哨兵选举出了备用节点作为主节点,这个时候就有两个主节点都在运行,相当于两个大脑在指挥这个集群干活,但到底听谁的呢?这个就是脑裂。
那怎么脑裂怎么会导致数据丢失呢?如果发生脑裂后,客户端还没来得及切换到新的主节点,连的还是第一个主节点,那么有些数据还是写入到了第一个主节点里面,新的主节点没有这些数据。那等到第一个主节点恢复后,会被作为备用节点连到集群环境,而且自身数据会被清空,重新从新的主节点复制数据。而新的主节点因没有客户端之前写入的数据,所以导致数据丢失了一部分。

  • 配置 min-slaves-to-write 1,表示至少有一个备用节点。
  • 配置 min-slaves-max-lag 10,表示数据复制和同步的延迟不能超过 10 秒。最多丢失 10 秒的数据
  • 注意:缓存雪崩、缓存穿透、缓存击穿并不是分布式所独有的,单机的时候也会出现。所以不在分布式的坑之列。

4、分库分表的坑

4.1 扩容存在的坑

  • 分库: 因一个数据库支持的最高并发访问数是有限的,可以将一个数据库的数据拆分到多个库中,来增加最高并发访问数。
  • 分表: 因一张表的数据量太大,用索引来查询数据都搞不定了,所以可以将一张表的数据拆分到多张表,查询时,只用查拆分后的某一张表,SQL 语句的查询性能得到提升。
  • 分库分表优势:分库分表后,承受的并发增加了多倍;磁盘使用率大大降低;单表数据量减少,SQL 执行效率明显提升。
  • 水平拆分: 把一个表的数据拆分到多个数据库,每个数据库中的表结构不变。用多个库抗更高的并发。比如订单表每个月有500万条数据累计,每个月都可以进行水平拆分,将上个月的数据放到另外一个数据库。
  • 垂直拆分: 把一个有很多字段的表,拆分成多张表到同一个库或多个库上面。高频访问字段放到一张表,低频访问的字段放到另外一张表。利用数据库缓存来缓存高频访问的行数据。比如将一张很多字段的订单表拆分成几张表分别存不同的字段(可以有冗余字段)。
  • 分库、分表的方式:
    • 根据租户来分库、分表。
    • 利用时间范围来分库、分表。
    • 利用 ID 取模来分库、分表。

垂直拆分带来的问题

  • 依然存在单表数据量过大的问题。
  • 部分表无法关联查询,只能通过接口聚合方式解决,提升了开发的复杂度。
  • 分布式事处理复杂。
    水平拆分带来的问题
  • 跨库的关联查询性能差。
  • 数据多次扩容和维护量大。
  • 跨分片的事务一致性难以保证。

4.2 唯一ID存在的坑

  • 如果要做分库分表,则必须得考虑表主键 ID 是全局唯一的,比如有一张订单表,被分到 A 库和 B 库。如果 两张订单表都是从 1 开始递增,那查询订单数据时就错乱了,很多订单 ID 都是重复的,而这些订单其实不是同一个订单。
  • 分库的一个期望结果就是将访问数据的次数分摊到其他库,有些场景是需要均匀分摊的,那么数据插入到多个数据库的时候就需要交替生成唯一的 ID 来保证请求均匀分摊到所有数据库。

生成唯一ID的方式
(1)数据库自增ID。

  • 多个库的ID可能重复。不适合分库分表后的ID生成。
  • 信息不安全。

(2)UUID

  • 太长,占用空间大
  • 不具有有序性,作为主键时,在写入数据时,不能产生有顺序的 append 操作,只能进行 insert 操作,导致读取整个 B+ 树节点到内存,插入记录后将整个节点写回磁盘,当记录占用空间很大的时候,性能很差。

(3)获取系统当前时间作为唯一ID

  • 高并发时,1ms内可能有多个相同的ID
  • 信息不安全

(4)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,自增模式

优点:

  • 毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。
  • 不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。
  • 可以根据自身业务特性分配bit位,非常灵活。

缺点:

  • 强依赖机器时钟,如果机器上时钟回拨(可以搜索 2017 年闰秒 7:59:60),会导致发号重复或者服务会处于不可用状态

(5)百度的UIDGenerator算法

  • 基于snowflake的优化算法
  • 借用未来时间和双 Buffer 来解决时间回拨与生成性能等问题,同时结合 MySQL 进行 ID 分配。

(6)美团的leaf-snowflake算法

  • 为什么叫 Leaf(叶子):来自数学家莱布尼茨的一句话:“世界上没有两片相同的树叶”,也就是说这个算法生成的 ID 是唯一的

5、分布式事务的坑

在分布式的世界中,存在着各个服务之间相互调用,链路可能很长,如果有任何一方执行出错,则需要回滚涉及到的其他服务的相关操作。比如订单服务下单成功,然后调用营销中心发券接口发了一张代金券,但是微信支付扣款失败,则需要退回发的那张券,且需要将订单状态改为异常订单。

分布式事务的几种主要方式

  • XA方案(两阶段提交方案)
    事务管理器负责协调多个数据库的事务,先问问各个数据库准备好了吗?如果准备好了,则在数据库执行操作,如果任一数据库没有准备,则回滚事务。
    适合单体应用,不适合微服务架构。因为每个服务只能访问自己的数据库,不允许交叉访问其他微服务的数据库。
    在这里插入图片描述

  • TCC(try、confim、cancel)

  • SAGA方案

  • 可靠消息最终一致性方案

  • 最大努力通知方案

猜你喜欢

转载自blog.csdn.net/qq_37935909/article/details/108812019