0
点赞
收藏
分享

微信扫一扫

分布式核心知识点

1. 分布式介绍:

分布式分为分布式缓存(Redis)、分布式锁(Redis 或 Zookeeper)、分布式服务(Dubbo 或 SpringCloud)、分布式服务协调(Zookeeper)、分布式消息队列(Kafka 、RabbitMq)、分布式 Session 、分布式事务、分布式搜索(Elasticsearch)等。不可能所有分布式内容都熟悉,一定要在某个领域有所专长。

2.分布式理论

分布式理论分为CAP,Base理论。

一.CAP理论

  1. 任何分布式系统都无法同时满足一致性(consistency),可用性(availibity),分区容错性(partition tolerance)这三项,最多只可同时满足其中的两项.
  2. zookeeper和Eureka做注册中心对比:
    2.1.zookeeper做注册中心是满足CP,
    2.2.Eurka是满足AP,
    2.3.nacos同时满足AP和CP

二.Base理论

Basically Available(基本可用)
       假设系统,出现了不可预知的故障,但还是能用, 可能会有性能或者功能上的影响,比如RT是	     10ms,变成50ms
       
Soft state(软状态)
      允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延时
    
Eventually consistent(最终一致性)
      系统能够保证在没有其他新的更新操作的情况下,数据最终一定能够达到一致的状态,因此所有客户端对系统的数据访问最终都能够获取到最新的值
  • 关于数据一致性
强一致:操作后的能立马一致且可以访问
弱一致:容忍部分或者全部访问不到
最终一致:弱一致性经过多一段时间后,都一致且正常

3.理解分布式事务?分布式事务的协议有哪些?

分布式事务是指会涉及到操作多个数据库的事务。目的是为了保证分布式系统中的数据一致性。分布式事务类型:二阶段提交 2PC ,三阶段提交 3PC。

 2PC :第一阶段:准备阶段(投票阶段)和第二阶段:提交阶段(执行阶段)。
 3PC :三个阶段:CanCommit 、PreCommit 、DoCommit。

4.分布式事务的解决方案有哪些?

分布式事务解决方案: TCC 、2PC和3PC 、事务消息。

5.TCC柔性事务的解决方案

什么是TCC柔性事务

  • 刚性事务:遵循ACID
  • 柔性事务:遵循BASE理论
  • TCC:
    • 将事务提交分为
      • Try:完成所有业务检查( 一致性 ) ,预留必须业务资源( 准隔离性 )
      • Confirm :对业务系统做确认提交,默认 Confirm阶段不会出错的 即只要Try成功,Confirm一定成功
      • Cancel : 业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放, 进行补偿性
    • TCC 事务和 2PC 的类似,Try为第一阶段,Confirm - Cancel为第二阶段,它对事务的提交/回滚是通过执行一段 confirm/cancel 业务逻辑来实现,并且也并没有全局事务来把控整个事务逻辑
    • TCC交互图图片来源于网络
  • 优点:
    • 它把事务运行过程分成 Try、Confirm/Cancel 两个阶段
    • 每个阶段由业务代码控制,这样事务的锁力度可以完全自由控制
    • 不存在资源阻塞的问题,每个方法都直接进行事务的提交
  • 缺点
    • 在业务层编写代码实现的两阶段提交,原本一个方法,现在却需要三个方法来支持
    • 对业务的侵入性很强,不能很好的复用
  • 注意:使用TCC时要注意Try - Confirm - Cancel 3个操作的幂等控制,由于网络原因或者重试操作都有可能导致这几个操作的重复执行

6.事务管理器宕掉了,怎么办

做冗余,设置多个事务管理器,一个宕掉了,其他的还可以用。

7.怎么保证分布式系统的幂等性

状态机制。版本号机制。

