在高并发、高性能、高可用 三高项目中如何设计适合实际业务场景的分布式id(一)(1)

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

超高并发、超高性能分布式ID生成系统三个超高

设计一个超高性能、超高并发且超低延迟的分布式ID生成系统是许多大型系统和微服务架构中的关键组件。这样的系统不仅需要生成全局唯一的ID,还要保证在极高的请求压力下仍能保持稳定的性能。

关键点:

以下是一些设计这样的系统时需要考虑的关键点:

超低延迟

要求:1 秒可处理 10W 并发请求,接口响应时间 5 ms 。

  • 算法选择:选择计算简单、性能高效的ID生成算法。例如,Snowflake算法就是一种常见的选择,它能够在不牺牲全局唯一性的情况下快速生成ID。
  • 缓存和预分配:通过缓存或预分配ID来减少生成ID时的计算延迟。例如,可以预先为每个服务实例分配一批ID,当实例需要生成ID时,直接从这批ID中取一个即可。
  • 减少网络开销:如果ID生成服务是一个独立的服务,那么网络延迟也是一个需要考虑的因素。可以通过将ID生成服务部署在靠近用户的位置或使用更高效的网络协议来减少网络延迟。
超高可用
  • 冗余部署:通过部署多个ID生成服务实例来提供冗余,确保即使部分实例发生故障,系统仍能继续生成ID。
  • 故障切换:实现故障检测和自动切换机制,当检测到某个实例故障时,自动将其从服务池中移除,并将请求路由到其他健康的实例。
  • 数据持久化:如果ID生成算法依赖于某些状态(如Snowflake中的时间戳和序列号),那么需要确保这些状态在故障转移时能够持久化并正确恢复。
超高并发
  • 水平扩展:通过增加更多的ID生成服务实例来分散负载,提高系统的并发处理能力。这通常需要一个无状态的ID生成算法或一种有效的状态同步机制。
  • 负载均衡:使用负载均衡器将请求均匀分配到各个ID生成服务实例上,避免单点压力过大。
  • 优化锁和同步:如果ID生成算法中涉及到锁或同步操作,需要对其进行优化以减少争用和等待时间。例如,可以使用分段锁或乐观锁等技术来减少锁的范围和持有时间。
  • 异步处理:将ID生成过程与其他业务逻辑解耦,采用异步方式生成ID,避免阻塞主线程或关键路径。

最后,达到如滴滴的tinyid那样的千万QPS级别的性能,通常需要结合具体的业务场景和系统架构进行深度的定制和优化。这可能包括使用专门的硬件、优化网络拓扑、调整操作系统和数据库配置等多个层面的工作。同时,还需要通过严格的性能测试和监控来确保系统在实际运行中能够达到预期的性能目标。

发展阶段

确实,随着企业业务的发展和系统复杂性的增加,ID生成服务经历了从各自封装到集成框架,再到独立服务的演进过程。下面我将详细解释ID生成服务在企业级使用场景中的各个阶段及其特点。

第一阶段:各自封装

在企业早期,各个系统或模块通常根据自己的需要实现ID生成逻辑。这些实现可能包括基于数据库自增ID、UUID、雪花算法(Snowflake)等。这种方式的优点是简单直接,但缺点是实现分散,难以统一管理和保证质量。此外,不同的ID生成策略可能导致ID冲突或不一致性,增加了系统间集成的复杂性。

第二阶段:集成框架

为了解决分散实现的问题,企业可能会开发一个统一的ID生成基础库,将各种ID生成逻辑集成到一个框架中。这样,业务方可以通过调用这个基础库来生成ID,而无需关心底层的实现细节。然而,对于像Snowflake这样需要分配worker ID的算法,业务系统仍然需要关注worker ID的分配逻辑。因此,有些企业会将Snowflake的逻辑封装到服务治理框架中,由框架负责worker ID的分配和服务内的唯一性。这种方式提高了ID生成的统一性和可管理性,但仍然存在一定的状态管理复杂性。

第三阶段:ID生成服务(idgen服务)

随着业务量的增长和系统稳定性的要求提高,企业需要一个更加稳定、高效且无状态的ID生成服务。因此,独立的ID生成服务应运而生。这种服务通常具有以下特点:

  1. 支持多种模式:如DB号段模式和Snowflake模式,以满足不同业务场景的需求。
  2. 高可用性和稳定性:通过冗余部署、故障切换和数据持久化等技术手段确保服务的高可用性和稳定性。同时,具备时钟校准能力以防止时钟回拨等问题导致的ID生成异常。
  3. 高吞吐量和低延迟:通过优化算法、减少网络开销和使用高性能的硬件等手段实现高吞吐量和低延迟的ID生成性能。TP99等关键指标必须非常低,以确保在极端情况下的性能稳定性。
  4. 兼容现有逻辑:为了方便业务迁移,ID生成服务需要兼容现有的ID生成逻辑。这可以通过配置化、插件化或版本控制等方式实现。
  5. 无状态部署:为了支持快速滚动升级和弹性伸缩,ID生成服务应该使用无状态部署方式(如Kubernetes中的Deployment)。这意味着服务实例之间不共享状态信息,可以独立地扩展和缩减实例数量而不影响服务的整体可用性。

