需求场景
在复杂分布式系统中,往往需要对大量的数据和消息进行唯一标识。如在美团点评的金融、支付、餐饮、酒店、猫眼电影等产品的系统中,数据日渐增长,对数据分库分表后需要有一个唯一ID来标识一条数据或消息,数据库的自增ID显然不能满足需求;特别一点的如订单、骑手、优惠券也都需要有唯一ID做标识。此时一个能够生成全局唯一ID的系统是非常必要的。总之需要全局id,多出现在分布式多节点部署项目,数据库需要分库分表。
方案对比
1.UUID
String uuid = UUID.randomUUID().toString()
结果示例:
046b6c7f-0b8a-43b9-b35d-6489e6daee91
最简单、最容易想到的就应该是使用UUID了,根据UUID的特性,可以产生一个唯一的字符串,这一点大家都知道。UUID是在本地生成的,所以相对性能较高、时延低、扩展性高,完全不受分库分表的影响!
但是使用UUID是有点小问题的,主要体现在:
1.UUID无法保证趋势递增;
2.UUID过长,往往用32位字符串表示,占用数据库空间较大,做主键的时候索引中主键ID占据的空间较大;
3.UUID作为主键建立索引查询效率低,常见优化方案为转化为两个uint64整数存储;
4.由于使用实现版本的不一样,在高并发情况下可能会出现UUID重复的情况;
为什么无序的UUID会导致入库性能变差呢?
这就涉及到 B+树索引的分裂:
众所周知,关系型数据库的索引大都是B+树的结构,拿ID字段来举例,索引树的每一个节点都存储着若干个ID。
如果我们的ID按递增的顺序来插入,比如陆续插入8,9,10,新的ID都只会插入到最后一个节点当中。当最后一个节点满了,会裂变出新的节点。这样的插入是性能比较高的插入,因为这样节点的分裂次数最少,而且充分利用了每一个节点的空间。
但是,如果我们的插入完全无序,不但会导致一些中间节点产生分裂,也会白白创造出很多不饱和的节点,这样大大降低了数据库插入的性能。
2.数据库自增主键
这种方式是使用数据库提供的自增数值型字段作为自增主键,它的优点是:
数据库自动编号,速度快,而且是增量增长,按顺序存放,对于检索非常有利;
数字型,占用空间小,易排序,在程序中传递也方便;
能够保证独立性,程序可以在不同的数据库间迁移,效果不受影响。
保证生成的ID不仅是表独立的,而且是库独立的,这点在你想切分数据库的时候尤为重要。
缺点 :
因为自动增长,在手动要插入指定ID的记录时会显得麻烦,尤其是当系统与其它系统集成时,需要数据导入时,很难保证原系统的ID不发生主键冲突(前提是老系统也是数字型的)。特别是在新系统上线时,新旧系统并行存在,并且是异库异构的数据库的情况下,需要双向同步时,自增主键将是你的噩梦;
在系统集成或割接时,如果新旧系统主键不同是数字型就会导致修改主键数据类型,这也会导致其它有外键关联的表的修改,后果同样很严重;
若系统也是数字型的,在导入时,为了区分新老数据,可能想在老数据主键前统一加一个字符标识(例如“o”,old)来表示这是老数据,那么自动增长的数字型又面临一个挑战。
如果经常有合并表的操作,就可能会出现主键重复的情况
很难处理分布式存储的数据表。数据量特别大时,会导致查询数据库操作变慢。此时需要进行数据库的水平拆分,划分到不同的数据库中,那么当添加数据时,每个表都会自增长,导致主键冲突。
总之,单体应用好处多多,分库分表之后容易造成主键冲突
3.全局Id解决方案,idworker
dworker是一个ID生成工具,可以生成一个全局唯一的长整形ID。也支持分布式环境下的使用。idworker采用了Snowflake算法,并在此基础上增加了奇偶抖动功能,避免在低并发的环境下生成全是偶数的情况。
说说Snowflake算法
snowflake算法所生成的ID结构是什么样子呢?我们来看看下图:
SnowFlake所生成的ID一共分成四部分:
1.第一位
占用1bit,其值始终是0,没有实际作用。
2.时间戳
占用41bit,精确到毫秒,总共可以容纳约140年的时间。
3.工作机器id
占用10bit,其中高位5bit是数据中心ID(datacenterId),低位5bit是工作节点ID(workerId),做多可以容纳1024个节点。
4.序列号
占用12bit,这个值在同一毫秒同一节点上从0开始不断累加,最多可以累加到4095。
SnowFlake算法在同一毫秒内最多可以生成多少个全局唯一ID呢?只需要做一个简单的乘法:
同一毫秒的ID数量 = 1024 X 4096 = 4194304
这个数字在绝大多数并发场景下都是够用的。
SnowFlake的代码实现
//初始时间截
private static final long INITIAL_TIME_STAMP = 1483200000000L;
//机器id所占的位数
private static final long WORKER_ID_BITS = 5L;
//数据标识id所占的位数
private static final long DATACENTER_ID_BITS = 5L;
//支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)
private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);
//支持的最大数据标识id,结果是31
private static final long MAX_DATACENTER_ID = ~(-1L << DATACENTER_ID_BITS);
//序列在id中占的位数
private final long SEQUENCE_BITS = 12L;
//机器ID的偏移量(12)
private final long WORKERID_OFFSET = SEQUENCE_BITS;
//数据中心ID的偏移量(12+5)
private final long DATACENTERID_OFFSET = SEQUENCE_BITS + SEQUENCE_BITS;
//时间截的偏移量(5+5+12)
private final long TIMESTAMP_OFFSET = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS;
//生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095)
private final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);
//工作节点ID(0~31)
private long workerId;
//数据中心ID(0~31)
private long datacenterId;
//毫秒内序列(0~4095)
private long sequence = 0L;
//上次生成ID的时间截
private long lastTimestamp = -1L;
/**
* 构造函数
*
* @param workerId 工作ID (0~31)
* @param datacenterId 数据中心ID (0~31)
*/
public SnowFlakeIdGenerator(long workerId, long datacenterId) {
if (workerId > MAX_WORKER_ID || workerId < 0) {
throw new IllegalArgumentException(String.format("WorkerID 不能大于 %d 或小于 0", MAX_WORKER_ID));
}
if (datacenterId > MAX_DATACENTER_ID || datacenterId < 0) {
throw new IllegalArgumentException(String.format("DataCenterID 不能大于 %d 或小于 0", MAX_DATACENTER_ID));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
/**
* 获得下一个ID (用同步锁保证线程安全)
*
* @return SnowflakeId
*/
public synchronized long nextId() {
long timestamp = System.currentTimeMillis();
//如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
if (timestamp < lastTimestamp) {
throw new RuntimeException("当前时间小于上一次记录的时间戳!");
}
//如果是同一时间生成的,则进行毫秒内序列
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & SEQUENCE_MASK;
//sequence等于0说明毫秒内序列已经增长到最大值
if (sequence == 0) {
//阻塞到下一个毫秒,获得新的时间戳
timestamp = tilNextMillis(lastTimestamp);
}
}
//时间戳改变,毫秒内序列重置
else {
sequence = 0L;
}
//上次生成ID的时间截
lastTimestamp = timestamp;
//移位并通过或运算拼到一起组成64位的ID
return ((timestamp - INITIAL_TIME_STAMP) << TIMESTAMP_OFFSET)
| (datacenterId << DATACENTERID_OFFSET)
| (workerId << WORKERID_OFFSET)
| sequence;
}
/**
* 阻塞到下一个毫秒,直到获得新的时间戳
*
* @param lastTimestamp 上次生成ID的时间截
* @return 当前时间戳
*/
protected long tilNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
public static void main(String[] args) {
final SnowFlakeIdGenerator idGenerator = new SnowFlakeIdGenerator(1, 1);
//线程池并行执行10000次ID生成
ExecutorService executorService = Executors.newCachedThreadPool();;
for (int i = 0; i < 10000; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
long id = idGenerator.nextId();
System.out.println(id);
}
});
}
executorService.shutdown();
}
这段代码改写自网上的SnowFlake算法实现,有几点需要解释一下:
1.获得单一机器的下一个序列号,使用Synchronized控制并发,而非CAS的方式,是因为CAS不适合并发量非常高的场景。
2.如果当前毫秒在一台机器的序列号已经增长到最大值4095,则使用while循环等待直到下一毫秒。
3.如果当前时间小于记录的上一个毫秒值,则说明这台机器的时间回拨了,抛出异常。但如果这台机器的系统时间在启动之前回拨过,那么有可能出现ID重复的危险。
SnowFlake的优势和劣势
SnowFlake算法的优点:
1.生成ID时不依赖于DB,完全在内存生成,高性能高可用。
2.ID呈趋势递增,后续插入索引树的时候性能较好。
SnowFlake算法的缺点:
依赖于系统时钟的一致性。如果某台机器的系统时钟回拨,有可能造成ID冲突,或者ID乱序。