在分布式系统中,经常需要使用全局唯一ID查找对应的数据。产生这种ID需要保证系统全局唯一,而且要高性能以及占用相对较少的空间。

全局唯一ID在数据库中一般会被设成主键,这样为了保证数据插入时索引的快速建立,还需要保持一个有序的趋势。

这样全局唯一ID就需要保证这两个需求:

  • 全局唯一
  • 趋势有序

全局ID产生的几种方式

数据库自增

当服务使用的数据库只有单库单表时,可以利用数据库的auto_increment来生成全局唯一递增ID.

优势:

  • 简单,无需程序任何附加操作
  • 保持定长的增量
  • 在单表中能保持唯一性

劣势:

  • 高并发下性能不佳,主键产生的性能上限是数据库服务器单机的上限。
  • 水平扩展困难,在分布式数据库环境下,无法保证唯一性。

UUID

一般的语言中会自带UUID的实现,比如Java中UUID方式UUID.randomUUID().toString(),可以通过服务程序本地产生,ID的生成不依赖数据库的实现。

优势:

  • 本地生成ID,不需要进行远程调用。
  • 全局唯一不重复。
  • 水平扩展能力非常好。

劣势:

  • ID有128 bits,占用的空间较大,需要存成字符串类型,索引效率极低。
  • 生成的ID中没有带Timestamp,无法保证趋势递增

Twitter Snowflake

snowflake是twitter开源的分布式ID生成算法,其核心思想是:产生一个long型的ID,使用其中41bit作为毫秒数,10bit作为机器编号,12bit作为毫秒内序列号。这个算法单机每秒内理论上最多可以生成1000*(2^12)个,也就是大约400W的ID,完全能满足业务的需求。

根据snowflake算法的思想,我们可以根据自己的业务场景,产生自己的全局唯一ID。因为Java中long类型的长度是64bits,所以我们设计的ID需要控制在64bits。

比如我们设计的ID包含以下信息:

| 41 bits: Timestamp | 3 bits: 区域 | 10 bits: 机器编号 | 10 bits: 序列号 |

产生唯一ID的Java代码:

import java.security.SecureRandom;

/**
* 自定义 ID 生成器
* ID 生成规则: ID长达 64 bits
*
* | 41 bits: Timestamp (毫秒) | 3 bits: 区域(机房) | 10 bits: 机器编号 | 10 bits: 序列号 |
*/
public class CustomUUID {
// 基准时间
private long twepoch = 1288834974657L; //Thu, 04 Nov 2010 01:42:54 GMT
// 区域标志位数
private final static long regionIdBits = 3L;
// 机器标识位数
private final static long workerIdBits = 10L;
// 序列号识位数
private final static long sequenceBits = 10L; // 区域标志ID最大值
private final static long maxRegionId = -1L ^ (-1L << regionIdBits);
// 机器ID最大值
private final static long maxWorkerId = -1L ^ (-1L << workerIdBits);
// 序列号ID最大值
private final static long sequenceMask = -1L ^ (-1L << sequenceBits); // 机器ID偏左移10位
private final static long workerIdShift = sequenceBits;
// 业务ID偏左移20位
private final static long regionIdShift = sequenceBits + workerIdBits;
// 时间毫秒左移23位
private final static long timestampLeftShift = sequenceBits + workerIdBits + regionIdBits; private static long lastTimestamp = -1L; private long sequence = 0L;
private final long workerId;
private final long regionId; public CustomUUID(long workerId, long regionId) { // 如果超出范围就抛出异常
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException("worker Id can't be greater than %d or less than 0");
}
if (regionId > maxRegionId || regionId < 0) {
throw new IllegalArgumentException("datacenter Id can't be greater than %d or less than 0");
} this.workerId = workerId;
this.regionId = regionId;
} public CustomUUID(long workerId) {
// 如果超出范围就抛出异常
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException("worker Id can't be greater than %d or less than 0");
}
this.workerId = workerId;
this.regionId = 0;
} public long generate() {
return this.nextId(false, 0);
} /**
* 实际产生代码的
*
* @param isPadding
* @param busId
* @return
*/
private synchronized long nextId(boolean isPadding, long busId) { long timestamp = timeGen();
long paddingnum = regionId; if (isPadding) {
paddingnum = busId;
} if (timestamp < lastTimestamp) {
try {
throw new Exception("Clock moved backwards. Refusing to generate id for " + (lastTimestamp - timestamp) + " milliseconds");
} catch (Exception e) {
e.printStackTrace();
}
} //如果上次生成时间和当前时间相同,在同一毫秒内
if (lastTimestamp == timestamp) {
//sequence自增,因为sequence只有10bit,所以和sequenceMask相与一下,去掉高位
sequence = (sequence + 1) & sequenceMask;
//判断是否溢出,也就是每毫秒内超过1024,当为1024时,与sequenceMask相与,sequence就等于0
if (sequence == 0) {
//自旋等待到下一毫秒
timestamp = tailNextMillis(lastTimestamp);
}
} else {
// 如果和上次生成时间不同,重置sequence,就是下一毫秒开始,sequence计数重新从0开始累加,
// 为了保证尾数随机性更大一些,最后一位设置一个随机数
sequence = new SecureRandom().nextInt(10);
} lastTimestamp = timestamp; return ((timestamp - twepoch) << timestampLeftShift) | (paddingnum << regionIdShift) | (workerId << workerIdShift) | sequence;
} // 防止产生的时间比之前的时间还要小(由于NTP回拨等问题),保持增量的趋势.
private long tailNextMillis(final long lastTimestamp) {
long timestamp = this.timeGen();
while (timestamp <= lastTimestamp) {
timestamp = this.timeGen();
}
return timestamp;
} // 获取当前的时间戳
protected long timeGen() {
return System.currentTimeMillis();
}
}

