【微服务38】分布式事务Seata源码解析六:全局/分支事务分布式ID如何生成?序列号超了怎么办?时钟回拨问题如何处理?

简介: 【微服务38】分布式事务Seata源码解析六:全局/分支事务分布式ID如何生成?序列号超了怎么办?时钟回拨问题如何处理?

@[TOC]

一、前言

至此,seata系列的内容包括:

  1. can not get cluster name in registry config ‘service.vgroupMapping.xx‘, please make sure registry问题解决
  2. Seata Failed to get available servers: endpoint format should like ip:port 报错原因/解决方案汇总版(看完本文必解决问题)
  3. Seata json decode exception, Cannot construct instance of java.time.LocalDateTime报错原因/解决方案最全汇总版
  4. 【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版)
  5. 【微服务 32】Spring Cloud整合Seata、Nacos实现分布式事务案例(巨细排坑版)【云原生】
  6. 【微服务33】分布式事务Seata源码解析一:在IDEA中启动Seata Server
  7. 【微服务34】分布式事务Seata源码解析二:Seata Server启动时都做了什么
  8. 【微服务35】分布式事务Seata源码解析三:从Spring Boot特性来看Seata Client 启动时都做了什么
  9. 【微服务36】分布式事务Seata源码解析四:图解Seata Client 如何与Seata Server建立连接、通信
  10. 【微服务37】分布式事务Seata源码解析五:@GlobalTransactional如何开启全局事务

本文接着来看Seata的全局事务ID(transactionId)和分支事务ID(branchId)是如何生成的?

二、分布式ID初始化

在Seata Server启动的时候( 【微服务34】分布式事务Seata源码解析二:Seata Server启动时都做了什么【云原生】)会初始化UUID生成器UUIDGenerator

在这里插入图片描述

1、UUIDGenerator

public class UUIDGenerator {

    private static volatile IdWorker idWorker;

    /**
     * generate UUID using snowflake algorithm
     * @return UUID
     */
    public static long generateUUID() {
        // DCL + volatile ,实现并发场景下保证idWorker的单例特性
        if (idWorker == null) {
            synchronized (UUIDGenerator.class) {
                if (idWorker == null) {
                    init(null);
                }
            }
        }
        // 每次通过雪花算法实现的nextId()获取一个新的UUID
        return idWorker.nextId();
    }

    /**
     * 初始化IDWorker
     */
    public static void init(Long serverNode) {
        idWorker = new IdWorker(serverNode);
    }
}

UUIDGenerator会委托给其组合的IdWorker根据雪花算法生成分布式ID,生成的雪花Id由0、10位的workerId、41位的时间戳、12位的sequence序列号组成。

2、IdWorker

IdWorker中有8个重要的成员变量/常量:

/**
 * Start time cut (2020-05-03)
 */
private final long twepoch = 1588435200000L;

/**
 * The number of bits occupied by workerId
 */
private final int workerIdBits = 10;

/**
 * The number of bits occupied by timestamp
 */
private final int timestampBits = 41;

/**
 * The number of bits occupied by sequence
 */
private final int sequenceBits = 12;

/**
 * Maximum supported machine id, the result is 1023
 */
private final int maxWorkerId = ~(-1 << workerIdBits);

/**
 * business meaning: machine ID (0 ~ 1023)
 * actual layout in memory:
 * highest 1 bit: 0
 * middle 10 bit: workerId
 * lowest 53 bit: all 0
 */
private long workerId;

/**
 * 又是一个雪花算法(64位,8字节)
 * timestamp and sequence mix in one Long
 * highest 11 bit: not used
 * middle  41 bit: timestamp
 * lowest  12 bit: sequence
 */
private AtomicLong timestampAndSequence;

/**
 * 从一个long数组类型中抽取出一个时间戳伴随序列号,偏向一个辅助性质
 * mask that help to extract timestamp and sequence from a long
 */
private final long timestampAndSequenceMask = ~(-1L << (timestampBits + sequenceBits));