8.消息中间件如何解决消息丢失问题

  • 事务消息

    • 消息队列提供类似Open XA的分布式事务功能,通过消息队列事务消息能达到分布式事务的最终一致
  • 半事务消息

    • 暂不能投递的消息,发送方已经成功地将消息发送到了消息队列服务端,但是服务端未收到生产者对该消息的二次确认,此时该消息被标记成“暂不能投递”状态,处于该种状态下的消息即半事务消息。
  • 消息回查

    • 由于网络闪断、生产者应用重启等原因,导致某条事务消息的二次确认丢失,消息队列服务端通过扫描发现某条消息长期处于“半事务消息”时,需要主动向消息生产者询问该消息的最终状态(Commit或是Rollback),该询问过程即消息回查
  • 交互图(来源rocketmq官方文档)交互图(来源rocketmq官方文档)

  • 目前较为主流的MQ,比如ActiveMQ、RabbitMQ、Kafka、RocketMQ等,只有RocketMQ支持事务消息

    • 如果其他队列需要事务消息,可以开发个消息服务,自行实现半消息和回查功能
  • 好处

    • 事务消息不仅可以实现应用之间的解耦,又能保证数据的最终一致性
    • 同时将传统的大事务可以被拆分为小事务,能提升效率
    • 不会因为某一个关联应用的不可用导致整体回滚,从而最大限度保证核心系统的可用性
  • 缺点

    • 不能实时保证数据一致性
    • 极端情况下需要人工补偿,比如 假如生产者成功处理本地业务,消费者始终消费不成功

9.常见分布式事务解决方案概览

  • 常见分布式事务解决方案
    2PC 和 3PC
    两阶段提交, 基于XA协议

    • TCC
      Try、Confirm、Cancel
    • 事务消息
      • 最大努力通知型
  • 分布式事务分类

    • 刚性事务:遵循ACID
    • 柔性事务:遵循BASE理论
  • 分布式事务框架

    • TX-LCN:支持2PC、TCC等多种模式
      • https://github.com/codingapi/tx-lcn
      • 更新慢(个人感觉处于停滞状态)
    • Seata:支持 AT、TCC、SAGA 和 XA 多种模式
      • https://github.com/seata/seata
      • 背靠阿里,专门团队推广
      • 阿里云商业化产品GTS
        • https://www.aliyun.com/aliware/txc
    • RocketMq:自带事务消息解决分布式事务
      • https://github.com/apache/rocketmq

10.分布式事务常见核心概念

  • 前置知识

    • X/OpenDTP 事务模型
    是X/Open 这个组织定义的一套分布式事务的标准,也就是定义了规范和 API 接口,由各个厂商进行具体的实现
    
    DTP 是分布式事物处理(Distributed Transaction Processing)的简称
    
    • XA协议
    XA是由X/Open组织提出的分布式事务规范。
    
    XA规范主要定义了(全局)事务管理器(TM)和(局 部)资源管理器(RM)之间的接口
    
    主流的数据库产品都实现了XA接口,是一个双向的系统接口,在事务管理器以及多个资源管理器之间作为通信桥梁
    
    • JTA
    Java Transaction API,java根据XA规范提供的事务处理标准
    
    • AP
    application, 应用程序也就是业务层,微服务等
    
    • RM
    Resource Manager,资源管理器。一般是数据库,也可以是其他资源管理器,比如消息队列,文件系统
    
    • TM
    Transaction Manager ,事务管理器、事务协调者,负责接收来自用户程序(AP)发起的 XA 事务指令,并调度和协调参与事务的所有 RM(数据库),确保事务正确完成
    
  • 事务模型

在分布式系统中,每一个机器节点能够明确知道自己在进行事务操作过程中的 结果是成功还是失败,但无法直接获取到其他分布式节点的操作结果

当一个事务操作跨越多个分布式节点的时候,为了保持事务处理的 ACID 特性,

需要引入一个“协调者”(TM)来统一调度所有分布式节点的执行逻辑,这些被调度的分布式节点被称为 AP。

TM 负责调度 AP 的行为,并最终决定这些 AP 是否要把事务真正进行提交到(RM)

本图来自小滴课堂

11.如何保证消息队列的高可用

可用采用集群来保证高可用,以RabbitMQ为例,推荐采用镜像集群,普通集群如果磁盘节点挂了就GG了,还是无法保证高可用,镜像集群的配置要用到HAProxy,需要在后台管理页面中设置策略,将ha-mode设置为all,表明每个节点上都存放镜像…限于篇幅,具体的集群配置我后面会专门写一篇博客总结.

12.项目中为什么引入消息队列

解耦,异步,削峰

  1. 解耦:
    在这里插入图片描述
  2. 异步
    在这里插入图片描述
  3. 削峰