使用自定义的这种方法需要注意的几点:

  • 为了保持增长的趋势,要避免有些服务器的时间早,有些服务器的时间晚,需要控制好所有服务器的时间,而且要避免NTP时间服务器回拨服务器的时间。
  • 在跨毫秒时,序列号总是归0,会使得序列号为0的ID比较多,导致生成的ID取模后不均匀,所以序列号不是每次都归0,而是归一个0到9的随机数。
  • 使用这个CustomUUID类,最好在一个系统中能保持单例模式运行。

全局唯一ID设计的更多相关文章

  1. 如何在高并发分布式系统中生成全局唯一Id

    月整理出来,有兴趣的园友可以关注下我的博客. 分享原由,最近公司用到,并且在找最合适的方案,希望大家多参与讨论和提出新方案.我和我的小伙伴们也讨论了这个主题,我受益匪浅啊…… 博文示例: 1.     ...

  2. 高并发分布式系统中生成全局唯一Id汇总

    数据在分片时,典型的是分库分表,就有一个全局ID生成的问题.单纯的生成全局ID并不是什么难题,但是生成的ID通常要满足分片的一些要求:   1 不能有单点故障.   2 以时间为序,或者ID里包含时间 ...

  3. 如何在高并发分布式系统中生成全局唯一Id(转)

    http://www.cnblogs.com/heyuquan/p/global-guid-identity-maxId.html 又一个多月没冒泡了,其实最近学了些东西,但是没有安排时间整理成博文, ...

  4. 游戏服务器生成全局唯一ID的几种方法

    在服务器系统开发时,为了适应数据大并发的请求,我们往往需要对数据进行异步存储,特别是在做分布式系统时,这个时候就不能等待插入数据库返回了取自动id了,而是需要在插入数据库之前生成一个全局的唯一id,使 ...

  5. (转)如何在高并发分布式系统中生成全局唯一Id

    又一个多月没冒泡了,其实最近学了些东西,但是没有安排时间整理成博文,后续再奉上.最近还写了一个发邮件的组件以及性能测试请看 <NET开发邮件发送功能的全面教程(含邮件组件源码)> ,还弄了 ...

  6. 全局唯一ID发号器的几个思路

    标识(ID / Identifier)是无处不在的,生成标识的主体是人,那么它就是一个命名过程,如果是计算机,那么它就是一个生成过程.如何保证分布式系统下,并行生成标识的唯一与标识的命名空间有着密不可 ...

  7. 高并发分布式环境中获取全局唯一ID[分布式数据库全局唯一主键生成]

    需求说明 在过去单机系统中,生成唯一ID比较简单,可以使用MySQL的自增主键或者Oracle中的sequence, 在现在的大型高并发分布式系统中,以上策略就会有问题了,因为不同的数据库会部署到不同 ...

  8. 框架篇:分布式全局唯一ID

    前言 每一次HTTP请求,数据库的事务的执行,我们追踪代码执行的过程中,需要一个唯一值和这些业务操作相关联,对于单机的系统,可以用数据库的自增ID或者时间戳加一个在本机递增值,即可实现唯一值.但在分布 ...

  9. 关于全局唯一ID生成方法

    引:最近业务开发过程中需要涉及到全局唯一ID生成.之前零零总总的收集过一些相关资料,特此整理以便后用 本博客已经迁移至:http://cenalulu.github.io/ 本篇博文已经迁移,阅读全文 ...

