全局id的使用场景
在一个应用系统中,我们可能需要使用到一个不管任何时间,任何机器上都必须是唯一的一串数字标识,用来辨别唯一的一条数据。例如订单系统中的订单必须是唯一的不能重复,数据库的分库分表中存的数据也需要唯一标识来找到对应的数据,还有其他的一些要求全局唯一的场景都是非常重要的。还有一些场景不仅需要数据符合全局唯一,还需要顺序或趋势递增、信息安全等。
UUID生成器
UUID是指在一台机器上生成的数字,它保证对在同一时空中的所有机器都是唯一的。按照开放软件基金会(OSF)制定的标准计算,用到了以太网卡地址、纳秒级时间、芯片ID码和许多可能的数字
UUID由以下几部分的组合:
- 当前日期和时间,UUID的第一个部分与时间有关,如果你在生成一个UUID之后,过几秒又生成一个UUID,则第一个部分不同,其余相同。
- 时钟序列。
- 全局唯一的IEEE机器识别号,如果有网卡,从网卡MAC地址获得,没有网卡以其他方式获得。
UUID的唯一缺陷在于生成的结果串会比较长。关于UUID这个标准使用最普遍的是微软的GUID(Globals Unique Identifiers)。在ColdFusion中可以用CreateUUID()函数很简单地生成UUID,其格式为:xxxxxxxx-xxxx- xxxx-xxxxxxxxxxxxxxxx(8-4-4-16),其中每个 x 是 0-9 或 a-f 范围内的一个十六进制的数字。而标准的UUID格式为:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (8-4-4-4-12),可以从cflib 下载CreateGUID() UDF进行转换。示例:72cbb6a8-8354-4ca5-82a4-4c63fe9a1ddb
实际代码也非常简单:
//生成全局唯一ID
UUID uuid = UUID.randomUUID();
数据库自增
数据库是有自增约束和唯一约束的,所以我们可以利用数据库定步长的方式自增来实现唯一性。
//创建对应的表
create table global_id(
id bigint(20) unsigned not null auto_increment,
primary key (id),
) engine=Innodb;
//查看步长
mysql> SHOW VARIABLES LIKE 'auto_inc%';
+--------------------------+-------+
| Variable_name | Value |
+--------------------------+-------+
| auto_increment_increment | 1 |
| auto_increment_offset | 1 |
+--------------------------+-------+
rows in set (0.00 sec)
mysql> SET @@auto_increment_increment=10; //设置步长
Query OK, 0 rows affected (0.00 sec)
//生成全局唯一id
insert into golbal_id(id) values(0);
当然,随着业务数据量的增加,我们会用到数据库的分布式集群,分库分表的场景。这就要考虑不同数据库之间的冲突,所以我们可以给每一个数据库指定不同的初始值用相同的步长递增。例如,我们有三台数据库就可以这样划分:
(1)数据库1:初始值为1,步长为3;
(2)数据库2:初始值为2,步长为3;
(3)数据库3:初始值为3,步长为3;
这样就不会有冲突了,但是这样做会有两个的问题,一个就是当数据量增长或者下降时,数据库的数量会变动,所以对应的初始值和步长都会改变,之前的id就不能再使用了不能实现系统的扩展。另一个问题是,数据库的压力会变大,每一次生成和获取id会对应的读写一次数据。当然这种场景也可以使用预支的方式解决,就是我们在获取id的时候,通过批量生成获取一段数据的id放到内存中。
雪花算法snowFlake
分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求。
snowflake生成id的结构图:
如图,我们看到第一位是不用的,其实是符号位0表示正数,而41位的时间戳是已毫秒为单位计时的,10位的工作机器id分为5位机器标识和5位数据标识,还有12位的序列号是为了在同一毫秒时间、同一机器上的id生成不冲突而产生唯一的序列号(0-4095),最多4096的id,再多就会阻塞直到这一毫秒过去,当然1毫秒4096的id的量是很多了。
Twitter是用Scala语言写的,具体的Java代码:
package test;
/**
* Twitter_Snowflake<br>
* SnowFlake的结构如下(每部分用-分开):<br>
* 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 <br>
* 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0<br>
* 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截)
* 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。
* 41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69<br>
* 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId<br>
* 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号<br>
* 加起来刚好64位,为一个Long型。<br>
* SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,
* 经测试,SnowFlake每秒能够产生26万ID左右。
*/
public class SnowFlake
{
// ==============================Fields===========================================
/** 开始时间截 (2015-01-01) */
private final long twepoch = 1420041600000L;
/** 机器id所占的位数 */
private final long workerIdBits = 5L;
/** 数据标识id所占的位数 */
private final long datacenterIdBits = 5L;
/** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
/** 支持的最大数据标识id,结果是31 */
private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
/** 序列在id中占的位数 */
private final long sequenceBits = 12L;
/** 机器ID向左移12位 */
private final long workerIdShift = sequenceBits;
/** 数据标识id向左移17位(12+5) */
private final long datacenterIdShift = sequenceBits + workerIdBits;
/** 时间截向左移22位(5+5+12) */
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
/** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
/** 工作机器ID(0~31) */
private long workerId;
/** 数据中心ID(0~31) */
private long datacenterId;
/** 毫秒内序列(0~4095) */
private long sequence = 0L;
/** 上次生成ID的时间截 */
private long lastTimestamp = -1L;
//==============================Constructors=====================================
/**
* 构造函数
* @param workerId 工作ID (0~31)
* @param datacenterId 数据中心ID (0~31)
*/
public SnowFlake(long workerId, long datacenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
// ==============================Methods==========================================
/**
* 获得下一个ID (该方法是线程安全的)
* @return SnowflakeId
*/
public synchronized long nextId() {
long timestamp = timeGen();
//如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
if (timestamp < lastTimestamp) {
throw new RuntimeException(
String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
//如果是同一时间生成的,则进行毫秒内序列
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
//毫秒内序列溢出
if (sequence == 0) {
//阻塞到下一个毫秒,获得新的时间戳
timestamp = tilNextMillis(lastTimestamp);
}
}
//时间戳改变,毫秒内序列重置
else {
sequence = 0L;
}
//上次生成ID的时间截
lastTimestamp = timestamp;
//移位并通过或运算拼到一起组成64位的ID
return ((timestamp - twepoch) << timestampLeftShift) //
| (datacenterId << datacenterIdShift) //
| (workerId << workerIdShift) //
| sequence;
}
/**
* 阻塞到下一个毫秒,直到获得新的时间戳
* @param lastTimestamp 上次生成ID的时间截
* @return 当前时间戳
*/
protected long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
/**
* 返回以毫秒为单位的当前时间
* @return 当前时间(毫秒)
*/
protected long timeGen() {
return System.currentTimeMillis();
}
//==============================Test=============================================
/** 测试 */
public static void main(String[] args) {
SnowFlake idWorker = new SnowFlake(0, 0);
for (int i = 0; i < 1000; i++) {
long id = idWorker.nextId();
System.out.println(Long.toBinaryString(id));
System.out.println(id);
}
}
}
利用redis生成唯一id
- redis有提供一种原子操作incr方法就可以顺序递增,考虑到会用到redis集群,多台redis的递增和步长的设置其实和数据库的设置是一样的,但是却比数据库快多了。而且因为redis是单线程的,不会出现线程安全问题。
- 当然我们在实际生产环境中不会把id从1开始这样id的长度会不一样。我们可以考虑以6位、8位的数据来递增,如从100001开始到999999的数据,可以取到899998个id。你可能嫌id的量少,没事,我们可以在获取的id前加上一些前缀,例如当前时间戳、当前机器的mac地址等(有没有像snowflake)这样就不会出现重复的id。
这四种方式的对比
方式 | 优点 | 缺点 |
---|---|---|
UUID | 性能高、无网络带宽 | 32个字符太长了、mac地址泄露问题 |
数据库 | 简单、自增有序 | 扩展性问题、读写性能问题 |
snowflake | 高性能、趋势递增 | 服务器时钟问题 |
redis | 高性能、有序 | 需要编码配置搭建环境 |
总的来说,UUID的性能是最好,其次是snowflake,redis,其实这两个差不多。最后就是数据库。推荐数据量大、变化多的情况下使用snowflake和redis。