在这里插入图片描述

13.如何保证消息的消费顺序?

以rabbitmq为例,消息1和消息2需要按顺序消费,必须先消费消息1,后消费消息2,我们可以将消息放顺序到不同的queue里,然后由worker来消费.

14.如何解决消息队列的延时及过期失效问题?

批量重导,自己写程序把失效的数据查出来然后重新导入队里中.

15.消息队列满了怎么处理?当消息过度积压怎么处理?

应当在设计上尽量避免出现这种问题,如果确实已经碰到了,可以采取服务降级策略,同时临时增加一些消费能力更强劲的消费者,以X倍速率消费队列中积压的消息.

16.消息队列怎么避免重复消费

任何消息队列产品不保证消息不重复,如果你的业务需要保证严格的不重复消息,需要你自己在业务端去重

kafka、rocketmq、rabbitmq等都是一样的

接口幂等性保障 ,消费端处理业务消息要保持幂等性

幂等性,通俗点说,就一个数据或者一个请求,给你重复来多次,你得确保对应的数据是不会改变的

Redis

   //Redis中操作,判断是否已经操作过 TODO

    boolean flag =  jedis.setNX(key);

    if(flag){

         //消费

     }else{

          //忽略,重复消费

     }

Incr 原子操作:key自增,大于0 返回值大于0则说明消费过

  int num =  jedis.incr(key);
                  if(num == 1){
                        //消费
                  }else{
                       //忽略,重复消费
                  }
上述两个方式都可以,但是排重可以不考虑原子问题,数据量多需要设置过期时间,考虑原子问题,

数据库去重表

某个字段使用Message的key做唯一索引

核心还是业务场景,不一定每个消息消费都需要加上述的操作,比如下面的场景

优惠券记录释放的MQ消息,即锁定的消息变成可用的,不管多少次都是一样的结果
update coupon_record set state='NEW' where id =#{id} and state='LOCK'

评论点赞计数,需要保证幂等性,因为一个消息就会导致数值发生变化

17.RabbitMQ是如何保障消息可靠性投递

什么是消息的可靠性投递

保证消息百分百发送到消息队列中去

详细
    保证mq节点成功接受消息
    消息发送端需要接受到mq服务端接受到消息的确认应答
    完善的消息补偿机制,发送失败的消息可以再感知并二次处理

RabbitMQ消息投递路径

生产者-->交换机->队列->消费者

通过两个的点控制消息的可靠性投递

    生产者到交换机
        通过confirmCallback

    交换机到队列
        通过returnCallback

建议

开启消息确认机制以后,保证了消息的准确送达,但由于频繁的确认交互, rabbitmq 整体效率变低,吞吐量下降严重,不是非常重要的消息真心不建议用消息确认机制

图片来自小滴课堂rabbitmq视频

18.分布式锁核心知识介绍和注意事项

  1. 避免单人超领劵
  • 加锁
    • 本地锁:synchronize、lock等,锁在当前进程内,集群部署下依旧存在问题
    • 分布式锁:redis、zookeeper等实现,虽然还是锁,但是多个进程共用的锁标记,可以用Redis、Zookeeper、Mysql等都可以
      图片来自小滴课堂

设计分布式锁应该考虑的东西

  • 排他性
    • 在分布式应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行
  • 容错性
    • 分布式锁一定能得到释放,比如客户端奔溃或者网络中断
  • 满足可重入、高性能、高可用
  • 注意分布式锁的开销、锁粒度

19.基于Redis实现分布式锁的几种坑《上》

  1. 实现分布式锁 可以用 Redis、Zookeeper、Mysql数据库这几种 , 性能最好的是Redis且是最容易理解
  • 分布式锁离不开 key - value 设置
  • key 是锁的唯一标识,一般按业务来决定命名,比如想要给一种商品的秒杀活动加锁,key 命名为 “seckill_商品ID” 。value就可以使用固定值,比如设置成1

基于redis实现分布式锁,文档:http://www.redis.cn/commands.html#string

  • 加锁 SETNX key value
setnx 的含义就是 SET if Not Exists,有两个参数 setnx(key, value),该方法是原子性操作

如果 key 不存在,则设置当前 key 成功,返回 1;

