前言
大家好,我是田螺。
我们日常开发中,经常需要使用到分布式ID。我们系统一般都是分布式部署的,一些分布式锁、幂等、数据库的唯一键,都需要分布式ID。
今天田螺哥盘点一些常见的分布式唯一ID生成方案。
1. 数据库自增ID
原理:利用数据库自增字段(如MySQL的AUTO_INCREMENT
)生成唯一ID
优点:简单易用、ID有序、索引效率高缺点:单点故障、扩展性差(分库分表困难)
适用场景:单机或简单主从架构系统
代码示例:
CREATE TABLE orders (
id INT AUTO_INCREMENT PRIMARY KEY,
order_data VARCHAR(255)
);
2. UUID
-
原理:基于MAC地址、时间戳、随机数生成128位字符串
-
优点:全局唯一、无需中心化服务
-
缺点:无序导致索引效率低、存储空间大(36字符)
-
适用场景:日志跟踪、非核心业务(如临时会话)
我们的项目中,有些伙伴为了简单方便,有时候会直接用它,如果业务性比较强的,就在它后缀拼接写个性化标记(业务标记)进来~
代码示例:
import java.util.UUID;
String uuid = UUID.randomUUID().toString();
3. 雪花算法(Snowflake)
-
原理:64位结构 = 时间戳(41位) + 机器ID(10位) + 序列号(12位)
-
优点:高性能(单机每秒4万+)、趋势递增29
-
缺点:依赖时钟同步(时钟回拨会导致重复)
-
适用场景:分布式高并发系统(如电商订单)
其实,我们现在的系统,很多场景就是用雪花算法生成的,如流水号等等~
代码示例:
public class Snowflake {
private long machineId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public synchronized long nextId() {
long timestamp = System.currentTimeMillis();
if (timestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨!");
}
if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & 4095; // 12位序列号
if (sequence == 0) timestamp = tilNextMillis(lastTimestamp);
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return (timestamp << 22) | (machineId << 12) | sequence;
}
}
4. 数据库号段模式
-
原理:批量获取ID段(如一次取1000个),减少数据库访问
-
优点:降低数据库压力、可用性高(缓存号段)、速度快
-
缺点:在服务器重启或故障转移等情况下,可能会导致ID的生成出现不连续的情况。
-
适用场景:中等并发业务(如用户ID生成)
我们的一些客户号,当前是用号段模式生成的,然后拼一些业务标记
表结构:
CREATE TABLE id_segment (
biz_tag VARCHAR(50) PRIMARY KEY,
max_id BIGINT NOT NULL,
step INT NOT NULL,
version INT NOT NULL
);
5. Redis分布式ID
-
原理:利用INCR原子操作生成递增ID
-
优点:性能优于数据库、天然有序、高性能、可扩展性强
-
缺点:依赖Redis可用性
-
适用场景:按日生成的流水号(如订单号=日期+自增)
代码示例:
Jedis jedis = new Jedis("redis-host");
Long orderId = jedis.incr("order:20240526");
6.百度的uid-generator
优点:避免频繁生成、吞吐量提升至600万/秒
适用场景:超大规模分布式系统
基于Twitter的Snowflake算法进行改进,增加了更多的配置和灵活性。
与原始的snowflake算法不同在于,uid-generator支持自定义时间戳、工作机器ID和 序列号 等各部分的位数,而且uid-generator中采用用户自定义workId的生成策略。
代码示例:
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);
}
}
7. 基于Zookeeper的顺序节点
利用Zookeeper的顺序节点特性来生成全局唯一ID。
优点:
-
利用Zookeeper的集群特性保证高可用。
-
ID全局唯一。
缺点:
-
需要依赖Zookeeper集群。
-
可能会受到Zookeeper性能的限制。
-
并发竞争较大不适合用Zookeeper
8. 数据库集群模式
单库的数据库自增ID会存在单点问题,所以可以用数据库集群模式,去解决这个问题。数据库集群模式:通过多个数据库实例设置不同的起始值和步长来生成全局唯一的ID。
数据库集群模式优点:
-
可以有效生成集群中的唯一ID。解决了单点的问题。
-
降低ID生成数据库操作的负载。
数据库集群模式缺点:
-
需要独立部署多个数据库实例,成本高。
-
后期不方便扩展
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);
}
}
});
}
}
-
优缺点:简单、轻量级,但性能可能不如其他方案。