通过提供独立的ID生成服务,企业可以更加灵活地满足各种业务场景的ID生成需求,同时提高系统的稳定性、可用性和性能。

DB 号段模式

DB 号段模式是一种用于生成唯一 ID 的策略,它优化了传统的数据库自增 ID 方案。在这种模式下,系统不是每次需要 ID 时都去数据库中查询和获取,而是采用批量获取的方式,定期从数据库中获取一个 ID 号段,然后将这个号段缓存在本地。当外部服务需要 ID 时,直接从本地缓存的号段中分配即可。这种方式大大减轻了数据库的压力,并提升了对外服务的性能。

本地ID生成器

本地ID生成器是指本地环境中生成唯一标识符ID的工具或算法, 本地ID生成器通常在单个进程或机器内部生成ID,不需要网络I/O,因此性能较高。常见的本地ID生成策略包括:

  • 自增ID:例如,使用数据库的自增主键。但这种方法在分布式环境中不可行,因为不同的机器可能生成相同的ID。
  • UUID(通用唯一标识符):UUID是基于时间和机器节点(通常是MAC地址)等信息生成的,具有很高的唯一性。但UUID较长且不易读,也不支持排序。
  • 雪花算法(Snowflake):这是一种分布式ID生成算法,但通过一些技巧(如时间戳、机器ID和序列号)在本地生成ID,同时保证了全局唯一性和有序性。

UUID

UUID是一种本地生成ID的方式,UUID(Universally Unique Identifier,通用唯一标识符)是一种标准的128位数字,用于在计算机系统中唯一地标识信息。它由一组特定的算法生成,可以确保在全球范围内生成的每个UUID都是独一无二的。

UUID的标准形式通常包含32个16进制数字,分为五段,形式为8-4-4-4-12的36个字符,其中包含了四个连字符“-”。这种格式的设计使得UUID既易于人类阅读和记录,又能够包含足够的信息以确保其唯一性。

UUID版本区别

  1. Version 1:基于时间戳和MAC地址生成。由于包含了时间信息,因此Version 1的UUID是有序的,并且可以在一定程度上反映生成时间。但是,由于依赖于MAC地址,如果MAC地址被篡改或不可用,可能会导致UUID的唯一性受到影响。
  2. Version 2:与Version 1类似,但还包含了POSIX UID/GID信息。这使得Version 2的UUID在某些特定的分布式环境中更加有用。然而,由于同样依赖于MAC地址和时间戳,因此也存在与Version 1相同的问题。
  3. Version 3:基于MD5哈希算法生成。通过对指定的命名空间(namespace)和名称(name)进行MD5哈希运算来生成UUID。这使得Version 3的UUID具有更好的唯一性和安全性。然而,由于MD5算法已知存在弱点,因此不推荐在安全性要求较高的场景中使用。
  4. Version 4:完全随机生成。Version 4的UUID不依赖于任何特定的信息或算法,而是通过随机数生成器来生成。这使得Version 4的UUID具有极高的唯一性和安全性。然而,由于是随机生成的,因此Version 4的UUID是无序的。
  5. Version 5:基于SHA-1哈希算法生成。与Version 3类似,但使用了更安全的SHA-1哈希算法来代替MD5。这使得Version 5的UUID在安全性方面更加可靠。同样地,由于是基于哈希算法生成的,因此Version 5的UUID也是无序的。

UUID的主要优点包括:

  1. 全局唯一性:UUID的生成算法基于多种信息,如时间戳、计算机的唯一标识符(如MAC地址)以及随机数等,以确保生成的标识符在实践中具有高度的唯一性。虽然UUID的概率冲突非常低,但并不能保证绝对的唯一性。然而,在实际应用中,UUID的冲突几乎可以忽略不计。
  2. 无需中央协调机构:UUID的生成是分布式的,不需要中央协调机构来管理或分配ID。这使得UUID非常适合在分布式系统中使用,其中每个节点都可以独立地生成ID,而无需与其他节点进行通信或协调。
  3. 灵活性:UUID提供了多种版本来满足不同的需求。例如,Version 1和Version 2基于时间和MAC地址生成有序的UUID;Version 3和Version 5基于哈希算法生成与特定命名空间相关的UUID;而Version 4则是完全随机的,适用于安全性要求较高的场景。

