这三年被分布式坑惨了,曝光十大坑

Posted 悟空聊架构

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了这三年被分布式坑惨了,曝光十大坑相关的知识,希望对你有一定的参考价值。

节点挂了。

  • Kafka的 Leader 选举机制,如果某个节点挂了,会从 follower 中重新选举一个 leader 出来。(leader 作为写数据的入口,follower 作为读的入口)
  • 多重影分身之术有什么缺点?

  • 会消耗大量的查克拉。分布式系统同样具有这个问题,需要几倍的资源来支持。
  • 定理和 Base 理论,这里给不知道的同学做一个扫盲。

    Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent(最终一致性)三个短语的缩写。BASE 理论是对 CAPAP 的一个扩展,通过牺牲强一致性来获得可用性,当出现故障允许部分不可用但要保证核心功能可用,允许数据在一段时间内是不一致的,但最终达到一致状态。满足 BASE 理论的事务,我们称之为柔性事务

  • 基本可用 : 分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。如电商网址交易付款出现问题来,商品依然可以正常浏览。
  • 软状态: 由于不要求强一致性,所以BASE允许系统中存在中间状态(也叫软状态),这个状态不影响系统可用性,如订单中的“支付中”、“数据同步中”等状态,待数据最终一致后状态改为“成功”状态。
  • 最终一致性: 最终一致是指的经过一段时间后,所有节点数据都将会达到一致。如订单的“支付中”状态,最终会变为“支付成功”或者“支付失败”,使订单状态与实际交易结果达成一致,但需要一定时间的延迟、等待。
  • 消息队列中间件都有可能出现消息重复消费问题。这种问题并不是 MQ 自己保证的,而是需要开发人员来保证。

    这几款消息队列中间都是是全球最牛的分布式消息队列,那肯定考虑到了消息的幂等性。我们以 Kafka 为例,看看 Kafka 是怎么保证消息队列的幂等性。

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

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

    如下图所示,队列中的数据 A、B、C,对应的偏移量分别为 100、101、102,都被消费者消费了,但是只有数据 A 的偏移量 100 提交成功,另外 2 个偏移量因系统重启而导致未及时提交。

    系统重启,偏移量未提交

    重启后,消费者又是拿偏移量 100 以后的数据,从偏移量 101 开始拿消息。所以数据 B 和数据 C 被重复消息。

    如下图所示:

    重启后,重复消费消息
    操作天然幂等性,所以不用考虑 Redis 写数据的问题。
  • 其他场景方案
  • 生产者发送每条数据时,增加一个全局唯一 id,类似订单 id。每次消费时,先去 Redis 查下是否有这个 id,如果没有,则进行正常处理消息,且将 id 存到 Redis。如果查到有这个 id,说明之前消费过,则不要进行重复处理这条消息。
  • 不同业务场景,可能会有不同的幂等性方案,大家选择合适的即可,上面的几种方案只是提供常见的解决思路。
  • 坑:消息丢失会带来什么问题?如果是订单下单、支付结果通知、扣费相关的消息丢失,则可能造成财务损失,如果量很大,就会给甲方带来巨大损失。

    那消息队列是否能保证消息不丢失呢?答案:否。主要有三种场景会导致消息丢失。

    消息队列之消息丢失
    ,如果消息没有进队列,则生产者受到异常报错,并进行回滚 channel.txRollback,然后重试发送消息;如果收到了消息,则可以提交事务 channel.txCommit。但这是一个同步的操作,会影响性能。

  • confirm 机制(推荐,异步方式)
  • 我们可以采用另外一种模式:confirm 模式来解决同步机制的性能问题。每次生产者发送的消息都会分配一个唯一的 id,如果写入到了 RabbitMQ 队列中,则 RabbitMQ 会回传一个 ack 消息,说明这个消息接收成功。如果 RabbitMQ 没能处理这个消息,则回调 nack 接口。说明需要重试发送消息。

    也可以自定义超时时间 + 消息 id 来实现超时等待后重试机制。但可能出现的问题是调用 ack 接口时失败了,所以会出现消息被发送两次的问题,这个时候就需要保证消费者消费消息的幂等性。

    confirm 模式的区别:的时候将其设置为持久化。
  • 发送消息的时候将消息的 deliveryMode 设置为 2 。
  • 开启生产者 confirm 模式,可以重试发送消息。
  • 给生产者。
  • 消费者处理完消息再主动 ack,告诉消息队列我处理完了。
  • 问题: 那这种主动 ack 有什么漏洞了?如果 主动 ack 的时候挂了,怎么办?

    则可能会被再次消费,这个时候就需要幂等处理了。

    问题: 如果这条消息一直被重复消费怎么办?

    则需要有加上重试次数的监测,如果超过一定次数则将消息丢失,记录到异常表或发送异常通知给值班人员。

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

    解决方案

  • 给 topic 设置 replication.factor 参数,值必须大于 1,要求每个 partition 必须有至少 2 个副本。
  • 给 kafka 服务端设置 min.insyc.replicas 必须大于 1,表示一个 leader 至少一个 follower 还跟自己保持联系。
  • 坑: 用户先下单成功,然后取消订单,如果顺序颠倒,则最后数据库里面会有一条下单成功的订单。

    RabbitMQ 场景:

  • 生产者向消息队列按照顺序发送了 2 条消息,消息1:增加数据 A,消息2:删除数据 A。
  • 期望结果:数据 A 被删除。
  • 但是如果有两个消费者,消费顺序是:消息2、消息 1。则最后结果是增加了数据 A。
  • RabbitMQ消息乱序场景
    RabbitMQ 消息乱序场景

    RabbitMQ 解决方案:

  • 将 Queue 进行拆分,创建多个内存 Queue,消息 1 和 消息 2 进入同一个 Queue。
  • 创建多个消费者,每一个消费者对应一个 Queue。
  • RabbitMQ 解决方案

    Kafka 场景:

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

    Kafka 解决方案:

  • 解决方案和 RabbitMQ 类似,利用多个 内存 Queue,每个线程消费 1个 Queue。
  • 具有相同 key 的消息 进同一个 Queue。
  • Kafka 消息乱序解决方案

    消息积压:消息队列里面有很多消息来不及消费。

    场景 1: 消费端出了问题,比如消费者都挂了,没有消费者来消费了,导致消息在队列里面不断积压。

    场景 2: 消费端出了问题,比如消费者消费的速度太慢了,导致消息不断积压。

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

    消息队列之消息积压

    解决方案:解铃还须系铃人

  • 修复代码层面消费者的问题,确保后续消费速度恢复或尽可能加快消费的速度。
  • 停掉现有的消费者。
  • 临时建立好原先 5 倍的 Queue 数量。
  • 临时建立好原先 5 倍数量的 消费者。
  • 将堆积的消息全部转入临时的 Queue,消费者来消费这些 Queue。
  • 消息积压解决方案

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

    消息过期失效

    解决方案:

  • 准备好批量重导的程序
  • 手动将消息闲时批量重导
  • 消息过期失效解决方案

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

    解决方案:

  • 判断哪些是无用的消息,RabbitMQ 可以进行 Purge Message 操作。
  • 如果是有用的消息,则需要将消息快速消费,将消息里面的内容转存到数据库。
  • 准备好程序将转存在数据库中的消息再次重导到消息队列。
  • 闲时重导消息到消息队列。
  • 当主节点发生故障时,需要进行主备切换,可能会导致数据丢失。

    因一个数据库支持的最高并发访问数是有限的,可以将一个数据库的数据拆分到多个库中,来增加最高并发访问数。

  • 分表: 因一张表的数据量太大,用索引来查询数据都搞不定了,所以可以将一张表的数据拆分到多张表,查询时,只用查拆分后的某一张表,SQL 语句的查询性能得到提升。

  • 分库分表优势:分库分表后,承受的并发增加了多倍;磁盘使用率大大降低;单表数据量减少,SQL 执行效率明显提升。

  • 水平拆分: 把一个表的数据拆分到多个数据库,每个数据库中的表结构不变。用多个库抗更高的并发。比如订单表每个月有500万条数据累计,每个月都可以进行水平拆分,将上个月的数据放到另外一个数据库。

  • 垂直拆分: 把一个有很多字段的表,拆分成多张表到同一个库或多个库上面。高频访问字段放到一张表,低频访问的字段放到另外一张表。利用数据库缓存来缓存高频访问的行数据。比如将一张很多字段的订单表拆分成几张表分别存不同的字段(可以有冗余字段)。

  • 分库、分表的方式:

  • 根据租户来分库、分表。
  • 利用时间范围来分库、分表。
  • 利用 ID 取模来分库、分表。
  • 坑:分库分表是一个运维层面需要做的事情,有时会采取凌晨宕机开始升级。可能熬夜到天亮,结果升级失败,则需要回滚,其实对技术团队都是一种煎熬。

    分库分表看似光鲜亮丽,但分库分表会引入什么新的问题呢?

    唯一 ID 的生成方式有 n 种,各有各的用途,别用错了。

    唯一 ID。

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

  • 高并发时,1 ms内可能有多个相同的 ID。
  • 信息不安全
  • 缺点
  • Twitter 的 snowflake(雪花算法):Twitter 开源的分布式 id 生成算法,64 位的 long 型的 id,分为 4 部分

    snowflake 算法
  • 基本原理和优缺点:

  • 算法。

    UIDGenerator 算法
  • 基于 Snowflake 的优化算法。
  • 借用未来时间和双 Buffer 来解决时间回拨与生成性能等问题,同时结合 mysql 进行 ID 分配。
  • 优点:解决了时间回拨和生成性能问题。
  • 缺点:依赖 算法。来实现消息事务。
  • 第一步: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 系统需要保证幂等性。
  • 分布式还有很多坑,这篇只是一个小小的总结,从这些坑中,我们也知道分布式有它的优势也有它的劣势,那到底该不该用分布式,完全取决于业务、时间、成本以及开发团队的综合实力。后续我会继续分享分布式中的一些底层原理,当然也少不了分享一些避坑指南。

    参考资料:

    美团的 Leaf-Snowflake 算法。
    百度的 UIDGenerator 算法。
    Advanced-Java

    回复 悟空 领取优质资料。

    「转发->在看->点赞->收藏->评论!!!」  是对我最大的支持!


    精彩内容 TOP 5

  • 和JavaGuide作者面基是种什么体验?

  • 干货 | 45张图庖丁解牛18种Queue,你知道几种?

  • 全网最细 | 21张图带你领略集合的线程不安全

  • 程序员深夜惨遭老婆鄙视,原因竟是CAS原理太简单?

  • 5000字 | 24张图带你彻底理解Java中的21种锁


  • 我是悟空,努力变强,变身超级赛亚人!


    点亮

    我被线程坑惨了!线程间的通信有毒!!!

    在java中,线程间的通信可以使用waitnotifynotifyAll来进行控制。从名字就可以看出来这3个方法都是跟多线程相关的,但是可能让你感到吃惊的是:这3个方法并不是Thread类或者是Runnable接口的方法,而是Object类的3个本地方法。

    其实要理解这一点也并不难,调用一个Object的wait与notify/notifyAll的时候,必须保证调用代码对该Object是同步的,也就是说必须在作用等同于synchronized(obj){......}的内部才能够去调用obj的wait与notify/notifyAll三个方法,否则就会报错:

    java.lang.IllegalMonitorStateException:current thread not owner
    也就是说,在调用这3个方法的时候,当前线程必须获得这个对象的锁,那么这3个方法就是和对象锁相关的,所以是属于Object的方法而不是Thread,因为不是每个对象都是Thread。所以我们在理解wait、notify、notifyAll之前,先要了解以下对象锁。

    多个线程都持有同一个对象的时候,如果都要进入synchronized(obj){…}的内部,就必须拿到这个对象的锁,synchronized的机制保证了同一时间最多只能有1个线程拿到了对象的锁,如下图:


    下面我们来看一下这3个方法的作用:

    • wait:线程自动释放其占有的对象锁,并等待notify
    • notify:唤醒一个正在wait当前对象锁的线程,并让它拿到对象锁
    • notifyAll:唤醒所有正在wait前对象锁的线程

    notify和notifyAll的最主要的区别是:notify只是唤醒一个正在wait当前对象锁的线程,而notifyAll唤醒所有。值得注意的是:notify是本地方法,具体唤醒哪一个线程由虚拟机控制;notifyAll后并不是所有的线程都能马上往下执行,它们只是跳出了wait状态,接下来它们还会是竞争对象锁。

    下面通过一个常用生产者、消费者的例子来说明。

    消息实体类:

    package com.podongfeng;
    
    /**
     * Title: Message.class<br>
     * Description: 消息实体<br>
     * Create DateTime: 2016年04月17日 下午1:27 <br>
     *
     * @author podongfeng
     *///加入Java开发交流君样:756584822一起吹水聊天
    public class Message {
    }
    

    生产者:

    package com.podongfeng;
    
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * Title: Producer.class<br>
     * Description: 消息生产者<br>
     * Create DateTime: 2016年04月17日 下午1:28 <br>
     *
     * @author podongfeng
     */
    public class Producer extends Thread {
    
        List<Message> msgList = new ArrayList<>();
    
        @Override public void run() {
            try {
                while (true) {
                    Thread.sleep(3000);
                    Message msg = new Message();
                    synchronized(msgList) {
                        msgList.add(msg);
                        msgList.notify(); //这里只能是notify而不能是notifyAll,否则remove(0)会报java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }//加入Java开发交流君样:756584822一起吹水聊天
    
        public Message waitMsg() {
            synchronized(msgList) {
                if(msgList.size() == 0) {
                    try {
                        msgList.wait();
                    } catch(InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                return msgList.remove(0);
            }
        }//加入Java开发交流君样:756584822一起吹水聊天
    }
    

    消费者:

    package com.podongfeng;
    
    /**
     * Title: Consumer.class<br>
     * Description: 消息消费者<br>
     * Create DateTime: 2016年04月17日 下午1:28 <br>
     *
     * @author podongfeng
     */
    public class Consumer extends Thread {
    
        private Producer producer;
    
        public Consumer(String name, Producer producer) {
            super(name);
            this.producer = producer;
        }
    //加入Java开发交流君样:756584822一起吹水聊天
        @Override public void run() {
            while (true) {
                Message msg = producer.waitMsg();
                System.out.println("Consumer " + getName() + " get a msg");
            }
        }
    
        public static void main(String[] args) {
            Producer p = new Producer();
            p.start();
            new Consumer("Consumer1", p).start();
            new Consumer("Consumer2", p).start();
            new Consumer("Consumer3", p).start();
        }
    }//加入Java开发交流君样:756584822一起吹水聊天
    

    消费者线程调用waitMsg去获取一个消息实体,如果msgList为空,则线程进入wait状态;生产这线程每隔3秒钟生产出体格msg实体并放入msgList列表,完成后,调用notify唤醒一个消费者线程去消费。

    最后再次提醒注意:

    wait、notify、notifyAll并不是Thread类或者是Runnable接口的方法,而是Object类的3个本地方法。
    在调用这3个方法的时候,当前线程必须获得这个对象的锁

    生命不止坚毅鱼奋斗,有梦想才是有意义的追求

    给大家推荐一个免费的学习交流群:

    最后,祝大家早日学有所成,拿到满意offer,快速升职加薪,走上人生巅峰。

    Java开发交流君样:756584822

    以上是关于这三年被分布式坑惨了,曝光十大坑的主要内容,如果未能解决你的问题,请参考以下文章

    又双叕获奖 鸿雁连续三年被评为“十大全屋智能家居品牌”

    又双叕获奖 鸿雁连续三年被评为“十大全屋智能家居品牌”

    我被线程坑惨了!线程间的通信有毒!!!

    List中remove()方法的陷阱,被坑惨了!

    苹果的策略获利丰厚,却坑惨了黄牛,炒作iPhone14赔惨了

    被苹果坑惨了

    (c)2006-2024 SYSTEM All Rights Reserved IT常识