数据结构论坛

首页 » 分类 » 问答 » 全局唯一Id的6种生成策略和Snowfl
TUhjnbcbe - 2025/7/31 17:47:00
白癜风手术成果展 http://www.yushiels.com/npxbb/npxlf/1603.html

随着业务的不断发展,业务功能的不断拆分,系统不再是以前的单体应用。往往一个系统会被拆分成多个微服务的模块部署,且在不同的服务器甚至是不同的数据库。在加上高并发的存在。尤其是电商项目。各个项目中需要保证时钟同步且全局唯一。这就给我们提出了两个个问题,这种全局唯一ID它有什么要求?和怎么才能保证这个功能的实现?。

全局ID的要求

1.全局唯一:最基本的要求

2.趋势递增:在MySQL的innoDB引擎中使用的是聚集索引,由于使用Btree的数据结构来存储索引数据,在主键的选择上面我们应该尽量使用有序的主键保证写入性能。

3.单调递增:保证下一个ID大于上一个ID,例如事务版本号、IM增量信息、排序等特殊需求

4.信息安全:如果ID是连续的,恶意用户的扒取工作就非常容易做了,直接按照顺序下载指定URL即可所以在一些应用场景下,需要ID无规则不规则,让竞争对手不好猜

5.含时间戳:这样就能在开发中快速了解分布式id的生成时间

6.高可用,低延迟,高QPS(对QPS不了解的,可以简单的理解为每秒的生产id的个数)

以下给大家提供几个解决方案,根据自己公司业务的需求,选择合适你的方案。

具体实现

UUIDStrings=UUID.randomUUID().toString();

会生产32位的16进制的数字,它性能比较高,因为其直接就是jdk自带的功能本地就可以直接生产,不会产出任何的网络消耗和额外的资源浪费。其唯一性是没毛病的。但其如果存到数据库做主要的流程业务编码的话性能较差。主要有3个问题:

其无序。就没法预测他的生产,对于业务上没法看出它和其他数据的先后。主键问题。做主键的字段mysql官方是不提倡用UUID,因为其太长,会占用更多的空间,在查询中导致不可预期的效率影响。索引问题。会导致B+树索引的分裂。如果在其字段上创建索引会导致mysql索引树的断层和分裂,因为每次插入新的UUID时,UUID的无序性会导致索引进行很大的修改,没法排序。最终导致插入和查询的效率都会降低。但UUID还有可以用在一些序列号呀或者只是为了区分数据的业务逻辑上。2.数据库自增主键

主要原理是数据库的自增主键和mysql数据库的replaceinto实现(如果数据库存在该ID怎删除旧的,添加新的,如果没有旧的数据则直接添加)

操作:创建数据库表

CREATETABLE`t_num`(

`id`bigint(20)unsignedNOTNULLAUTO_INCREMENT,

`num`tinyint(1)NOTNULLDEFAULT0,

PRIMARYKEY(`id`),

UNIQUEKEY`num`(`num`)

)ENGINE=InnoDBDEFAULTCHARSET=utf8;

通过以下sql获取id

REPLACEINTOt_num(num)VALUES(0);

SELECTLAST_INSERT_ID();

通过以上你就可以通过mysql获取到唯一的ID。但是其也不太适用于高并发,集群环境复杂的业务中。因为当业务需要将数据库做水平的扩展时,就会出现多台数据库可能出现重复的问题(因为步长和机器台数的问题导致)。所有其适用业务较小,需求量不大的系统中。大中型项目不建议适用。

3.Redis实现

原理因为Redis是单线的天生保证原子性,可以使用原子操作INCR和INCRBY来实现(具体操作就是操作Redis的ApiRedis系列-通用命令)

注意:在Redis集群情况下,同样和MySQL一样需要设置不同的增长步长,同时key定要设置有效期

可以使用Redis集群来获取更高的吞吐量。

假如一个集群中有5台Redis.可以初始化每台Redis的值分别是1,2,3,4,5,然后步长都是5。

每个Redis生成的ID为:

A:1,6,11,16,21B:2,7,12,17,22

C:3,8,13,18,23D:4,9,14,19,24E:5,10,15,20,25

缺点:1、需要在原来系统没有Redis的情况下引入Redis。2、需要维护Redis集群环境。3、如果有个别机器宕了导致数字的不连续。

4.雪花算法Snowflake

SnowFlake生成ID能够按照时间有序生成,生成的id是一个64bit大小的整数,为一个Long型(转换成字符串后长度最多19)。分布式系统内不会产生ID碰撞(由datacenter和workerld作区分)并且效率较高。

撸代码环节