如果当前 key 已经存在,则设置当前 key 失败,返回 0
  • 解锁 del (key)
得到锁的线程执行完任务,需要释放锁,以便其他线程可以进入,调用 del(key)
  • 配置锁超时 expire (key,30s)
客户端奔溃或者网络中断,资源将会永远被锁住,即死锁,因此需要给key配置过期时间,以保证即使没有被显式释放,这把锁也要在一定时间后自动释放

  • 综合伪代码
methodA(){
  String key = "coupon_66"

  if(setnx(key,1== 1{
      expire(key,30,TimeUnit.MILLISECONDS)
      try {
          //做对应的业务逻辑
          //查询用户是否已经领券
          //如果没有则扣减库存
          //新增领劵记录
      } finally {
          del(key)
      }
  }else{

    //睡眠100毫秒,然后自旋调用本方法
		methodA()
  }
}
存在哪些问题,大家自行思考下

20.基于Redis实现分布式锁的几种坑你是否踩过《下》

  1. 多个命令之间不是原子性操作,如setnxexpire之间,如果setnx成功,但是expire失败,且宕机了,则这个资源就是死锁
使用原子命令:设置和配置过期时间  setnx / setex
如: set key 1 ex 30 nx
java里面 redisTemplate.opsForValue().setIfAbsent("seckill_1",1,30,TimeUnit.MILLISECONDS)

图片来源于小滴课堂

  1. 业务超时,存在其他线程勿删,key 30秒过期,假如线程A执行很慢超过30秒,则key就被释放了,其他线程B就得到了锁,这个时候线程A执行完成,而B还没执行完成,结果就是线程A删除了线程B加的锁
可以在 del 释放锁之前做一个判断,验证当前的锁是不是自己加的锁, 那 value 应该是存当前线程的标识或者uuid

String key = "coupon_66"
String value = Thread.currentThread().getId()

if(setnx(key,value) == 1{
    expire(key,30,TimeUnit.MILLISECONDS)
    try {
        //做对应的业务逻辑
    } finally {
    	//删除锁,判断是否是当前线程加的
    	if(get(key).equals(value)){
					//还存在时间间隔
					del(key)
        }
    }
}else{
	
	//睡眠100毫秒,然后自旋调用本方法

}

进一步细化误删

  • 当线程A获取到正常值时,返回带代码中判断期间锁过期了,线程B刚好重新设置了新值,线程A那边有判断value是自己的标识,然后调用del方法,结果就是删除了新设置的线程B的值
  • 核心还是判断和删除命令 不是原子性操作导致

21.手把手教你彻底掌握分布式锁lua脚本+redis原生代码编写

前面说了redis做分布式锁存在的问题

  • 核心是保证多个指令原子性,加锁使用setnx setex 可以保证原子性,那解锁使用 判断和删除怎么保证原子性
  • 文档:http://www.redis.cn/commands/set.html
  • 多个命令的原子性:采用 lua脚本+redis, 由于【判断和删除】是lua脚本执行,所以要么全成功,要么全失败
//获取lock的值和传递的值一样,调用删除操作返回1,否则返回0

String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";

//Arrays.asList(lockKey)是key列表,uuid是参数
Integer result = redisTemplate.execute(new DefaultRedisScript<>(script, Integer.class), Arrays.asList(lockKey), uuid);

全部代码

/**
* 原生分布式锁 开始
* 1、原子加锁 设置过期时间,防止宕机死锁
* 2、原子解锁:需要判断是不是自己的锁
*/
String uuid = CommonUtil.generateUUID();
String lockKey = "lock:coupon:"+couponId;
Boolean nativeLock=redisTemplate.opsForValue().setIfAbsent(lockKey,uuid,Duration.ofSeconds(30));
    if(nativeLock){
      //加锁成功
      log.info("加锁:{}",nativeLock);
      try {
           //执行业务  TODO
        }finally {
           String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";

                Integer result = redisTemplate.execute(new DefaultRedisScript<>(script, Integer.class), Arrays.asList(lockKey), uuid);
                log.info("解锁:{}",result);
            }

        }else {
            //加锁失败,睡眠100毫秒,自旋重试
            try {
                TimeUnit.MILLISECONDS.sleep(100L);
            } catch (InterruptedException e) { }
            return addCoupon( couponId, couponCategory);
        }
        //原生分布式锁 结束
  • 遗留一个问题,锁的过期时间,如何实现锁的自动续期 或者 避免业务执行时间过长,锁过期了?
    • 原生方式的话,一般把锁的过期时间设置久一点,比如10分钟时间

22.基于Redis官方推荐-分布式锁最佳实践介绍

原生代码+redis实现分布式锁使用比较复杂,且有些锁续期问题更难处理

  • 官方推荐方式:https://redis.io/topics/distlock
  • 多种实现客户端框架
  • Redisson官方中文文档:https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95

聚合工程锁定版本,common项目添加依赖(多个服务都会用到分布式锁)

<!--分布式锁-->
<dependency>
      <groupId>org.redisson</groupId>
      <artifactId>redisson</artifactId>
      <version>3.10.1</version>
</dependency>

创建redisson客户端

 @Value("${spring.redis.host}")
    private String redisHost;

    @Value("${spring.redis.port}")
    private String redisPort;

    @Value("${spring.redis.password}")
    private String redisPwd;
    
		/**
     * 配置分布式锁
     * @return
     */
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();

        //单机模式
        //config.useSingleServer().setPassword("123456").setAddress("redis://8.129.113.233:3308");
        config.useSingleServer().setPassword(redisPwd).setAddress("redis://"+redisHost+":"+redisPort);

        //集群模式
        //config.useClusterServers()
        //.setScanInterval(2000)
        //.addNodeAddress("redis://10.0.29.30:6379", "redis://10.0.29.95:6379")
        // .addNodeAddress("redis://127.0.0.1:6379");

        RedissonClient redisson = Redisson.create(config);

        return redisson;
    }

23.Redisson实现优惠券微服务领劵接口的分布式锁

优惠券微服务,分布式锁实现方式

Lock lock = redisson.getLock("lock:coupon:"+couponId);
//阻塞式等待,一个线程获取锁后,其他线程只能等待,和原生的方式循环调用不一样
lock.lock();
        try {
            CouponDO couponDO = couponMapper.selectOne(new QueryWrapper<CouponDO>().eq("id", couponId)
                    .eq("category", couponCategory)
                    .eq("publish", CouponPublishEnum.PUBLISH));

            this.couponCheck(couponDO,loginUser.getId());

            CouponRecordDO couponRecordDO = new CouponRecordDO();
            BeanUtils.copyProperties(couponDO,couponRecordDO);
            couponRecordDO.setCreateTime(new Date());
            couponRecordDO.setUseState(CouponStateEnum.NEW.name());
            couponRecordDO.setUserId(loginUser.getId());
            couponRecordDO.setUserName(loginUser.getName());
            couponRecordDO.setCouponId(couponId);
            couponRecordDO.setId(null);
            //高并发下扣减劵库存,采用乐观锁,当前stock做版本号,一次只能领取1张
            int rows = couponMapper.reduceStock(couponId);

            if(rows == 1){
                //库存扣减成功才保存
                couponRecordMapper.insert(couponRecordDO);
            }else {
                log.warn("发放优惠券失败:{},用户:{}",couponDO,loginUser);
                throw new BizException(BizCodeEnum.COUPON_NO_STOCK);
            }

        }finally {
            lock.unlock();
        }

24.Redisson是怎样解决分布式锁的里面的坑

简介:redisson解决分布式锁里面的坑

  • 问题 : Redis锁的过期时间小于业务的执行时间该如何续期?

    • watch dog看门狗机制
负责储存这个分布式锁的Redisson节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。或者业务执行时间过长导致锁过期,

为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。

Redisson中客户端一旦加锁成功,就会启动一个watch dog看门狗。watch dog是一个后台线程,会每隔10秒检查一下,如果客户端还持有锁key,那么就会不断的延长锁key的生存时间


默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定

指定加锁时间

// 加锁以后10秒钟自动解锁
// 无需调用unlock方法手动解锁
lock.lock(10, TimeUnit.SECONDS);

// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);

if (res) {
   try {
     ...
   } finally {
       lock.unlock();
   }
}

小滴课堂官网:https://xdclass.net/#/index

举报

相关推荐

0 条评论