变量/常量解释:

  1. 常量twepoch表示我们的时间戳时间从2020-05-03开始计算,即当前时间的时间戳需要减去twepoch的值1588435200000L
  2. 常量workerIdBits表示机器号workerId占10位;
  3. 常量timestampBits表示时间戳timestamp占41位;
  4. 常量sequenceBits表示序列化占12位;
  5. 常量maxWorkerId表示机器号的最大值为1023;
  6. long类型的变量workerId本身也是一个雪花算法,只是从头往后数,第2位开始,一共10位用来表示workerId,其余位全是0;
  7. AtomicLong类型的变量timestampAndSequence,其本身也是一个雪花算法,头11位不使用,中间41位表示timestamp,最后12位表示sequence;
  8. long类型的常量timestampAndSequenceMask,用于从一个完整的雪花ID(long类型)中摘出timestamp 和 sequence

IdWorker构造器中会分别初始化TimestampAndSequence、WorkerId。

public IdWorker(Long workerId) {
    // 初始化时间戳sequence
    initTimestampAndSequence();
    // 初始化workerId
    initWorkerId(workerId);
}

1) 初始化时间戳和序列号

initTimestampAndSequence()方法负责初始化timestampsequence

private void initTimestampAndSequence() {
    // 拿到当前时间戳 - (2020-05-03 时间戳)的数值,即当前时间相对2020-05-03的时间戳
    long timestamp = getNewestTimestamp();
    // 把时间戳左移12位,后12位留给sequence使用
    long timestampWithSequence = timestamp << sequenceBits;
    // 把混合sequence(默认为0)的时间戳赋值给timestampAndSequence
    this.timestampAndSequence = new AtomicLong(timestampWithSequence);
}

// 获取当前时间戳
private long getNewestTimestamp() {
    //当前时间的时间戳减去2020-05-03的时间戳
    return System.currentTimeMillis() - twepoch;
}

2)初始化机器ID

initWorkerId(Long workerId)方法负责初始化workId,默认不会传过来workerId,如果传过来则使用传过来的workerId,并校验其不能大于1023,然后将其左移53位;

private void initWorkerId(Long workerId) {
   if (workerId == null) {
       // workid为null时,自动生成一个workerId
       workerId = generateWorkerId();
   }
   // workerId最大只能是1023,因为其只占10bit
   if (workerId > maxWorkerId || workerId < 0) {
       String message = String.format("worker Id can't be greater than %d or less than 0", maxWorkerId);
       throw new IllegalArgumentException(message);
   }
   this.workerId = workerId << (timestampBits + sequenceBits);
}

1> 如果没传则基于MAC地址生成;
在这里插入图片描述

机器id由两值相加:

  • (byte值 & 0B11) << Byte.SIZE,即最大值为 0B11=3;然后左移8位为:1100000000; 所以此处最大十进制值为768
  • (byte值 & 0xFF) ;16进制的F对应二进制为 1111,所以最大十进制值FF: 255;
  • 然后:768 + 255 = 1023,即机器id(workerId)最大不会超过1023
    0B对应二进制,0x对应十六进制

2> 如果基于MAC地址生成workerId出现异常,则也1023为基数生成一个随机的workerId;
在这里插入图片描述

最后同样,校验workerId不能大于1023,然后将其左移53位,用于拼接出分布式ID。

三、分布式ID获取

上面我们了解到在Seata Server启动时会初始化UUID生成器UUIDGenerator的成员IDWorker,以用于生成分布式ID;在后续TM开启全局事务、或RM创建分支事务加入到全局事务时,都会调用UUIDGenerator#generateUUID()方法生成分布式事务ID(全局事务ID transactionId、分支事务ID branchId);

在这里插入图片描述

1、生成UUID的入口

public static long generateUUID() {
    // DCL + volatile ,实现并发场景下保证idWorker的单例特性
    if (idWorker == null) {
        synchronized (UUIDGenerator.class) {
            if (idWorker == null) {
                init(null);
            }
        }
    }
    // 每次通过雪花算法实现的nextId()获取一个新的UUID
    return idWorker.nextId();
}

idWorker变量被volatile关键字所修饰,确保其在多线程环境下的可见性,再结合DCL(Double Check Lock,双重检查锁)确保idWorker的单例性。

每次要获取新的一个UUID时,会通过IdWorker#nextId()方法实现;

2、如何生成一个UUID

IdWorker#nextId()方法负责生成一个UUID;其中:

  • highest 1 bit: 最高位的1bit始终是0
  • next 10 bit: workerId 10个bit表示机器号;
  • next 41 bit: timestamp 41个bit表示当前机器的时间戳(ms级别),每毫秒递增;
  • lowest 12 bit: sequence 12位的序号,如果一台机器在一毫秒内有很多线程要来生成id,12bit的sequence会自增;