然而,UUID也存在一些缺点

  1. 存储效率:UUID的字符串表示形式相对较长,占用的存储空间较大。虽然可以使用二进制格式来减少存储需求,但这会增加处理的复杂性。
  2. 可读性:UUID是一长串字符,对于人类来说不易于阅读和记忆。这可能会影响调试和日志分析等方面的便利性。为了解决这个问题,可以将UUID与更具可读性的标识符(如数据库中的主键或业务逻辑中的实体名称)进行关联。
  3. 无序性:由于UUID是基于多种信息生成的,因此它们是无序的。在数据库中按照UUID排序可能会导致性能下降。为了解决这个问题,可以在需要排序的场景中使用其他类型的ID(如自增ID或时间戳)。

总的来说,UUID是一种非常有用的工具,可以在分布式系统中生成全局唯一的标识符。它的优点在于全局唯一性、无需中央协调机构和灵活性;而缺点则在于存储效率、可读性和无序性。在使用UUID时,需要根据具体的应用场景和需求来权衡这些优缺点。

UUID在实际应用中确实可能面临一些问题和挑战。以下是一些主要的考虑点:

  1. 存储和性能: UUID是128位的标识符,通常以36个字符(包括4个连字符)的字符串形式表示。相比于较小的整数型主键,UUID占用更多的存储空间,并可能导致索引效率降低,特别是在数据库环境中。例如,在InnoDB存储引擎中,主键索引(聚集索引)与数据紧密关联,无序的UUID主键可能导致频繁的页分裂和随机I/O,从而影响性能。
  2. 可读性和可调试性: UUID的随机性和长度使得它们对人类来说难以阅读和记忆。这在调试、日志记录和错误跟踪时可能会增加复杂性。
  3. 生成策略: 不同的UUID版本有不同的生成策略。Version 1和2基于时间和节点(如MAC地址)生成,可能在某种程度上泄露系统信息。Version 4是随机生成的,但完全随机的UUID在数据库插入时可能导致性能问题。选择合适的UUID版本以满足特定需求是一个挑战。
  4. 唯一性冲突: 尽管UUID的冲突概率非常低,但在极端情况下仍有可能发生。特别是在大量生成UUID的系统中,需要采取措施来检测和处理潜在的冲突。
  5. 业务逻辑整合: 在某些业务场景中,可能需要将UUID与其他业务逻辑或系统整合。例如,将UUID用作数据库主键时,可能需要考虑如何与其他表或系统进行有效的关联和查询。
  6. 安全性考虑: 如果UUID被用作安全令牌或访问控制的一部分,那么它们的随机性和不可预测性就变得至关重要。在这种情况下,需要确保使用的UUID生成算法符合安全标准,并且难以被攻击者猜测或预测。

为了缓解这些问题和挑战,可以采取一些策略,如使用二进制格式存储UUID以节省空间、优化数据库索引策略、选择适当的UUID版本以及实施冲突检测和处理机制等。此外,还可以考虑将UUID与其他标识符(如业务主键)结合使用,以平衡唯一性、可读性和性能的需求。

UUID(Universally Unique Identifier)适合在多种场景下使用,特别是那些需要全局唯一标识符的场合。以下是一些常见的使用场景:

  1. 数据库主键:在数据库中,UUID可以用作表的主键,确保每个记录具有唯一的标识符。这有助于避免冲突和重复,特别是在分布式数据库环境中。
  2. 分布式系统:在分布式系统中,UUID用于唯一标识各个节点、实体或资源。由于UUID的生成是分布式的,不需要中央协调机构,因此非常适合在分布式环境中进行准确的识别和跟踪。
  3. Web开发:在Web开发中,UUID可以用作会话标识符、临时文件名或URL的一部分,用于跟踪用户会话、生成唯一的资源标识符等。
  4. 软件开发:在软件开发中,UUID可用于生成唯一的文件名、标识插件或组件、识别对象实例等。这有助于确保软件组件的唯一性和可追踪性。
  5. 数据同步和复制:在数据同步和复制过程中,UUID可以用于标识不同数据源或副本中的记录,确保数据在多个系统之间的一致性和唯一性。

此外,UUID还适合在不需要明确时间上下文或排序的场景中使用。例如,在微服务架构中,UUID可以确保全局ID的唯一性,避免主键自增ID的一些缺陷。然而,需要注意的是,UUID并不适合作为需要频繁排序或具有明确时间顺序要求的场景中的主键,因为UUID是无序的。在这些情况下,可以考虑使用其他类型的标识符(如时间戳或自增ID)。

