分布式唯一ID(一)--常见的分布式唯一ID生成方案

一、背景:

当需要给数据添加唯一标识符,就需要分布式唯一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,是最推荐的。
posted @ 2022-02-28 16:39  Diamond-Shine  阅读(888)  评论(0编辑  收藏  举报