目录
一、背景:
当需要给数据添加唯一标识符,就需要分布式唯一ID生成器。
如果MySQL是单库单表,直接使用数据库的自增主键就可以了。
如果是分库分表,肯定无法使用自增主键来完成。
二、常见的分布式唯一ID生成方案:
1、数据库自增主键:
通过一个表来生成全局唯一ID,插入一条数据,返回一个全局唯一的ID,保证全局唯一。
优点:
- 实现简单,很容易落地,专门搞个对应的库和表就行了。
缺点:
- 单库单表,抗不住太高的并发,如果并发达到几千,机器可能就有挂的风险。
- 单库有高可用的问题。
- 随着不断插入表数据会越来越多,需要定期清理。
适用场景:
很少直接在生产环境直接使用这个方案,通过flickr实现这个更好一点。
2、UUID:
Java自带的UUID api就可以生成一个唯一id。
UUID.randomUUID().toString()
UUID.randomUUID().toString().replaceAll("-", "")
76dfa90b-8e45-4ec7-838a-3aa24de79482
3fd88264b6664d9ca993236bcc7ea1a6
优点:
- 本地生成,没有并发压力。
缺点:
- 字段太长了;
- 作为主键不太靠谱,因为不是有序的,会出现数据库频繁页分裂问题!
适用场景:
- 除数据库主键之外的其他唯一键场景,如很多业务编码,都是适用的。
3、Twitter开源的SnowFlake方案:
核心思想:
- 64个bit位,最高位1个bit是0,41位放时间戳(单位毫秒,最多使用69年);
- 10位放机器标识(最多可以部署在1024台机器上);
- 12位放序号(每毫秒,每台机器,可以顺序生成4096个ID);
- 通过时间戳 + 机器id + 序号 -> long类型的唯一id。
SnowFlake程序分布式部署在多台机器上,每台机器每毫秒最多4096个ID,基于内存生成,性能高的一批,不用担心并发问题。
优点:
- 高并发,高可用,集群可伸缩,最多扩展1024台机器。
缺点:
- 目前的开源算法还需要考虑时钟回拨等问题,如果想要解决,还要重新开发。
适用场景:
- 中大型公司,对于高并发生成唯一ID场景,基于snowflake算法自研。
- 加入时钟回拨、多机房等解决方案。
4、Redis自增机制:
核心思想:
- Redis能够实现有序自增incrby;
- 例如5台机器集群部署,那么每台机器的初始值依次为1、2、3、4、5,每台机器的自增步长是5。
- 第1台机器就是1、6、11、16、21,
- 第2台机器就是2、7、12、17、22,
- 以此类推。。。
- 直到第5台机器就是5、10、15、20、25。
优点:
- 公司几乎都有Redis集群,可以直接用,或者申请独立的集群。
- 高并发,高可用,集群化,全局唯一。
缺点:
- 客户端需要自己开发封装。
- Redis机器数量是否要支持配置,因为万一需要加机器呢,支持动态感知吗?
- 扩容之后,步长就变了,之前的数据是否需要处理。
适用场景:
- 一般不用redis集群玩自增主键生成。
- 对未来的并发是可预期的。
- Redis主从同步是异步的,如果故障转移,是不是有可能出现重复ID。
5、时间戳 + 业务id:
1、业务背景:
例如打车业务,需要生成订单ID。
2、实现:
- 打车:时间戳 + 起点编号 + 车牌号,肯定是能保证唯一的。
- 电商:可以用时间戳 + 用户ID + 渠道 + 其他业务id,也是可以保证唯一的。
3、优点:
- 实现简单,没额外成本,没并发之类的扩容问题。
4、缺点:
- 不是所有的业务场景都能这样用,例如现在用户模块需要做分库分表。
5、适用场景:
- 如果业务上能使用这个方案,建议使用。
6、flickr(雅虎旗下的图片分享平台)的数据库唯一id生成方案:
1、创建数据库表:
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;
1、优化:
每一台机器要申请一个唯一id,用自己机器的ip地址去replace into,那么自己的机器id不停自增,通过下面语句查询:
REPLACE INTO uid_sequence (stub) VALUES ('a')
SELECT LAST_INSERT_ID();
如果是不同的业务:不同的业务都会有自己的一条数据:
1 order
5 account
2、优点:
- 用replace into替代了insert into,避免表数据量过大。
3、缺点:
- 用这个方案生成唯一id,低并发场景下可以用于生产。
4、建议:
- 而且一般会部署数据库高可用方案,MySQL双机高可用方案,两个库设置不同的起始位置和步长,分别是1、3、5,以及2、4、6。
TicketServer1:
auto-increment-increment = 2
auto-increment-offset = 1
TicketServer2:
auto-increment-increment = 2
auto-increment-offset = 2
7、基于flickr方案的高并发优化:
1、背景:
flickr方案的核心问题在于并发瓶颈,所以可以把ID优化为号段。
2、封装客户端:
- 每台机器都引入封装的客户端,只要一旦服务启动,客户端就直接有一个线程采用flickr方案获取一个id。
- 当服务启动,通过flickr方案的replace into拿到id为1。
- 每个号段是10000个id号,id就是[10000, 20000)。
volatile AtomicLong idGenerator = new AtomicLong(10000)
volatile long maxId = 20000
3、获取ID:
- 通过封装的客户端,IdGenerator.next(),每次拿一个id,就是AtomicLong.incrementAndGet(),直接原子递增。
- 如果拿到了号段里最大id,此时需要进行阻塞。
- 然后重新到数据库获取ID。
4、特点:
高可用 -> 两台数据库(不同起始offset,相同步长)+ 故障自动转移
不需要考虑表数据量
支持多种业务
高并发 + 高性能 -> 不需要伸缩和扩容
号段自动更新 + 号段本地磁盘持久化
5、缺点:
- 每次重启服务,就会浪费一个号段里还没自增到的ID,重启后又是新的号段。
- 如果要优化,可以在spring销毁事件里,不允许获取id了,接着把AtomicLong的值持久化到本地磁盘,下次服务重启后直接从本地磁盘里读取。
6、总结:
- 优化后的方案可以直接用到生产,我司也是这个方案,只是做了改动。
- 相对没有snowflake生产级方案具备普适性,SnowFlake不涉及号段问题,不依赖数据库,就是peer-to-peer的集群架构,随时可以扩容。
- 时间戳+业务id,是最推荐的。