分布式环境下如何保证 ID 的唯一性
mhr18 2025-05-05 17:07 21 浏览 0 评论
前言
首先说下我们为什么需要分布式 ID,以及分布式 ID 是用来解决什么问题的。当我们的项目还处于单体架构的时候,我们使用数据库的自增 ID 就可以解决很多数据标识问题。但是随着我们的业务发展我们的架构就会逐渐演变成分布式架构,那么这个时候再使用数据的自增 ID 就不行了,因为一个业务的数据可能会放在好几个数据库里面,此时我们就需要一个分布式 ID 用来标识一条数据,因此我们需要一个分布式 ID 的生成服务。那么分布式 ID 的服务有什么要求和挑战呢?
要求
- 全局唯一:既然是用来标识数据唯一的,那么一个分布式 ID 肯定要是全局唯一的,在同一业务下的每个服务下面都是一致的,不会变的,这是一个基本的要求;
- 全局递增:递增这个也很好理解,我们要保证生成的 ID 是依次递增的,因为很多时候 ID 是给人看的,如果说不具备递增性,就缺乏了很多的可读性;
- 信息安全:分布式 ID 的安全性也很重要,因为我们提到生成的 ID 是递增的,这就有可能会给竞争对手知道我们的 ID 的生成频率,这种在电商等场景会有很大的问题,但是这个往往跟全局递增有点冲突;
- 高可用性:分布式 ID 的生成服务必须是高可用,毕竟一旦不能生成 ID,后续的所有服务都无法继续使用;
常见的分布式 ID 实现
在当下的互联网当中,根据业务场景以及需求的不同,对于分布式 ID 的实现有如下几种实现方式:
- UUID;
- Redis;
- 变形的数据库自增 ID;
- 推特雪花算法
- 美团的 Leaf——雪花算法的变形;
UUID
写 Java 的朋友对 UUID 肯定不陌生,7dbb9f04-d15e-4c88-b74b-72a35e0d7580 这是一个标准的 UUID,虽然都说 UUID 是全球唯一,具备我们前面提到的要求中的第一点,但是很显然不具备全局递增,这种分布式 ID 可读性很差,如果说只是用来记录日志或者不需要人去理解的场景是可以用,但是不适合我们这里说的业务数据的唯一标识。而且这种无序的 UUID 如果作为主键会很严重影响性能。
Redis
Redis 有个 incr 的命令,这个命令是能保证原子递增的,在某种程度上也是可以生成全局 ID,不过使用 Redis 有两个问题:
- 不美观,虽然说我们需要的是一个全局 ID,但是 incr 命令是从 1 开始的整型,所以会导致全局 ID 的长度不一致,虽然说也可以用来标识唯一业务数据,但是某些场景也缺少可读性,因为不携带日期信息;
- 依赖 Redis 的高可用,因为 Redis 是基于内存的,为了保证 ID 的不丢失所以需要对 Redis 进行持久化,但是关于 Redis 的两种持久化的方式各有优缺点,详细的可以参考公众号之前的文章 面试官:请说下 Redis 是如何保证在宕机后数据不丢失的;
数据库自增 ID
前面我们提到单个数据库在分布式环境下已经没办法使用自增 ID 了,因为每个 MySQL 的实例自增 ID 都是从 1 开始,并且步长都按照 1依次递增,这种情况下我们很容易想到是不是可以考虑给每个数据库设置不同的步长。如果我们设置了不同的步长,这样就可以保证每个数据库实例都可以生成 ID,并且不会重复。虽然简单的系统可以这样用,但是也有几个问题:
- 依赖数据库 DB,在分布式环境下,如果过多的依赖数据库是有风险的,无法支持高并发的情况,特别是对于一些电商交易的场景,每秒几十万的 QPS,数据库是扛不住的;
- 不同数据库实例的数据不能直接关联上,需要额外的存储,才能把数据串起来,增加业务复杂度;
推特的雪花算法—— snowflake
snowflake 算法是推特开源的分布式 ID 生成算法,这个算法提供了一个标准的思路,很多公司都参考这个算法做了自己的实现,比较有名的是美团的 Leaf。这里我们就着重看下雪花算法是怎么实现的。
感兴趣的可以去参考文章 https://tech.meituan.com/2017/04/21/mt-leaf.html 看下美团的 leaf 的实现原理。
雪花算法的思想是化整为零,将分布式 ID 的生成分散到每个机房和机器上,采用一个 64 位 long 类型的的结构来表示一个 ID,64 的结构如下所示,第一位符号位 0,然后是 41 位的时间戳,接下来的 10 位是机房加机器,最后的 12 位是序列号。
上面这个结构是雪花算法的基本结构,不同公司根据自身的业务会进行相应的调整,有的可以采用 32 位或者其他位数,而且时间戳的位数也可以根据实际情况进行调整,10 位的 workerID 有机房的公司可以用机房加机器组成,没有机房的公司可以直接用机器来组成,序列位也可以根据情况适当调整。
我们可以简单算一下,41 位的时间位是2 ^ 41 / (365 * 24 * 3600 * 1000) = 69 年,每个机器每毫秒可以生成 2 ^ 12 = 4096 个 ID。
那是不是说我们这个代码只能运行 69 年呢?其实不是的,这里服务在启动的时候会设置一个初始值,这里的时间戳是用机器的时间减去初始值的差值。那 SnowFlake 算法有什么优缺点呢?
- 因为有时间戳,所以满足自增的要求,同时也具备一定的可读性;
- 化整为零每个服务在各自的机器上可以直接生成唯一 ID,只需要配置好机房和机器编号即可;
- 长度可以根据业务自行调整;
- 缺点是依赖机器的时钟,如果说机器的时钟有问题,会导致生成的 ID 可能会重复,这个需要控制;
结合上面的原理,我们可以通过 Java 代码来具体实现,代码如下:
public class SnowFlakeUtil {
//初始时间戳
private final static long START_TIMESTAMP = 1624796691000L;
//数据中心占用的位数
private final static long DATA_CENTER_BIT = 5;
//机器标识占用的位数
private final static long MACHINE_BIT = 5;
//序列号占用的位数
private final static long SEQUENCE_BIT = 12;
/**
* 每一部分的最大值
*/
private final static long MAX_SEQUENCE = ~(-1L << SEQUENCE_BIT);
private final static long MAX_MACHINE_NUM = ~(-1L << MACHINE_BIT);
private final static long MAX_DATA_CENTER_NUM = ~(-1L << DATA_CENTER_BIT);
/**
* 每一部分向左的位移
*/
private final static long MACHINE_LEFT = SEQUENCE_BIT;
private final static long DATA_CENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
private final static long TIMESTAMP_LEFT = DATA_CENTER_LEFT + DATA_CENTER_BIT;
private final long idc;
private final long serverId;
private long sequence = 0L;
private long lastTimeStamp = -1L;
private long getNextMill() {
long mill = System.currentTimeMillis();
while (mill <= lastTimeStamp) {
mill = System.currentTimeMillis();
}
return mill;
}
/**
* 根据指定的数据中心ID和机器标志ID生成指定的序列号
*
* @param idc 数据中心ID
* @param serverId 机器标志ID
*/
public SnowFlakeUtil(long idc, long serverId) {
if (idc > MAX_DATA_CENTER_NUM || idc < 0) {
throw new IllegalArgumentException("IDC 数据中心编号非法!");
}
if (serverId > MAX_MACHINE_NUM || serverId < 0) {
throw new IllegalArgumentException("serverId 机器编号非法!");
}
this.idc = idc;
this.serverId = serverId;
}
/**
* 生成下一个 ID
*
* @return
*/
public synchronized long genNextId() {
long currTimeStamp = System.currentTimeMillis();
if (currTimeStamp < lastTimeStamp) {
throw new RuntimeException("Clock moved backwards. Refusing to generate id");
}
if (currTimeStamp == lastTimeStamp) {
//相同毫秒内,序列号自增
sequence = (sequence + 1) & MAX_SEQUENCE;
//同一毫秒的序列数已经达到最大
if (sequence == 0L) {
currTimeStamp = getNextMill();
}
} else {
//不同毫秒内,序列号置为0
sequence = 0L;
}
lastTimeStamp = currTimeStamp;
return (currTimeStamp - START_TIMESTAMP) << TIMESTAMP_LEFT | idc << DATA_CENTER_LEFT | serverId << MACHINE_LEFT | sequence;
}
public static void main(String[] args) {
SnowFlakeUtil snowFlake = new SnowFlakeUtil(4, 3);
for (int i = 0; i < 100; i++) {
System.out.println(snowFlake.genNextId());
}
}
}
相关推荐
- SQL入门知识篇(sql入门新手教程视频)
-
一、什么是数据库?什么是SQL?1、数据库:存放数据,可以很多人一起使用2、关系数据库:多张表+各表之间的关系3、一张表需要包含列、列名、行4、主键:一列(或一组列),其值能够唯一区分表中的每个行。5...
- postgresql实现跨库查询-dblink的妙用
-
技术导语:用惯了oracle的dblink,转战postgresql,会一时摸不着头脑。本期就重点详细讲解postgresql如何安装dblink模块及如何使用dblink实现跨库查询。安装cont...
- Oracle VM VirtualBox虚拟机软件(oracle vm virtualbox win10)
-
OracleVMVirtualBox是一款完全免费的虚拟机软件,下载银行有提供下载,软件支持安装windows、linux等多个操作系统,让用户可以在一台设备上实现多个操作系统的操作。同时软件有着...
- 开源 SPL 轻松应对 T+0(开源srs)
-
T+0问题T+0查询是指实时数据查询,数据查询统计时将涉及到最新产生的数据。在数据量不大时,T+0很容易完成,直接基于生产数据库查询就可以了。但是,当数据量积累到一定程度时,在生产库中进行大数据...
- 中小企业佳选正睿ZI1TS4-4536服务器评测
-
随着科技的不断发展,各行各业对于数据使用越加频繁,同时针对服务器的选择方面也就越来越多样化和细分化。那么对于我们用户来说,如何选择符合自身业务需求和最优性价比的产品呢?笔者将通过刚刚购买的这台服务器的...
- MFC转QT:Qt基础知识(mfc和qt的区别)
-
1.Qt框架概述Qt的历史和版本Qt是一个跨平台的C++应用程序开发框架,由挪威公司Trolltech(现为QtCompany)于1991年创建。Qt的发展历程:1991年:Qt项目启动1995年...
- 数据库,QSqlTableModel(数据库有哪些)
-
QMYSQL——mysqlQSQLITE——sqliteQOICQ——orcale所需头文件.pro增加sql#include<QSqlDatabase>#include<Q...
- python通过oledb连接dbf数据库(python连接jdbc)
-
起因:因为工作需要,需要读取dbf文件和系统数据中数据进行校对,因为知道dbf文件可以用sql查询,所以想能不能像mysql/oracle那样连接,再调用执行sql方法,通过一系列百度,尝试,最终通过...
- Excel常用技能分享与探讨(5-宏与VBA简介 VBA与数据库)
-
在VBA(VisualBasicforApplications)中使用数据库(如Access、SQLServer、MySQL等)具有以下优点,适用于需要高效数据管理和复杂业务逻辑的场景:1....
- Excel常用技能分享与探讨(5-宏与VBA简介 VBA与数据库-二)
-
以下是常见数据库软件的详细配置步骤,涵盖安装、驱动配置、服务启动及基本设置,确保VBA能够顺利连接:一、MicrosoftAccess适用场景:小型本地数据库,无需独立服务。配置步骤:安装Acces...
- Windows Docker 安装(docker安装windows容器)
-
Docker并非是一个通用的容器工具,它依赖于已存在并运行的Linux内核环境。Docker实质上是在已经运行的Linux下制造了一个隔离的文件环境,因此它执行的效率几乎等同于所部署的L...
- Windows下安装Ubuntu虚拟机方法(windows下安装ubuntu20)
-
在Windows下安装Ubuntu虚拟机。选择使OracleVMVirtualBox安装Ubuntu虚拟机。1.下载和安装OracleVMVirtualBox:访问OracleVMVir...
- java入门教程1 - 安装和配置(win和linux)
-
windows安装和配置安装javahttps://www.oracle.com/java/technologies/javase/javase-jdk8-downloads.html目前大部分项目的...
- Centos7 安装Tomcat8服务及配置jdk1.8教程
-
1、下载jdk1.8压缩包下载地址:https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.htmltom...
- 全网最完整的免费java教程讲义(一)——java配置和安装
-
一,安装Java1)安装JDK要学习和使用java,首先需要安装JDK(JavaDevelopemntKit),相当于java安装包。Java的下载页在甲骨文官网上:https://www.or...
你 发表评论:
欢迎- 一周热门
- 最近发表
-
- SQL入门知识篇(sql入门新手教程视频)
- postgresql实现跨库查询-dblink的妙用
- Oracle VM VirtualBox虚拟机软件(oracle vm virtualbox win10)
- 开源 SPL 轻松应对 T+0(开源srs)
- 中小企业佳选正睿ZI1TS4-4536服务器评测
- MFC转QT:Qt基础知识(mfc和qt的区别)
- 数据库,QSqlTableModel(数据库有哪些)
- python通过oledb连接dbf数据库(python连接jdbc)
- Excel常用技能分享与探讨(5-宏与VBA简介 VBA与数据库)
- Excel常用技能分享与探讨(5-宏与VBA简介 VBA与数据库-二)
- 标签列表
-
- oracle位图索引 (63)
- oracle批量插入数据 (62)
- oracle事务隔离级别 (53)
- oracle 空为0 (50)
- oracle主从同步 (55)
- oracle 乐观锁 (51)
- redis 命令 (78)
- php redis (88)
- redis 存储 (66)
- redis 锁 (69)
- 启动 redis (66)
- redis 时间 (56)
- redis 删除 (67)
- redis内存 (57)
- redis并发 (52)
- redis 主从 (69)
- redis 订阅 (51)
- redis 登录 (54)
- redis 面试 (58)
- 阿里 redis (59)
- redis 搭建 (53)
- redis的缓存 (55)
- lua redis (58)
- redis 连接池 (61)
- redis 限流 (51)