一般场景的唯一ID我们都可以使用数据库的自增主键来实现,但是当数据量过大的时候,一张表无法承载那么大的数据量,我们就需要使用分库分表的技术方案,此时如果多张表需要不能重复的主键,就需要使用到我们今天所讲的分布式唯一ID生成方案了。简单来说,就是需要一个特定规则的发号器来给你的分布式系统生成唯一ID
UUID
JDK自带的UUID功能就可以生成一个唯一的ID,也没有并发的压力,但是这种方案生成的唯一ID非常长,而且无序,这样就会产生一个很严重的问题,导致数据库经常会进行页分裂。因为数据库的索引查询是基于有序的主键来的,无序的主键就会经常发生页分裂,降低数据库的性能,页分裂具体的细节可以查看我之前的博客(图解MySQL页分裂)
Redis自增
Redis本身有自增的方法incrby,可以指定要增加的步长,同时天然支持高并发。在使用的时候,我们可以进行Redis的集群部署,有多少台机器就可以指定多少的步长。比如3台机器组成的集群,步长就是3,第一台机器生成的主键ID就是1,4,7…,第二台机器生成的主键ID就是2,5,8…,第三台机器生成的主键ID就是3,6,9…,以此类推
这个方案看起来很美好,但是实际使用的时候也存在一些问题,比如说在机器伸缩的时候,这个步长需要动态进行调整,需要额外的开发量,同时Redis集群进行数据同步的时候,也可能存在一些极端的场景导致主键ID重复,比如leader生成了主键ID但是还未同步至slave就发生了宕机,此时slave就会产生重复的主键ID
时间戳 + 业务ID
举个例子,比如说在生成订单ID的时候,就可以使用时间戳 + 客户ID + 商品ID或者再加上一些其他的业务ID共同来组成我们需要的唯一ID,这种方式生成唯一ID简单方便,而且也不会存在其他的并发或者性能问题,只要能保证当前业务场景下生成的ID是唯一的话,那么这个方案应该是第一优先级被考虑的
数据库自增主键
既然不能用本身这张表的数据库的自增主键来生成的话,那么是不是可以单独从一个库里拿出一张表,就干生成主键ID的事情,这样做的话其实也很简单,不需要额外的开发成本。但是这个方案最大的缺点就是单库单表的并发量不会很高,无法支持高并发的场景,而且这张表一直生成唯一ID数据的话,最后历史数据会越来越多,需要定时清理(之所以将这种方式放这里讲,是为了和下面的方案进行一个对比)
flickr(雅虎旗下的图片分享平台)的数据库唯一id生成方案
这种方式可以认为是使用数据库自增主键的一个变种方式,首先这种方式需要一个单独的数据库表
CREATE TABLE `id_generator` (
`id` bigint(20) unsigned NOT NULL auto_increment,
`stub` char(1) NOT NULL default '',
PRIMARY KEY (`id`),
UNIQUE KEY `stub` (`stub`)
) ENGINE=MyISAM;
上面的建表语句非常常规,唯一需要注意的话,就是这里推荐使用MyISAM数据库引擎,而不是InnoDB,可以屏蔽掉一些数据库事务的相关影响
当我们有了这张表之后,要想获得我们需要的分布式唯一ID,就需要下面的SQL了
REPLACE INTO id_generator(stub) VALUES ('value');
SELECT LAST_INSERT_ID();
REPLACE INTO会生成一条新的数据,如果这条数据的value值存在的话,就会替代原先的数据,同时数据库的id还是会递增,这么做的好处就是不会再生成大量的insert数据,导致数据库表的数据太多,需要定时清理了,同时后面的value值我们可以进行自定义,比如说我们想生成订单的唯一ID,那么这个value我们可以指定为order,如果想生成库存的唯一ID就可以指定为stock等等,这样的话,这一张表就可以承载大量业务场景下的分布式唯一ID生成数据
查询分布式唯一ID的语句中的last_insert_id()函数是connection级别的,就是这个连接的最近insert生成的id,多个客户端之间没影响,不会产生并发问题
这种方式一般都会配合MySQL双机高可用方案,两个库设置不同的起始ID和相同的步长,类似Redis的那种方式,比如说库A起始ID是1,步长为2,那么生成的ID就是1,3,5…,库B起始ID是2,步长也为2,生成的ID就是2,4,6…
Integer n = 1; // 通过数据库查询的结果
volatile AtomicLong uniqueId = new AtomicLong(n * 10000);
volatile AtomicLong maxId = new AtomicLong((n + 1) * 10000);
volatile boolean needRefresh = false;
public Long idGenerator() {
uniqueId.incrementAndGet();
if (uniqueId >= maxId) {
// 如果needRefresh需要刷新的标记为true的话,就会再次去请求数据库查询新的号段
needRefresh = true;
Thread.sleep(10);
}
return uniqueId;
}
大致思路就是这样的,通过号段的方式来减少数据库的查询次数,从而提高并发量,在控制好号段边界的情况下不断取出号段中的值我们就能获得想要的唯一ID了
要解决这个问题的话,首先得记录下上一次生成ID的时间,然后针对时钟回拨的时间长短分几种不同的情况:
如果时间回拨的时间很短(一般场景下都是很短的,几百毫秒之内)且实在不想等待的话,还有一个办法就是在最近几秒内每一毫秒生成的最大ID都记录下来,当发生时钟回拨时,直接在当前毫秒内生成的最大ID基础上再进行递增,也能保证ID唯一