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

本文涉及的产品
注册配置 MSE Nacos/ZooKeeper,118元/月
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
云原生网关 MSE Higress,422元/月
简介: 【微服务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全局事务的执行流程。

相关文章
|
1月前
|
Java 数据库
在Java中使用Seata框架实现分布式事务的详细步骤
通过以上步骤,利用 Seata 框架可以实现较为简单的分布式事务处理。在实际应用中,还需要根据具体业务需求进行更详细的配置和处理。同时,要注意处理各种异常情况,以确保分布式事务的正确执行。
|
17天前
|
消息中间件 SQL 中间件
大厂都在用的分布式事务方案,Seata+RocketMQ带你打破10万QPS瓶颈
分布式事务涉及跨多个数据库或服务的操作,确保数据一致性。本地事务通过数据库直接支持ACID特性,而分布式事务则需解决跨服务协调难、高并发压力及性能与一致性权衡等问题。常见的解决方案包括两阶段提交(2PC)、Seata提供的AT和TCC模式、以及基于消息队列的最终一致性方案。这些方法各有优劣,适用于不同业务场景,选择合适的方案需综合考虑业务需求、系统规模和技术团队能力。
129 7
|
29天前
|
存储 Java 关系型数据库
在Spring Boot中整合Seata框架实现分布式事务
可以在 Spring Boot 中成功整合 Seata 框架,实现分布式事务的管理和处理。在实际应用中,还需要根据具体的业务需求和技术架构进行进一步的优化和调整。同时,要注意处理各种可能出现的问题,以保障分布式事务的顺利执行。
51 6
|
29天前
|
数据库
如何在Seata框架中配置分布式事务的隔离级别?
总的来说,配置分布式事务的隔离级别是实现分布式事务管理的重要环节之一,需要认真对待和仔细调整,以满足业务的需求和性能要求。你还可以进一步深入研究和实践 Seata 框架的配置和使用,以更好地应对各种分布式事务场景的挑战。
29 6
|
28天前
|
消息中间件 运维 数据库
Seata框架和其他分布式事务框架有什么区别
Seata框架和其他分布式事务框架有什么区别
25 1
|
3月前
|
SQL NoSQL 数据库
SpringCloud基础6——分布式事务,Seata
分布式事务、ACID原则、CAP定理、Seata、Seata的四种分布式方案:XA、AT、TCC、SAGA模式
SpringCloud基础6——分布式事务,Seata
|
1月前
|
设计模式 Java API
微服务架构演变与架构设计深度解析
【11月更文挑战第14天】在当今的IT行业中,微服务架构已经成为构建大型、复杂系统的重要范式。本文将从微服务架构的背景、业务场景、功能点、底层原理、实战、设计模式等多个方面进行深度解析,并结合京东电商的案例,探讨微服务架构在实际应用中的实施与效果。
130 6
|
1月前
|
设计模式 Java API
微服务架构演变与架构设计深度解析
【11月更文挑战第14天】在当今的IT行业中,微服务架构已经成为构建大型、复杂系统的重要范式。本文将从微服务架构的背景、业务场景、功能点、底层原理、实战、设计模式等多个方面进行深度解析,并结合京东电商的案例,探讨微服务架构在实际应用中的实施与效果。
51 1
|
20天前
|
Java 开发者 微服务
从单体到微服务:如何借助 Spring Cloud 实现架构转型
**Spring Cloud** 是一套基于 Spring 框架的**微服务架构解决方案**,它提供了一系列的工具和组件,帮助开发者快速构建分布式系统,尤其是微服务架构。
141 68
从单体到微服务:如何借助 Spring Cloud 实现架构转型
|
19天前
|
运维 监控 持续交付
微服务架构解析:跨越传统架构的技术革命
微服务架构(Microservices Architecture)是一种软件架构风格,它将一个大型的单体应用拆分为多个小而独立的服务,每个服务都可以独立开发、部署和扩展。
149 36
微服务架构解析:跨越传统架构的技术革命

推荐镜像

更多