publicclassIdWorker{

/**开始时间截(-01-01),单位毫秒*/

privatestaticfinallongSTART_TIMESTAMP=0L;

/**机器ID所占的位数*/

privatestaticfinallongWORKER_ID_BITS=5L;

/**数据标识ID所占的位数*/

privatestaticfinallongDATACENTER_ID_BITS=5L;

/**支持的最大机器ID,结果是31:0B*/

privatestaticfinallongMAX_WORKER_ID=-1L^(-1LWORKER_ID_BITS);

/**支持的最大数据中心ID,结果是31:0B*/

privatestaticfinallongMAX_DATACENTER_ID=-1L^(-1LDATACENTER_ID_BITS);

/**序列在ID中占的位数*/

privatestaticfinallongSEQUENCE_BITS=12L;

/**机器ID向左移12位*/

privatestaticfinallongWORKER_ID_SHIFT=SEQUENCE_BITS;

/**数据中心ID向左移17位(12+5)*/

privatestaticfinallongDATACENTER_ID_SHIFT=SEQUENCE_BITS+WORKER_ID_BITS;

/**时间截向左移22位(5+5+12)*/

privatestaticfinallongTIMESTAMP_LEFT_SHIFT=SEQUENCE_BITS+WORKER_ID_BITS+DATACENTER_ID_BITS;

/**生成序列的掩码,这里为(0B11=0xFFF=)*/

privatestaticfinallongSEQUENCE_MASK=-1L^(-1LSEQUENCE_BITS);

/**工作机器ID(0~31)*/

privatelongworkerId;

/**数据中心ID(0~31)*/

privatelongdatacenterId;

/**毫秒内序列(0~)*/

privatelongsequence=0L;

/**上次生成ID的时间截*/

privatelonglastTimestamp=-1L;

/**

*创建ID生成器的方式一:使用工作机器的序号,范围是[0,],优点是方便给机器编号

*

*

paramworkerId工作机器ID

*/

publicIdWorker(longworkerId){

longmaxMachineId=(MAX_DATACENTER_ID+1)*(MAX_WORKER_ID+1)-1;//

if(workerId0

workerIdmaxMachineId){

thrownewIllegalArgumentException(String.format(WorkerIDcantbegreaterthan%dorlessthan0,maxMachineId));

}

this.datacenterId=(workerIdWORKER_ID_BITS)MAX_DATACENTER_ID;

this.workerId=workerIdMAX_WORKER_ID;

*创建ID生成器的方式二:使用工作机器ID和数据中心ID,优点是方便分数据中心管理

*

paramdatacenterId数据中心ID(0~31)

*

paramworkerId工作机器ID(0~31)

publicIdWorker(longdatacenterId,longworkerId){

if(workerIdMAX_WORKER_ID

workerId0){

thrownewIllegalArgumentException(String.format(WorkerIDcantbegreaterthan%dorlessthan0,MAX_WORKER_ID));

if(datacenterIdMAX_DATACENTER_ID

datacenterId0){

thrownewIllegalArgumentException(String.format(DatacenterIDcantbegreaterthan%dorlessthan0,MAX_DATACENTER_ID));

this.workerId=workerId;

this.datacenterId=datacenterId;

*获得下一个ID(该方法是线程安全的),同一机器同一时间可产生个ID,70年内不生成重复的ID

*

returnlong类型的ID

publicsynchronizedStringnextId(){

longtimestamp=timeGen();

//如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常

if(timestamplastTimestamp){

thrownewRuntimeException(String.format(Clockmovedbackwards.Refusingtogenerateidfor%dmilliseconds,lastTimestamp-timestamp));

//如果是同一时间生成的,则进行毫秒内序列

if(lastTimestamp==timestamp){

sequence=(sequence+1)SEQUENCE_MASK;

//毫秒内序列溢出

if(sequence==0){

//阻塞到下一个毫秒,获得新的时间戳

timestamp=tilNextMillis(lastTimestamp);

}else{

sequence=0L;//时间戳改变,毫秒内序列重置

//上次生成ID的时间截

lastTimestamp=timestamp;

//移位并通过或运算拼到一起组成64位的ID

returnString.valueOf((((timestamp-START_TIMESTAMP)TIMESTAMP_LEFT_SHIFT)

(datacenterIdDATACENTER_ID_SHIFT)

(workerIdWORKER_ID_SHIFT)

sequence));

*阻塞到下一个毫秒,直到获得新的时间戳

*

paramlastTimestamp上次生成ID的时间截

*

return当前时间戳(毫秒)

protectedlongtilNextMillis(longlastTimestamp){

while(timestamp=lastTimestamp){

timestamp=timeGen();

returntimestamp;

*返回当前时间,以毫秒为单位

*

return当前时间(毫秒)

protectedlongtimeGen(){

returnSystem.currentTimeMillis();

publicstaticStringgenerateOrderNum(){

try{

Stringip=InetAddress.getLocalHost().getHostAddress();

System.out.println(IP

1
查看完整版本: 全局唯一Id的6种生成策略和Snowfl