总之,UUID提供了一种可靠的方法来生成全局唯一的标识符,适用于分布式系统、数据库管理、软件开发以及其他需要唯一标识的场景。但在使用时,也需要根据具体的应用场景和需求来权衡其优缺点。


    @Test
    public void uuidExample(){

        //生成一个随机的UUID(第4版)
        UUID uuid = UUID.randomUUID();

        System.out.println("Generated UUID:"+uuid.toString());

        //也可以从字符串中解析UUID
        String uuidString = "f47ac10b-58cc-4372-a567-0e02b2c3d479";
        UUID parsedUUID = UUID.fromString(uuidString);

        // 输出解析后的UUID
        System.out.println("Parsed UUID: " + parsedUUID);
    }

shortuuid

ShortUUID 是一种用于生成全局唯一标识符(GUID)的算法和格式,其特别之处在于生成的标识符比传统的 UUID(Universally Unique Identifier)更短,且长度固定为22个字符。ShortUUID 是基于 UUID Version 4 设计的,并使用了特定的 alphabet(字符集)来缩短表示长度。

组成与生成步骤

  1. 初始值

    • ShortUUID 的初始值基于 UUID Version 4。UUID Version 4 是一种基于随机数的 UUID,其生成过程中包含了足够的随机性以确保全局唯一性。
  2. Alphabet 变量长度

    • ShortUUID 使用了一个预定义的 alphabet,其长度固定为 57 个字符。这个 alphabet 通常由小写字母、大写字母和数字组成,有时还可能包含一些特殊字符,以提供足够的字符组合空间。
  3. ID 长度计算

    • 尽管 ShortUUID 的最终长度是固定的22个字符,但实际上,这个长度并不直接由 alphabet 的长度计算得出。相反,它是基于所需的唯一性级别和可接受的冲突概率来确定的。需要注意的是,将 128 位的 UUID 压缩到 22 个字符中,必然会导致一定的信息丢失和冲突风险。
  4. DivMod 映射

    • ShortUUID 使用 DivMod(欧几里得除法和模)算法来将 UUID 的数值映射到预定义的 alphabet 上。这个过程涉及将 UUID 转换为一个大整数,然后反复应用 DivMod 算法来生成一系列索引值,这些索引值随后被转换为 alphabet 中的对应字符。

特点

  • 全局唯一性:尽管 ShortUUID 比传统的 UUID 短得多,但它仍然旨在提供全局唯一性。然而,由于信息压缩,ShortUUID 的唯一性不如完整长度的 UUID。
  • 长度固定:ShortUUID 的长度固定为 22 个字符,这使得它在存储和传输时更加高效。
  • 基于 UUID:ShortUUID 是基于 UUID Version 4 设计的,因此它继承了 UUID 的一些优点,如跨平台兼容性和广泛的接受度。
  • 冲突风险:由于 ShortUUID 的长度较短且信息被压缩,因此存在比传统 UUID 更高的冲突风险。这种风险在高并发或大规模应用中尤为显著。
  • 不可逆性:ShortUUID 的生成过程是不可逆的,即无法从 ShortUUID 还原出原始的 UUID。

应用
ShortUUID 适用于那些需要唯一标识符但又希望减少存储和传输开销的场景。然而,由于其潜在的冲突风险,使用 ShortUUID 时需要谨慎评估其适用性,特别是在对唯一性要求极高的系统中。常见的应用场景包括短链接生成、内部标识符等。在这些场景中,ShortUUID 提供了一种在可接受的冲突概率下减少标识符长度的有效方法。

 public  static String  generateShortUuid(){
        StringBuffer  shortBuffer = new StringBuffer();
        String uuid = UUID.randomUUID().toString().replace("-","");
        for (int i=0;i<8;i++){
            String str = uuid.substring(i\*4,i\*4+4);
            int x = Integer.parseInt(str,16);
            shortBuffer.append(chars[x % 0x3E]);

        }
        return shortBuffer.toString();
 }

KSUID

KSUID是由Segment.io开发的一种分布式ID生成方案。它的设计目标是为了提供高性能、唯一性,并确保ID的可排序性。KSUID生成的ID是一个全局唯一的字符串,这使得它非常适用于各种需要唯一标识符的场合。

组成

  1. 时间戳(32位)

    • 使用32位来存储秒级的时间戳。
    • 表示自协调世界时(UTC)1970年1月1日以来的秒数。
    • 与传统的UNIX时间戳相比,KSUID使用了更长的时间戳,因此可以支持更长的时间范围。
  2. 随机字节(16位)

    • 这部分是为了增加ID的唯一性而随机生成的16位字节。
  3. 附加信息(可选)

    • KSUID的格式允许包含附加的信息,例如节点ID或其他标识符。
    • 这部分是可选的,具体是否使用取决于特定的应用场景和需求。

特点

  • 全局唯一性:由于KSUID结合了时间戳和随机字节,它生成的ID在全球范围内都是唯一的。
  • 可排序性:由于KSUID的ID是按照时间顺序生成的,因此它们可以很方便地按照生成的顺序进行排序和比较。
  • 去中心化:KSUID不依赖于任何中央化的ID生成服务,这使得它在分布式系统中特别有用。
  • 高性能:KSUID的生成算法设计得非常简单和高效,确保在高并发环境下也能快速生成ID。

