为什么在京东面试中需要理解分布式ID的重要性?深度解析分布式ID的实现方法与应用场景

今天,我将与大家分享一位读者在京东面试中遇到的一个问题:“为什么需要分布式ID?在你的项目中,你是如何实现的?”在这篇文章中,我将探讨分布式ID的相关知识,包括其基本要求及常见解决方案。

分布式ID的基础知识

ID的定义

在日常开发中,我们需要使用ID对系统中的数据进行唯一标识。例如,用户ID对应唯一的用户,商品ID对应具体的商品,订单ID则对应特定的订单。在生活中,我们也使用各种ID,例如身份证ID对应唯一的个人,地址ID对应特定的地点。

简而言之,ID是数据的唯一标识符

分布式ID的概念

分布式ID是在分布式系统中使用的ID。与现实生活中的ID不同,分布式ID是计算机系统中的一个概念。

让我举一个分库分表的例子。在某个项目中,我们最初使用的是单机MySQL。然而,项目上线一个月后,随着用户数量的增加,系统数据量迅速膨胀,单机MySQL已经无法支持了,因此需要进行分库分表(推荐使用Sharding-JDBC)。

在分库后,数据分散在不同服务器的数据库中,数据库的自增主键无法满足全局唯一性的要求。**在这种情况下,我们如何为不同的数据节点生成全局唯一的主键呢?**这时,分布式ID的生成就显得尤为重要。

分布式ID的基本要求

分布式ID是分布式系统中的关键组成部分,广泛应用于各个地方。一个基本的分布式ID生成方案需要满足以下要求:

  • 全局唯一性:分布式ID必须在全局范围内唯一。
  • 高性能:ID的生成速度要快,并且对本地资源的消耗要小。
  • 高可用性:生成ID的服务要尽可能保持高可用性。
  • 易于使用:应当方便接入,能够快速上手。

此外,理想的分布式ID还应具备以下特性:

  • 安全性:ID中不应包含任何敏感信息。
  • 有序递增:当将ID存储到数据库时,ID的有序性能够提升数据库的写入效率,且我们还可能通过ID进行排序。
  • 业务含义:生成的ID如果能与具体业务相关联,将有助于问题定位和开发透明化。
  • 独立部署:分布式ID生成服务应当独立于其他业务相关服务,尽管这会增加网络调用的消耗,但在需求较高的场景下,独立的发号器服务是必要的。

常见的分布式ID解决方案

数据库

主键自增方式

此方式是最简单的,通过关系型数据库的自增主键生成唯一ID。例如,在MySQL中,可以通过以下步骤实现:

  1. 创建一个数据库表:
CREATE TABLE `sequence_id` (  
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,  
  `stub` char(10) NOT NULL DEFAULT '',  
  PRIMARY KEY (`id`),  
  UNIQUE KEY `stub` (`stub`)  
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;  
  1. 使用replace into插入数据并获取ID:
BEGIN;  
REPLACE INTO sequence_id (stub) VALUES ('stub');  
SELECT LAST_INSERT_ID();  
COMMIT;  

这种方式的优缺点明显:

  • 优点:实现简单,ID有序递增,存储空间消耗小。
  • 缺点:并发支持有限,存在数据库单点故障风险(可通过数据库集群解决,但会增加复杂性),ID缺乏业务含义,获取ID时需访问数据库(增加压力,速度较慢)。

数据库号段模式

由于主键自增模式在ID需求较大时存在性能瓶颈,我们可以采用号段模式,批量获取并存储在内存中,从而提高性能。

以MySQL为例,步骤如下:

  1. 创建数据库表:
CREATE TABLE `sequence_id_generator` (  
  `id` int(10) NOT NULL,  
  `current_max_id` bigint(20) NOT NULL COMMENT '当前最大id',  
  `step` int(10) NOT NULL COMMENT '号段的长度',  
  `version` int(20) NOT NULL COMMENT '版本号',  
  `biz_type` int(20) NOT NULL COMMENT '业务类型',  
  PRIMARY KEY (`id`)  
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;  
  1. 插入一行数据:
INSERT INTO `sequence_id_generator` (`id`, `current_max_id`, `step`, `version`, `biz_type`)  
VALUES  
  (1, 0, 100, 0, 101);  
  1. 通过SELECT获取批量唯一ID:
SELECT `current_max_id`, `step`, `version` FROM `sequence_id_generator` WHERE `biz_type` = 101  
  1. 更新以获取新的ID:
UPDATE sequence_id_generator SET current_max_id = current_max_id + 100, version = version + 1 WHERE version = 0 AND `biz_type` = 101  
SELECT `current_max_id`, `step`, `version` FROM `sequence_id_generator` WHERE `biz_type` = 101  

数据库的号段模式相较自增主键方式,减少了对数据库的访问次数,减轻了数据库的压力。为了避免单点故障,可以使用主从模式提高可用性。

NoSQL

在大多数情况下,使用Redis来生成分布式ID也非常流行。通过Redis的incr命令,可以轻松实现ID的原子递增:

127.0.0.1:6379> set sequence_id_biz_type 1  
OK  
127.0.0.1:6379> incr sequence_id_biz_type  
(integer) 2  
127.0.0.1:6379> get sequence_id_biz_type  
"2"  

为了提高可用性和并发性能,可以采用Redis Cluster或开源的Codis集群方案。

算法

UUID

UUID(通用唯一标识符)包含32个16进制数字。可以通过JDK轻松生成UUID:

UUID uuid = UUID.randomUUID();  

虽然UUID可以保证唯一性,但其存储空间大(128位),且无序性会影响数据库性能,因此在使用MySQL作为主键时并不常见。

Snowflake(雪花算法)

Snowflake是Twitter开源的分布式ID生成算法,由64位二进制数字组成,具有良好的性能和灵活性。它将ID分为几个部分,包括时间戳、机器ID及序列号。

开源框架

  • UidGenerator(百度):基于雪花算法的唯一ID生成器,提供定制的ID组成结构。
  • Leaf(美团):支持号段模式和雪花算法,解决了多个业务线的ID生成问题。
  • Tinyid(滴滴):基于数据库号段模式,具有双号段缓存机制以提高性能。

结论

通过本文,我总结了分布式ID生成的常见解决方案。除了以上介绍,像ZooKeeper这样的中间件也可以被用来生成唯一ID。没有完美的解决方案,选择最合适的方式需结合实际项目需求。

在面试中,面试官可能会结合具体的业务场景来考察你对分布式ID设计的理解,你可以参考这篇文章:分布式ID设计指南。

参考资料

  1. Tinyid: GitHub
  2. Codis: GitHub
  3. RFC 4122: IETF
  4. UUID介绍: 维基百科
  5. Seata基于改良版雪花算法的分析: Seata官网
  6. Leaf: GitHub
  7. Tinyid: GitHub
  8. IdGenerator: [GitHub](