前言
日常工作中,我们开发的系统、或者中间件,都是分布式部署的。比如你的订单数据库,做了分库分表,这时候,你需要一个唯一的ID来标记一条数据。这时候,就需要分布式ID。分布式ID是在分布式系统下使用的ID,用于在多个节点中生成全局唯一的标识符。
1. UUID
UUID是通用唯一识别码(Universally Unique Identifier)
的缩写。它通过一定的算法机器生成,包括网卡MAC地址、时间戳、名字空间(Namespace)、随机或伪随机数、时序等元素。
代码示例:
UUID randomUUID = UUID.randomUUID();
System.out.println(randomUUID);
//输出示例:a4332986-c548-4a39-9202-c82e2ea3b455
UUID的优点:
-
本地生成ID,不需要进行远程调用,时延低,性能高。
-
全球唯一,数据迁移容易。
UUID的缺点:
-
UUID过长,生成的是字符串,存储空间高。
-
不是有序的,无法保证趋势递增。
-
可读性差,不体现业务信息。
2. 数据库自增ID
利用数据库的自增策略,如MySQL的auto_increment,来实现分布式ID。
数据库自增ID的优点:
-
主键自动增长,不用手工设值。
-
数字型,占用空间小、检索有利。
-
绝对有序。
数据库自增ID的缺点:
-
并发性能不高,受限于数据库性能。
-
不太适合重构的系统,因为涉及旧数据迁移容易ID冲突。
-
暴露商业信息,例如可以推断出来订单量。
-
存在单点问题
3. 数据库集群模式
单库的数据库自增ID会存在单点问题,所以可以用数据库集群模式,去解决这个问题。数据库集群模式:通过多个数据库实例设置不同的起始值和步长来生成全局唯一的ID。
数据库集群模式优点:
-
可以有效生成集群中的唯一ID。解决了单点的问题。
-
降低ID生成数据库操作的负载。
数据库集群模式缺点:
-
需要独立部署多个数据库实例,成本高。
-
后期不方便扩展
4. 数据库号段模式
数据库号段模式:直接从数据库生成批量的ID到本地,直到本地ID号段用完再申请。
号段模式的表结构,可以这么设计:
CREATE TABLE id_generator (
id INT(10) NOT NULL,
max_id BIGINT(20) NOT NULL COMMENT '当前最大id',
step INT(20) NOT NULL COMMENT '号段的长度',
biz_type INT(20) NOT NULL COMMENT '业务类型',
version INT(20) NOT NULL COMMENT '版本号, 用于乐观锁',
PRIMARY KEY (`id`)
);
数据库号段模式的优点:
-
速度快。
-
如果生成ID的服务器宕机,本地ID可以支撑一段时间。
数据库号段模式的缺点:
-
如果生成ID的服务器宕机且本地ID用尽,会出现ID大量空的情况。
-
存储空间高。
-
在服务器重启或故障转移等情况下,可能会导致ID的生成出现不连续的情况。
5.Redis实现的分布式ID
我们可以利用,Redis的自增命令incr生成全局唯一ID。具体实现方式是:在Redis中维护一个自增的计数器,每次生成ID时,从Redis中获取计数器的值,然后将其加一并更新回Redis。
这种方式实现的分布式ID,是唯一的,并且是有序的,我们可以拼接时间、机器标识、业务类型等,让这个ID更有特殊意思。
Redis实现的分布式ID优点:
-
高性能、可扩展性强
-
唯一有序的
Redis实现的分布式ID缺点:
-
需要依赖Redis集群,否则存在单点问题。
-
可能存在ID冲突的风险(如果Redis集群设计不当)。
6. Snowflake雪花算法
雪花算法是一种生成分布式全局唯一ID的算法,生成的ID称为Snowflake IDs。这种算法由Twitter创建,并用于推文的ID。
一个Snowflake ID有64位。
-
第1位:Java中long的最高位是符号位代表正负,正数是0,负数是1,一般生成ID都为正数,所以默认为0。
-
接下来前41位是时间戳,表示了自选定的时期以来的毫秒数。
-
接下来的10位代表计算机ID,防止冲突。
-
其余12位代表每台机器上生成ID的序列号,这允许在同一毫秒内创建多个Snowflake ID。
Snowflake雪花算法的优点:
-
生成的ID全局唯一、趋势递增。
-
性能高,可扩展性强。
Snowflake雪花算法的缺点:
-
需要时钟回拨处理机制。
-
依赖机器ID和数据中心ID的分配。
7. 基于Zookeeper的顺序节点
利用Zookeeper的顺序节点特性来生成全局唯一ID。
优点:
-
利用Zookeeper的集群特性保证高可用。
-
ID全局唯一。
缺点:
-
需要依赖Zookeeper集群。
-
可能会受到Zookeeper性能的限制。
-
并发竞争较大不适合用Zookeeper
8.百度的uid-generator
基于Twitter的Snowflake算法进行改进,增加了更多的配置和灵活性。
与原始的snowflake算法不同在于,uid-generator支持自定义时间戳、工作机器ID和 序列号 等各部分的位数,而且uid-generator中采用用户自定义workId的生成策略。
代码demo:
import com.baidu.fsg.uid.UidGenerator;
import com.baidu.fsg.uid.impl.CachedUidGenerator;
public class UidGeneratorDemo {
public static void main(String[] args) {
// 创建一个UidGenerator实例
UidGenerator uidGenerator = new CachedUidGenerator();
// 初始化,这里只是一个简单的示例,实际使用时你可能需要根据你的业务场景进行更复杂的配置
// 例如,设置workerId、epoch等
// 注意:在多实例部署时,每个实例的workerId必须唯一
long workerId = 1L; // 示例ID,实际使用时需要保证每个实例的唯一性
long datacenterId = 1L; // 数据中心ID,示例
uidGenerator.init(workerId, datacenterId, null);
// 生成一个UID
long uid = uidGenerator.getUID();
System.out.println("Generated UID: " + uid);
}
}
-
优缺点:类似Snowflake算法,但配置更灵活。
9. 美团(Leaf)
Leaf是美团点评开源的分布式ID生成系统,包含基于数据库和基于Zookeeper的两种实现方式。
以基于数据库的自增ID生成策略为例(数据库表结构):
CREATE TABLE leaf_alloc (
biz_tag VARCHAR(128) NOT NULL COMMENT '业务key',
max_id BIGINT(20) NOT NULL COMMENT '当前已分配的最大id',
step INT(11) NOT NULL COMMENT '每次id的增长步长',
PRIMARY KEY (biz_tag)
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4;
Java 实现:
import java.sql.*;
public class LeafIdGenerator {
private static final String JDBC_URL = "jdbc:mysql://localhost:3306/your_database?useSSL=false&serverTimezone=UTC";
private static final String USERNAME = "your_username";
private static final String PASSWORD = "your_password";
private static final String UPDATE_SQL = "UPDATE leaf_alloc SET max_id = max_id + ? WHERE biz_tag = ?";
private static final String SELECT_SQL = "SELECT max_id FROM leaf_alloc WHERE biz_tag = ? FOR UPDATE";
public synchronized long getId(String bizTag) throws SQLException {
Connection conn = null;
PreparedStatement updateStmt = null;
PreparedStatement selectStmt = null;
ResultSet rs = null;
try {
conn = DriverManager.getConnection(JDBC_URL, USERNAME, PASSWORD);
selectStmt = conn.prepareStatement(SELECT_SQL);
selectStmt.setString(1, bizTag);
rs = selectStmt.executeQuery();
if (rs.next()) {
long maxId = rs.getLong("max_id");
int step = 1000; // 假设步长为1000,你可以从数据库中读取这个值
// 假设这里只是简单演示,不检查是否超过max_id + step是否溢出
updateStmt = conn.prepareStatement(UPDATE_SQL);
updateStmt.setInt(1, step);
updateStmt.setString(2, bizTag);
updateStmt.executeUpdate();
// 返回ID区间中的一个ID,这里简单返回maxId(实际应用中可能需要更复杂的策略)
return maxId;
} else {
// 如果没有找到对应的bizTag,则需要初始化
// ... 初始化代码省略 ...
throw new RuntimeException("BizTag not found: " + bizTag);
}
} finally {
// 关闭资源,省略了异常处理
if (rs != null) rs.close();
if (selectStmt != null) selectStmt.close();
if (updateStmt != null) updateStmt.close();
if (conn != null) conn.close();
}
}
public static void main(String[] args) {
LeafIdGenerator generator = new LeafIdGenerator();
try {
long id = generator.getId("test-biz-tag");
System.out.println("Generated ID: " + id);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
优点:
-
结合了数据库和Zookeeper的优点,提供了高可用和高性能的ID生成服务。缺点:
-
就是时钟回拨问题、复杂性高。
10. 滴滴(Tinyid)
Tinyid是滴滴开源的轻量级分布式ID生成系统,它是基于号段模式原理实现的与Leaf如出一辙,每个服务获取一个号段(1000,2000]、(2000,3000]、(3000,4000]
以下是一个简化的Tinyid,服务端的伪代码:
// 假设我们有一个ID生成器,这里用AtomicLong模拟
import java.util.concurrent.atomic.AtomicLong;
public class TinyidService {
private AtomicLong idGenerator = new AtomicLong(0);
// 模拟的ID生成方法
public synchronized long generateId() {
return idGenerator.incrementAndGet();
}
// 这里应该是RESTful API的实现,但为简化起见,我们省略了HTTP部分
// 客户端应该通过HTTP请求调用此方法
public long getIdOverHttp() {
return generateId();
}
}
客户端(Java示例)
import okhttp3.*;
public class TinyidClient {
private static final String TINYID_SERVICE_URL = "http://localhost:8080/tinyid/generate";
public static void main(String[] args) {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(TINYID_SERVICE_URL)
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
e.printStackTrace();
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (!response.isSuccessful()) {
throw new IOException("Unexpected code " + response);
} else {
// 假设服务端返回的是纯文本格式的ID
String responseBody = response.body().string();
long id = Long.parseLong(responseBody);
System.out.println("Generated ID: " + id);
}
}
});
}
}
-
优缺点:简单、轻量级,但性能可能不如其他方案。