应用
KSUID广泛应用于需要全局唯一标识符的各种场景,特别是那些要求ID具有可排序性的场合。例如,在分布式数据库、日志记录、消息队列等领域,KSUID都是一个非常有用的工具。

XID

XID是一个用于生成全局唯一标识符(GUID)的库。它采用基于时间的、分布式的ID生成算法,旨在确保高性能和唯一性。XID生成的ID是一个64位的整数,由时间戳、机器ID和序列号三部分组成。

XID的组成

  1. 时间戳(40位)

    • 使用40位存储纳秒级的时间戳。
    • 支持约34年的时间范围。
    • 与雪花算法相比,具有更高的时间分辨率。
  2. 机器ID(16位)

    • 用于表示分布式系统中机器的唯一标识符。
    • 每个机器应具有唯一的机器ID,可以通过手动配置或自动分配获得。
  3. 序列号(8位)

    • 在同一纳秒内生成的序列号。
    • 如果在同一纳秒内生成的ID数量超过了8位能够表示的范围,会等待下一纳秒再生成ID。

XID的特点

  1. 长度短:生成的ID是一个64位的整数,相对较短,便于存储和传输。
  2. 有序:由于包含时间戳成分,生成的ID是趋势递增的,具有良好的有序性。
  3. 不重复:通过合理的分配机器ID和序列号,确保在分布式环境下生成的ID不重复。
  4. 时钟回拨处理:通过时间戳的随机数原子+1操作(但这里可能存在误解,因为通常时间戳不是随机数),可以在一定程度上避免时钟回拨问题。然而,这部分描述可能不够准确或完整,需要更多上下文来理解具体实现。

与其他算法的比较

与雪花算法相比,XID具有以下优势:

  • 更高的时间分辨率:使用纳秒级时间戳。
  • 适用于分布式环境下的ID生成需求。

然而,在唯一性方面,XID可能稍弱一些,因为它使用了较短的机器ID和序列号。这意味着在极端情况下(如大量机器在短时间内生成大量ID),可能存在ID冲突的风险。

XID库通常提供以下功能:

  1. 生成ID:根据当前时间戳、机器ID和序列号生成新的ID。
  2. 解析ID:将生成的ID解析回其组成成分,以便分析和调试。
  3. 验证ID:验证给定ID是否有效,即是否符合XID的格式和规范。

这些功能使得XID成为一个灵活且易于使用的ID生成解决方案,适用于各种分布式系统场景。

snowflake

Snowflake是Twitter开源的一种分布式ID生成算法,它的主要目标是在分布式系统中生成全局唯一的ID。Snowflake算法结合了时间戳、机器标识和序列号等元素,确保生成的ID既唯一又具有趋势递增的特性。这种设计使得Snowflake算法非常适用于需要高性能、低延迟和有序ID的场景,如数据库索引、分布式存储系统等。

Snowflake生成的ID是一个64位的整数,通常由以下几部分组成:

在这里插入图片描述

  1. 时间戳(Timestamp):占据ID的高位部分,用于记录ID生成的时间。时间戳的精度通常到毫秒级或纳秒级,这取决于具体实现。由于时间戳是递增的,因此可以保证生成的ID具有趋势递增的特性。
  2. 机器标识(Machine ID):用于标识生成ID的机器或节点。在分布式系统中,每台机器或节点都应该有一个唯一的标识,以确保不同机器生成的ID不会冲突。
  3. 数据中心标识(Data Center ID):可选的部分,用于标识生成ID的数据中心。这对于跨数据中心的分布式系统非常有用,可以确保不同数据中心生成的ID也是唯一的。
  4. 序列号(Sequence Number):在同一时间戳内,用于区分不同ID的序列号。当在同一时间戳内需要生成多个ID时,序列号可以确保这些ID的唯一性。

Snowflake算法的特点

  1. 全局唯一性:通过合理设计时间戳、机器标识和序列号的组合方式,确保在分布式系统中生成的ID是全局唯一的。
  2. 趋势递增:由于时间戳占据ID的高位部分,因此生成的ID具有趋势递增的特性。这对于数据库索引等场景非常有利,可以提高插入性能和减少索引的分裂与碎片化。
  3. 高性能与低延迟:Snowflake算法的设计目标之一就是高性能和低延迟。通过合理的位分配和算法优化,可以实现快速生成ID并降低对系统性能的影响。
  4. 安全性:与UUID相比,Snowflake算法不会暴露MAC地址等敏感信息,因此更安全。同时,生成的ID也不会过于冗余,可以节省存储空间和网络带宽。