随机推荐

  1. 我是如何在SQLServer中处理每天四亿三千万记录的

    首先声明,我只是个程序员,不是专业的DBA,以下这篇文章是从一个问题的解决过程去写的,而不是一开始就给大家一个正确的结果,如果文中有不对的地方,请各位数据库大牛给予指正,以便我能够更好的处理此次业务. ...

  2. 如何在高并发环境下设计出无锁的数据库操作(Java版本)

    一个在线2k的游戏,每秒钟并发都吓死人.传统的hibernate直接插库基本上是不可行的.我就一步步推导出一个无锁的数据库操作. 1. 并发中如何无锁. 一个很简单的思路,把并发转化成为单线程.Jav ...

  3. gradle学习笔记(1)

    1. 安装     (1) 下载最新gradle压缩包,解压到某处.地址是:Gradle web site:     (2) 添加环境变量:             1) 变量名:GRADLE_HOM ...

  4. Vue-Router 页面正在加载特效

    Vue-Router 页面正在加载特效 如果你在使用 Vue.js 和 Vue-Router 开发单页面应用.因为每个页面都是一个 Vue 组件,你需要从服务器端请求数据,然后再让 Vue 引擎来渲染 ...

  5. scrapy 知乎用户信息爬虫

    zhihu_spider 此项目的功能是爬取知乎用户信息以及人际拓扑关系,爬虫框架使用scrapy,数据存储使用mongo,下载这些数据感觉也没什么用,就当为大家学习scrapy提供一个例子吧.代码地 ...

  6. Xamarin+Prism开发详解三:Visual studio 2017 RC初体验

    Visual studio 2017 RC出来一段时间了,最近有时间就想安装试试,随带分享一下安装使用体验. 1,卸载visual studio 2015 虽然可以同时安装visual studio ...

  7. 如何利用tcpdump对mysql进行抓包操作

    命令如下: tcpdump -s -l -w - dst -i eno16777736 |strings 其中-i指定监听的网络接口,在RHEL 7下,网络接口名不再是之前的eth0,而是 eno16 ...

  8. 阿里云学生优惠Windows Server 2012 R2安装IIS,ftp等组件,绑定服务器域名,域名解析到服务器,域名备案,以及安装期间错误的解决方案

     前言: 这几天终于还是按耐不住买了一个月阿里云的学生优惠.只要是学生,在学信网上注册过,并且支付宝实名认证,就可以用9块9的价格买阿里云的云服务ECS.确实是相当的优惠. 我买的是Windows S ...

  9. JAVA程序员常用软件整理下载

    ********为了大家学习方便,特意整理软件下载如下:*************Java类软件:-------------------------------JDK7.0:http://pan.ba ...

  10. 用MongoDB分析合肥餐饮业

    看了<从数据角度解析福州美食>后难免心痒,动了要分析合肥餐饮业的念头,因此特地写了Node.js爬虫爬取了合肥的大众点评数据.分析数据库我并没有采用MySQL而是用的MongoDB,是因为 ...