public long nextId() {
    // 解决sequence序列号被用尽问题!
    waitIfNecessary();
    // 自增时间戳的sequence,等于对一个毫秒内的sequence做累加操作,或 timestamp + 1、sequence置0
    long next = timestampAndSequence.incrementAndGet();
    // 把最新时间戳(包括序列号)和mask做一个与运算,得到真正的时间戳伴随的序列
    long timestampWithSequence = next & timestampAndSequenceMask;
    // 最后和workerId做或运算,得到最终的UUID;
    return workerId | timestampWithSequence;
}

nextId()方法逻辑:

  1. 解决sequence序列号被用尽问题;
  2. 累加序列号sequence,Seata中设计的sequence是和timestamp放在同一个变量里,累加之后再和mask后53位全是1)做一个与运算,得到真正的时间戳伴随的序列;
  3. 将workerId 和 时间戳伴随的序列 通过或运算组合成最终的UUID。

下面细看一下waitIfNecessary()是如何解决序列号被用尽的问题;

1)如何解决序列号被用尽的问题

waitIfNecessary()会解决序列号被用尽的问题;

private void waitIfNecessary() {
    // 获取当前时间戳 以及相应的sequence序列号
    long currentWithSequence = timestampAndSequence.get();
    // 通过位运算拿到当前的时间戳
    long current = currentWithSequence >>> sequenceBits;
    // 获取当前真实的时间戳
    long newest = getNewestTimestamp();
    // 如果`timestampAndSequence`中的当前时间戳 大于等于 真实的时间戳,说明当前机器时间之前的sequence序号 / 某个毫秒内的序列号 已经被耗尽了;
    if (current >= newest) {
        try {
            // 如果获取UUID的QPS过高,导致时间戳对应的sequence序号被耗尽了
            // 线程休眠5毫秒
            Thread.sleep(5);
        } catch (InterruptedException ignore) {
            // don't care
        }
    }
}

如果有大量的线程并发获取UUID、获取UUID的QPS过高,可能会导致从初始化IdWorker时间戳开始 到 当前时间戳的序列号全部用完了(也可以理解为某一个毫秒内的sequence耗尽);但是时间戳却累加了、进到下一个毫秒(或下几毫秒);然而当前实际时间却还没有到下一毫秒。如果恰巧此时重启了seata server,再初始化IdWorker时的时间戳就有可能会出现重复,进而导致UUID重复。

所以Seata为了尽可能的保证UUID生成算法的稳定性;

如果 timestampAndSequence中的当前时间戳 大于等于 服务器真实的时间戳,会将线程 睡眠5ms

博主看到这里时有两个问题:

  1. 为什么判断时间戳时是大于等于,而不是大于?
  2. 为什么就让线程睡眠了5ms?

为什么判断时间戳时是大于等于,而不是大于?

如果是大于(current > newest),而不是大于等于(current >= newest);

考虑一种极端的场景,UUID的时间戳已经累加到了当前时间,此时Seata Server立马关机重启(假设这个过程耗时不到1ms),就会出现重复的UUID。不过这种场景仅存在于理论上;现实应该不会,所以我认为 大于(current > newest) 是没有问题的。

为什么就让线程睡眠了5ms?

这里就睡眠5ms,可能只是把所有的流量都往后均摊,因为往往高峰期时间也比较短;并且一个毫秒会有4096个序列号,而从seata Server启动开始也不会立刻就是高峰期,所以到当前时间之前 也会有很多的时间戳给UUID使用;不过这个简单粗暴的阻塞线程确实会浪费一些系统资源。

为什么是睡眠5ms,而不是3ms、2ms,可能是出于压测的结论,也可能作者也没想那么多。

2)时钟回拨问题的解决

UUIDGenerator(或者说IdWorker)通过借用未来时间来解决sequence天然存在的并发限制,如果timestampAndSequence中的当前时间戳大约 服务器的当前时间,仅仅会睡眠5ms,起一个缓冲的作用;但timestampAndSequence仍会继续递增,使用未来的时间。

Seata Server服务不重启基本没有问题,当接入Seata Server的服务们QPS比较高时,重启Seata Server就可能会出现新生成的UUID和历史UUID重复问题。

四、总结和后续