Snowflake算法适用于需要在分布式环境下生成唯一ID的场景,如:

  1. 数据库主键生成:在分布式数据库中,可以使用Snowflake算法生成主键ID,确保不同节点生成的主键不会冲突。
  2. 分布式存储系统:在分布式存储系统中,可以使用Snowflake算法为文件或对象生成唯一的标识符。
  3. 消息队列:在分布式消息队列中,可以使用Snowflake算法为消息生成唯一的ID,以便进行追踪和排序。
  4. 日志系统:在分布式日志系统中,可以使用Snowflake算法为日志条目生成唯一的ID,方便进行日志聚合和查询。

Snowflake是一种高性能、低延迟和趋势递增的分布式ID生成算法。它结合了时间戳、机器标识和序列号等元素,确保生成的ID既唯一又具有有序性。Snowflake算法适用于需要在分布式环境下生成唯一ID的场景,如数据库索引、分布式存储系统等。与UUID相比,Snowflake算法更安全且生成的ID更简洁。

由于雪花算法的一部分id序列是基于时间戳的, 那么就会存在时钟回拨的问题。

什么是时钟回拨问题呢。 首先我们来看下服务器上的时间突然退回之前的时间:

  • 可能是人为调整时间,
  • 也可能是服务器之间的时间校对。

具体来说,时钟回拨(Clock Drift) 指的是系统时钟在某个时刻向回调整, 即时间向过去移动。 时钟回拨可能发生在分布式系统中的某个节点上, 这可能是由于时钟同步问题、时钟漂移或其他原因导致的。

时钟回拨可能对系统造成一些问题, 特别是对于依赖与时间顺序的应用程序或算法。

在分布式系统中, 时钟回拨可能导致一下问题

  • ID 冲突: 如果系统使用基于时间的算法生成唯一ID(如雪花算法),时钟回拨可能导致生成的ID与之前生成的ID冲突,破坏了唯一性。
  • 数据不一致:时钟回拨可能导致不同节点之间的时间戳不一致,这可能影响到分布式系统中的时间相关操作,如事件排序、超时判断等。数据的一致性可能会受到影响。
  • 缓存失效:时钟回拨可能导致缓存中的过期时间计算错误,使得缓存项在实际过期之前被错误地认为是过期的,从而导致缓存失效。

为了应对时钟回拨问题,可以采取以下措施

  • 使用时钟同步服务:通过使用网络时间协议(NTP) 等时钟同步服务,可以将节点的时钟与参考时钟进行同步,减少时钟回拨的可能性。
  • 引入时钟漂移校正:在分布式系统中,可以通过周期性地校正节点的时钟漂移,使其保持与其他节点的时间同步。
  • 容忍时钟回拨:某些应用场景下,可以容忍一定范围的时钟回拨。在设计应用程序时,可以考虑引入一些容错机制,以适应时钟回拨带来的影响。

总之, 时钟回拨是分布式系统中需要关注的一个问题, 可能对系统的时间相关操作、数据一致性和唯一ID生成等方面产生影响。

通过使用时钟同步服务、时钟漂移校正和容忍机制等方法, 可以减少时钟回拨带来的问题。

参考leaf, snowflake本身的容错有两点,一是防止自身节点时钟回拨, 另一点是防止节点自身时钟的不正确。

  • 防止节点自身时钟回拨

Snowflake通过定时上报当前时间并在etcd或zookeeper等分布式协调服务中记录节点上次的时间来解决时钟回拨问题。当节点启动时,它会根据节点ID从etcd或zookeeper中取回之前的时间。如果检测到时钟回拨,Snowflake会采取相应的措施。如果回拨时间很少,Snowflake可以选择等待回拨时间过后,再正常启动。如果回拨过大,节点将直接启动失败并报错,此时需要人为介入处理。

此外,Snowflake还采用了一种策略来避免新节点和旧节点之间的时间冲突风险。当节点定时上报时间时,它可以选择上报当前时间加上一个时间间隔(now+interval)的方式。这样,新节点需要超过这个时间戳才能启动,从而避免了时间冲突的问题。

  • 防止节点时钟不正确

为了降低时钟错误的风险,Snowflake要求每个节点都会定期上报自己的节点信息(IP/Port)到etcd或zookeeper,并提供一个RPC方法以供外界获取本节点的时间戳。当一个新节点启动时,它会通过etcd或zookeeper注册的其他节点信息,并发调用RPC方法获取其他节点的时间戳,并进行一一对比。如果时间戳差异过大,则代表本节点时间戳可能有问题,直接报错并需要人为介入处理。

这种解决方案的准确性相对较高,因为它不是简单地取各个节点上报的时间戳进行判断,而是通过实时获取其他节点的时间戳进行对比。这可以减少由于各节点定期上报时间戳导致的时间差异,并提高判断时间偏差的准确性。

