全局唯一ID设计
2016-04-07 15:49
302 查看
在分布式系统中,经常需要使用
全局唯一ID在数据库中一般会被设成主键,这样为了保证数据插入时索引的快速建立,还需要保持一个有序的趋势。
这样全局唯一ID就需要保证这两个需求:
全局唯一
趋势有序
优势:
简单,无需程序任何附加操作
保持定长的增量
在单表中能保持唯一性
劣势:
高并发下性能不佳,主键产生的性能上限是数据库服务器单机的上限。
水平扩展困难,在分布式数据库环境下,无法保证唯一性。
优势:
本地生成ID,不需要进行远程调用。
全局唯一不重复。
水平扩展能力非常好。
劣势:
ID有128 bits,占用的空间较大,需要存成字符串类型,索引效率极低。
生成的ID中没有带Timestamp,无法保证趋势递增
根据
比如我们设计的ID包含以下信息:
产生唯一ID的
使用自定义的这种方法需要注意的几点:
为了保持增长的趋势,要避免有些服务器的时间早,有些服务器的时间晚,需要控制好所有服务器的时间,而且要避免
在跨毫秒时,序列号总是归0,会使得序列号为0的ID比较多,导致生成的ID取模后不均匀,所以序列号不是每次都归0,而是归一个0到9的随机数。
使用这个
全局唯一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类,最好在一个系统中能保持单例模式运行。
相关文章推荐
- 魅族电商运维之路
- gitlab 的使用策略和简单介绍
- Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)
- wamp配置小细节
- hdu1029(基础)
- MySQL命令行下执行.sql脚本
- 批量数据采集过程中方差的计算
- C# Hadoop
- Teradata中SQL语言基础
- [LeetCode]22. Generate Parentheses
- linux下配置JDK JAVA环境
- Java序列化(Serializable)与反序列化
- Android SoundPool播放实例和方法详解
- Android 开发之 定时器 Handler + Runnable方式
- iOS开发基础学习笔记——导航控制器2->导航栏(UINavigationBar)/工具栏(toolBar)/导航元素按钮(UINavigationItem)
- sqlserver查找包含关键词的视图
- Dubbo与Zookeeper、SpringMVC整合和使用(负载均衡、容错)
- 为 HTML 添加新元素(html5新功能)
- PHP语言开发Paypal支付demo的具体实现
- shell_sctipts: 删除mysql备份到最后7日