本文聊了Seata中分布式ID是使用雪花算法生成的,对一个64位的UUID,其最高位恒为0,10个bit表示机器号,41个bit表示当前机器的时间戳(ms级别),12位的序号。

seata又对毫秒内序列号用尽、时钟回拨做了特殊处理。

下一篇文章我们将聊Seata全局事务的执行流程。

相关文章
|
运维 持续交付 云计算
深入解析云计算中的微服务架构:原理、优势与实践
深入解析云计算中的微服务架构:原理、优势与实践
882 86
|
存储 Java 文件存储
微服务——SpringBoot使用归纳——Spring Boot使用slf4j进行日志记录—— logback.xml 配置文件解析
本文解析了 `logback.xml` 配置文件的详细内容,包括日志输出格式、存储路径、控制台输出及日志级别等关键配置。通过定义 `LOG_PATTERN` 和 `FILE_PATH`,设置日志格式与存储路径;利用 `&lt;appender&gt;` 节点配置控制台和文件输出,支持日志滚动策略(如文件大小限制和保存时长);最后通过 `&lt;logger&gt;` 和 `&lt;root&gt;` 定义日志级别与输出方式。此配置适用于精细化管理日志输出,满足不同场景需求。
3187 1
|
运维 监控 持续交付
微服务架构解析:跨越传统架构的技术革命
微服务架构(Microservices Architecture)是一种软件架构风格,它将一个大型的单体应用拆分为多个小而独立的服务,每个服务都可以独立开发、部署和扩展。
3804 37
微服务架构解析:跨越传统架构的技术革命
|
传感器 监控 安全
智慧工地云平台的技术架构解析:微服务+Spring Cloud如何支撑海量数据?
慧工地解决方案依托AI、物联网和BIM技术,实现对施工现场的全方位、立体化管理。通过规范施工、减少安全隐患、节省人力、降低运营成本,提升工地管理的安全性、效率和精益度。该方案适用于大型建筑、基础设施、房地产开发等场景,具备微服务架构、大数据与AI分析、物联网设备联网、多端协同等创新点,推动建筑行业向数字化、智能化转型。未来将融合5G、区块链等技术,助力智慧城市建设。
795 1
|
消息中间件 算法 调度
分布式系统学习10:分布式事务
本文是小卷关于分布式系统架构学习系列的第13篇,重点探讨了分布式事务的相关知识。随着业务增长,单体架构拆分为微服务后,传统的本地事务无法满足需求,因此需要引入分布式事务来保证数据一致性。文中详细介绍了分布式事务的必要性、实现方案及其优缺点,包括刚性事务(如2PC、3PC)和柔性事务(如TCC、Saga、本地消息表、MQ事务、最大努力通知)。同时,还介绍了Seata框架作为开源的分布式事务解决方案,提供了多种事务模式,简化了分布式事务的实现。
711 5
|
Java 关系型数据库 数据库
微服务SpringCloud分布式事务之Seata
SpringCloud+SpringCloudAlibaba的Seata实现分布式事务,步骤超详细,附带视频教程
1183 1
|
弹性计算 持续交付 API
构建高效后端服务:微服务架构的深度解析与实践
在当今快速发展的软件行业中,构建高效、可扩展且易于维护的后端服务是每个技术团队的追求。本文将深入探讨微服务架构的核心概念、设计原则及其在实际项目中的应用,通过具体案例分析,展示如何利用微服务架构解决传统单体应用面临的挑战,提升系统的灵活性和响应速度。我们将从微服务的拆分策略、通信机制、服务发现、配置管理、以及持续集成/持续部署(CI/CD)等方面进行全面剖析,旨在为读者提供一套实用的微服务实施指南。
|
负载均衡 Java 持续交付
深入解析微服务架构中的服务发现与负载均衡
深入解析微服务架构中的服务发现与负载均衡
865 7
|
Kubernetes Java 微服务
微服务上下线动态感知实现的技术解析
随着微服务架构的广泛应用,服务的动态管理和监控变得尤为重要。在微服务架构中,服务的上下线是一个常见的操作,如何实时感知这些变化,确保系统的稳定性和可靠性,成为了一个关键技术挑战。本文将深入探讨微服务上下线动态感知的实现方式,从技术基础、场景案例、解决思路和底层原理等多个维度进行阐述,并分别使用Java和Python进行演示介绍。
454 4

推荐镜像

更多
  • DNS