至于第一个节点时间戳错误的情况,虽然发生的几率较低,但Snowflake的解决方案会在启动正常节点时报错并需要人为介入。在这种情况下,可以停掉异常节点,然后逐个启动正常的新节点。第一个新节点启动时,由于etcd或zookeeper内没有其他节点信息,无需进行校验。

总的来说,Snowflake的时钟回拨解决方案通过结合定时上报时间、分布式协调服务和实时时间戳对比等方法,有效地减少了时钟回拨和时钟错误对分布式系统的影响。

Q: 为什么不采用把各个节点上报时间戳到etcd,新启动节点直接取 etcd 内的时间戳进行逐个判断呢?

主要考虑时间校准的准确性, 如果各节点定期上报时间戳, 各节点时间戳差异会比较大, 这会导致我们判断时间偏差的幅度不较大,准确性会下降。

Q: 如果第一个节点时间戳是错误的, 后续正确节点启动怎么办?

首先,这种情况发生的几率非常低并且此时我们启动正常节点时肯定会报错,人为介入。

报错时,直接停掉异常节点,然后逐个启动正常的新节点,第一个新节点启动时, etcd 内也没有其他节点信息,无需校验。

利用zookeeper 解决时钟回拨问题:

在使用ZooKeeper解决Snowflake时钟回拨问题时,我们主要利用ZooKeeper的分布式协调功能来同步和校验各个Snowflake节点的时间戳。以下是一个详细的解决方案:

  1. 节点时间上报与同步

步骤一: 每个Snowflake节点在启动时或定期(如每分钟)向ZooKeeper上报其当前的时间戳。这个时间戳可以包含节点的IP地址、端口号和时间戳值。

步骤二: ZooKeeper将这些时间戳存储在其数据结构中,例如使用ZNode来存储每个节点的时间戳信息。

  1. 节点时间校验

当一个新的Snowflake节点启动时,或者在运行过程中检测到可能的时钟回拨时,该节点会执行以下操作来进行时间校验:

步骤一: 节点从ZooKeeper中获取其他所有节点的时间戳信息。

步骤二: 节点比较自己的时间戳与其他节点的时间戳。如果发现自己的时间戳明显落后于其他节点(超过一个预设的阈值,如5分钟),则可能存在时钟回拨问题。

步骤三: 如果检测到时钟回拨,节点可以采取以下策略之一:

  • 等待策略:节点可以等待一段时间(超过回拨的时间差),然后再次尝试启动或继续操作。
  • 报错并停止:节点可以立即报错并停止运行,通知管理员进行手动干预。
  • 自动调整时间:在某些情况下,节点可以尝试自动调整其系统时间以与其他节点同步。但这通常不推荐,因为直接修改系统时间可能导致其他问题。

注意事项

  • 网络延迟:由于网络延迟的存在,不同节点之间的时间戳可能会有微小的差异。因此,在设置时间差阈值时需要考虑这一因素。
  • ZooKeeper的性能:ZooKeeper的性能和稳定性对于此解决方案至关重要。如果ZooKeeper集群出现问题,可能会影响到Snowflake节点的正常运行。
  • 安全性:确保ZooKeeper集群的安全性,防止恶意节点上报错误的时间戳信息。

优化策略

  • 使用更精确的时间同步协议:例如,可以使用NTP(网络时间协议)或PTP(精确时间协议)来同步节点的时间,而不是完全依赖ZooKeeper。
  • 增加时间戳上报的频率:通过更频繁地上报时间戳,可以更快地检测到时钟回拨问题。
  • 实现自动恢复机制:在检测到时钟回拨后,可以自动尝试重启节点或重新同步时间,以减少人工干预的需要。

总的来说,使用ZooKeeper来解决Snowflake时钟回拨问题是一个可行的方案,但需要根据实际情况进行配置和优化。

分布式id

数据库自增ID

在数据库设计中,主键自增索引是一种常见且方便的策略,用于为表中的每一行分配一个唯一的标识符。这种策略在多种数据库系统中都有支持,如MySQL、PostgreSQL、SQL Server等。主键自增索引不仅简化了数据插入的过程,还在某些场景下优化了数据存储和检索的性能。然而,它也有一些潜在的问题和限制,特别是在高并发和大数据量的环境中。

主键自增索引的特点

  1. 架构简单,易于实现:主键自增是最直接的ID生成策略之一。数据库负责为新插入的行生成唯一的ID,开发者无需编写额外的逻辑来生成或管理这些ID。
  2. ID有序递增,IO写入连续性好:由于ID是顺序生成的,数据的物理存储往往也是连续的,这有助于减少磁盘碎片,提高IO性能。
  3. INT和BIGINT类型占用空间较小:相比其他更复杂的主键生成策略(如UUID),INT和BIGINT类型的自增主键占用的存储空间较小。
  4. 易暴露业务量:由于ID是顺序递增的,外部观察者可以通过分析ID的增长速度来估算系统的业务量。
  5. 受到数据库性能限制:在高并发场景下,单一数据库实例可能无法快速生成和处理大量的自增ID,这可能成为系统的性能瓶颈。

