随着业务的不断发展,业务功能的不断拆分,系统不再是以前的单体应用。往往一个系统会被拆分成多个微服务的模块部署,且在不同的服务器甚至是不同的数据库。在加上高并发的存在。尤其是电商项目。各个项目中需要保证时钟同步且全局唯一。这就给我们提出了两个个问题,这种全局唯一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类型的IDpublicsynchronizedStringnextId(){
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