主键自增索引的问题与挑战

  1. 主键冲突:虽然理论上BIGINT类型的自增主键可以支持非常大的数据量(2^64-1),但实际上,单个数据库表很难达到这个极限。然而,在分表或数据库迁移等场景中,如果不小心处理,可能会出现主键冲突的情况。例如,当两个表的自增主键序列意外地合并到一个表中时,就可能出现重复的ID。
  2. 扩展性问题:随着业务量的增长,单一数据库实例可能无法满足性能需求。虽然可以通过分库分表来扩展系统的处理能力,但这会增加系统的复杂性和维护成本。此外,分库分表后如何保证全局唯一的主键也是一个需要解决的问题。
  3. 安全性考虑:由于自增主键是顺序生成的,攻击者可能会利用这一点来探测系统的漏洞或进行其他形式的攻击。例如,他们可以尝试通过递增的ID来访问未授权的数据。

适用场景

  1. 中小规模应用:对于中小规模的应用,主键自增索引是一种简单且有效的选择,可以满足基本的数据存储和检索需求。
  2. 低并发场景:在低并发场景下,主键自增索引的性能瓶颈不明显,可以提供较好的性能表现。
  3. 业务逻辑简单:对于业务逻辑相对简单的应用,主键自增索引可以简化开发过程,提高开发效率。

需要注意的是,在选择主键自增索引时,应充分考虑其优缺点以及具体的业务需求和数据量。对于需要高并发处理、大数据量存储或复杂业务逻辑的应用,可能需要考虑其他更合适的主键生成策略。同时,在使用主键自增索引时,还需要注意数据库的性能监控和优化,以确保系统的稳定性和性能表现。

redis 分布式id

在分布式系统中,生成全局唯一的ID是一个常见的需求。相比数据库自增ID,使用Redis的原子操作(如INCR和INCRBY)来生成ID具有更好的性能和灵活性。Redis作为内存数据库,其读写速度远超传统磁盘数据库,且提供了丰富的原子操作,非常适合用于生成分布式ID。

然而,使用Redis作为ID生成器也存在一些挑战,如架构强依赖Redis可能导致单点问题,以及在流量较大的场景下网络耗时可能成为瓶颈。因此,在使用Redis生成分布式ID时,需要综合考虑系统架构、性能需求和网络环境等因素。

实现步骤

  1. 选择合适的Redis原子操作:INCR和INCRBY是Redis提供的两个原子操作,用于增加key对应的值。INCR将key的值增加1,而INCRBY可以将key的值增加指定的整数。根据具体需求选择合适的操作。
  2. 设置初始值和步长:在使用Redis生成ID之前,需要设置初始值和步长。初始值通常是0或1,步长可以根据需要进行设置。步长越大,每次生成的ID间隔越大,但可能会浪费更多的ID。
  3. 使用Redis客户端进行操作:在Java中,可以使用Redis客户端库(如Lettuce)来连接Redis并执行原子操作。Lettuce是一个高性能、线程安全的Redis客户端,支持同步、异步和响应式编程模型。
  4. 处理网络延迟和单点问题:为了降低网络延迟的影响,可以将Redis部署在与应用服务器相同的网络环境中。同时,为了避免单点问题,可以使用Redis集群或哨兵模式来提高可用性和容错性。
  5. 代码实现与测试:根据具体需求编写Java代码实现ID生成器,并进行充分的测试以确保其正确性和性能。

使用Lettuce客户端实现Redis分布式ID生成器

  1. 添加Lettuce依赖

首先,在项目的pom.xml(如果是Maven项目)或build.gradle(如果是Gradle项目)中添加Lettuce的依赖。

  1. 配置Redis连接

配置Redis连接,包括主机名、端口、密码(如果有)以及集群配置(如果使用Redis集群)。

  1. 实现ID生成器

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  1. 代码实现与测试:根据具体需求编写Java代码实现ID生成器,并进行充分的测试以确保其正确性和性能。

使用Lettuce客户端实现Redis分布式ID生成器

  1. 添加Lettuce依赖

首先,在项目的pom.xml(如果是Maven项目)或build.gradle(如果是Gradle项目)中添加Lettuce的依赖。

  1. 配置Redis连接

配置Redis连接,包括主机名、端口、密码(如果有)以及集群配置(如果使用Redis集群)。

  1. 实现ID生成器

[外链图片转存中…(img-3TbH8zZj-1715735353402)]
[外链图片转存中…(img-yHtbSXv2-1715735353402)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 12
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值