分布式全局唯一ID生成策略?

一、背景

分布式系统中我们会对一些数据量大的业务进行分拆,如:用户表,订单表。因为数据量巨大一张表无法承接,就会对其进行分库分表。
但一旦涉及到分库分表,就会引申出分布式系统中唯一主键ID的生成问题。

1.1 唯一ID的特性

  1. 整个系统ID唯一;
  2. ID是数字类型,而且是趋势递增;
  3. ID简短,查询效率快。

1.2 递增与趋势递增

递增 趋势递增
第一次生成的ID为12,下一次生成的ID是13,再下一次生成的ID是14。 什么是?如:在一段时间内,生成的ID是递增的趋势。如:再一段时间内生成的ID在【0,1000】之间,过段时间生成的ID在【1000,2000】之间。但在【0-1000】区间内的时候,ID生成有可能第一次是12,第二次是10,第三次是14。

二、方案

2.1 UUID

UUID全称:Universally Unique Identifier。标准型式包含32个16进制数字,以连字号分为五段,形式为8-4-4-4-12的36个字符,示例:9628f6e9-70ca-45aa-9f7c-77afe0d26e05

  • 优点:
  1. 代码实现简单;
  2. 本机生成,没有性能问题;
  3. 因为是全球唯一的ID,所以迁移数据容易。
  • 缺点:
  1. 每次生成的ID是无序的,无法保证趋势递增;
  2. UUID的字符串存储,查询效率慢;
  3. 存储空间大;
  4. ID本身无业务含义,不可读。
  • 应用场景:
  1. 类似生成token令牌的场景;
  2. 不适用一些要求有趋势递增的ID场景,不适合作为高性能需求的场景下的数据库主键。

也有在线生成UUID的网站,如果你的项目上用到了UUID,可以用来生成临时的测试数据。https://www.uuidgenerator.net/

2.2 MySQL主键自增

利用了MySQL的主键自增auto_increment,默认每次ID1

优点:

  1. 数字化,ID递增;
  2. 查询效率高;
  3. 具有一定的业务可读。
  • 缺点:
  1. 存在单点问题,如果MySQL挂了,就没法生成ID了;
  2. 数据库压力大,高并发抗不住。

2.3 MySQL多实例主键自增

这个方案就是解决MySQL的单点问题,在auto_increment基本上面,设置step步长

如上,每台的初始值分别为1,2,3...N,步长为N(这个案例步长为4

  • 优点:解决了单点问题;
  • 缺点:一旦把步长定好后,就无法扩容;而且单个数据库的压力大,数据库自身性能无法满足高并发。
  • 应用场景:数据不需要扩容的场景。

2.4 基于Redis实现

  • 单机:Redisincr函数在单机上是原子操作,可以保证唯一且递增。
  • 集群:单机Redis可能无法支撑高并发。集群情况下,可以使用步长的方式。比如有5个Redis节点组成的集群,它们生成的ID分别为:
A: 1,6,11,16,21
B: 2,7,12,17,22
C: 3,8,13,18,23
D: 4,9,14,19,24
E: 5,10,15,20,25
  • 优点:有序递增,可读性强。
  • 缺点:占用带宽,每次要向Redis进行请求。

三、优化方案

3.1、改造数据库主键自增

数据库的自增主键的特性,可以实现分布式ID,适合做userId,正好符合如何永不迁移数据和避免热点? 但这个方案有严重的问题:

  1. 一旦步长定下来,不容易扩容;
  2. 数据库压力山大。
  • 为什么压力大?

因为我们每次获取ID的时候,都要去数据库请求一次。那我们可以不可以不要每次去取?

可以请求数据库得到ID的时候,可设计成获得的ID是一个ID区间段。

  • 上图ID规则表含义:
  1. id表示为主键,无业务含义;
  2. biz_tag为了表示业务,因为整体系统中会有很多业务需要生成ID,这样可以共用一张表维护;
  3. max_id表示现在整体系统中已经分配的最大ID;
  4. desc描述;
  5. update_time表示每次取的ID时间;
  • 整体流程:
  1. 【用户服务】在注册一个用户时,需要一个用户ID;会请求【生成ID服务(是独立的应用)】的接口;
  2. 【生成ID服务】会去查询数据库,找到user_tagid,现在的max_id0step=1000;
  3. 【生成ID服务】把max_idstep返回给【用户服务】;并且把max_id更新为max_id = max_id + step,即更新为1000;
  4. 【用户服务】获得max_id=0step=1000;
  5. 这个用户服务可以用ID=【max_id + 1,max_id+step】区间的ID,即为【1,1000】;
  6. 【用户服务】会把这个区间保存到jvm中;
  7. 【用户服务】需要用到ID的时候,在区间【1,1000】中依次获取ID,可采用AtomicLong中的getAndIncrement方法;
  8. 如果把区间的值用完了,再去请求【生产ID服务】接口,获取到max_id1000,即可以用【max_id + 1,max_id+step】区间的ID,即为【1001,2000】
  9. 该方案就非常完美的解决了数据库自增的问题,而且可以自行定义max_id的起点,和step步长,非常方便扩容;
  10. 也解决了数据库压力的问题,因为在一段区间内,是在jvm内存中获取的,而不需要每次请求数据库。即使数据库宕机了,系统也不受影响,ID还能维持一段时间。

3.2 竞争问题

以上方案中,如果是多个用户服务,同时获取ID,同时去请求【ID服务】,在获取max_id的时候会存在并发问题。如:

用户服务A,取到的max_id=1000 ;用户服务B取到的也是max_id=1000,那就出现了问题,ID重复了。

解决方案是:加分布式锁,保证同一时刻只有一个用户服务获取max_id

3.3 突发阻塞问题

因为竞争问题,所有只有一个用户服务去操作数据库,其他二个会被阻塞。出现的现象就是一会儿突然系统耗时变长,怎么去解决?

  • buffer方案

流程如下:

  1. 当前获取IDbuffer1中,每次获取IDbuffer1中获取;
  2. buffer1中的ID已经使用到了100,也就是达到区间的10%;
  3. 达到了10%,先判断buffer2中有没有去获取过,如果没有就立即发起请求获取ID线程,此线程把获取到的ID,设置到buffer2中;
  4. 如果buffer1用完了,会自动切换到buffer2;
  5. buffer2用到10%了,也会启动线程再次获取,设置到buffer1中;
  6. 依次往返。

3.4 总结

  1. buffer的方案就达到了业务场景用的ID,都是在jvm内存中获得的,从此不需要到数据库中获取了,数据库宕机时长长点儿也没太大影响了。
  2. 因为会有一个线程,会观察什么时候去自动获取。两个buffer之间自行切换使用,就解决了突发阻塞的问题。

四、其他方式

还有一些其他的ID生成方案,比如:

  1. 滴滴:时间+起点编号+车牌号;
  2. 淘宝订单:时间戳+用户ID
  3. 其他电商:时间戳+下单渠道+用户ID,有的会加上订单第一个商品的ID;
  4. MongoDBID:通过时间+机器码+pid+inc共12个字节,4+3+2+3的方式最终标识成一个24长度的十六进制字符。

原文地址:https://www.cnblogs.com/vandusty/p/11462585.html

时间: 2024-10-02 23:46:46

分布式全局唯一ID生成策略?的相关文章

常见分布式全局唯一ID生成策略

全局唯一的 ID 几乎是所有系统都会遇到的刚需.这个 id 在搜索, 存储数据, 加快检索速度 等等很多方面都有着重要的意义.工业上有多种策略来获取这个全局唯一的id,针对常见的几种场景,我在这里进行简单的总结和对比. 简单分析一下需求 [1] 所谓全局唯一的 id 其实往往对应是生成唯一记录标识的业务需求. 这个 id 常常是数据库的主键,数据库上会建立聚集索引(cluster index),即在物理存储上以这个字段排序.这个记录标识上的查询,往往又有分页或者排序的业务需求.所以往往要有一个t

分布式全局唯一ID的实现

分布式全局唯一ID的实现 前言 上周末考完试,这周正好把工作整理整理,然后也把之前的一些素材,整理一番,也当自己再学习一番. 一方面正好最近看到几篇这方面的文章,另一方面也是正好工作上有所涉及,所以决定写一篇这样的文章. 先是简单介绍概念和现有解决方案,然后是我对这些方案的总结,最后是我自己项目的解决思路. 概念 在复杂分布式系统中,往往需要对大量的数据和消息进行唯一标识. 如在金融.电商.支付.等产品的系统中,数据日渐增长,对数据分库分表后需要有一个唯一ID来标识一条数据或消息,数据库的自增I

分布式全局唯一ID与自增序列

包含时间顺序的ID 此场景最简单的实现方案,就是采用 twitter 的 Snowflake 算法.ID总长64位,第1位不可用,41位表示时间戳,10位表示生成机器的id,后12位表示序列号. 为什么第一位不可用?第一位为0,可以确保ID在java的long类型数据一直为正整数递增 同一时间戳即毫秒内,能产生多少个ID? 2^12 = 4096 个ID [ 0 ~ 4095 ] 唯一性?通过机器ID预先已经做了一次空间隔离,再通过时间戳做了一次时间隔离,最后通过时间戳内的计数实现了一定程度内的

zookeeper全局唯一id生成

一背景 传统生成id方式可以靠数据库的自增来实现,但是在分布式环境下不太适应.依赖数据库容易造成单点. 为什么不用UUID的,网上看别人介绍的时候,从两个方面去分析: 1 大并发的情况下,UUID会出现重复. 2.UUID是随即的,含义不明.从业务角度去考虑,如果用作订单,用户查询订单在数据分片的情况下很可能分散在多个库,查询困难. 全局唯一id的要求比较高: 不能有单点故障. 性能好,毫秒级返回. 能顺序便于DB存储及划分. 二 使用zookeeper生成全局唯一id. 2.1 利用Zooke

详解分布式系统中的唯一id生成策略

系统唯一ID是我们在设计一个系统的时候常常会遇见的问题,也常常为这个问题而纠结.生成ID的方法有很多,适应不同的场景.需求以及性能要求.所以有些比较复杂的系统会有多个ID生成的策略.下面就介绍一些常见的ID生成策略. 平常应用中方式很多 1. 数据库自增长序列或字段 2. UUID 3.uuid的变种 4.redis生成 5. Twitter的snowflake算法 snowflake是Twitter开源的分布式ID生成算法,结果是一个long型的ID.其核心思想是:使用41bit作为毫秒数,1

分布式系统全局唯一ID生成

一 什么是分布式系统唯一ID 在复杂分布式系统中,往往需要对大量的数据和消息进行唯一标识. 如在金融.电商.支付.等产品的系统中,数据日渐增长,对数据分库分表后需要有一个唯一ID来标识一条数据或消息,数据库的自增ID显然不能满足需求,此时一个能够生成全局唯一ID的系统是非常必要的. 二.分布式系统唯一ID的特点 全局唯一性:不能出现重复的ID号,既然是唯一标识,这是最基本的要求. 趋势递增:在MySQL InnoDB引擎中使用的是聚集索引,由于多数RDBMS使用B-tree的数据结构来存储索引数

mysql全局唯一ID生成方案(二)

MySQL数据表结构中,一般情况下,都会定义一个具有‘AUTO_INCREMENT’扩展属性的‘ID’字段,以确保数据表的每一条记录都可以用这个ID唯一确定: 随着数据的不断扩张,为了提高数据库查询性能,降低查询热点,一般都会把一张表按照一定的规则分成多张数据表,即常说的分表: 分表除了表名的索引不同之外,表结构都是一样的,如果各表的‘ID’字段仍采用‘AUTO_INCREMENT’的方式的话,ID就不能唯确定一条记录了. 这时就需要一种处于各个分表之外的机制来生成ID,我们一般采用一张单独的数

Twitter全局唯一ID生成算法

测试:private static void TestIdWorker() { HashSet<long> set = new HashSet<long>(); IdWorker idWorker1 = new IdWorker(0, 0); IdWorker idWorker2 = new IdWorker(1, 0); //762884413578018816 //762884520121729024 Stopwatch sw = new Stopwatch(); sw.Sta

分布式高并发下全局ID生成策略

数据在分片时,典型的是分库分表,就有一个全局ID生成的问题.单纯的生成全局ID并不是什么难题,但是生成的ID通常要满足分片的一些要求:   1 不能有单点故障.   2 以时间为序,或者ID里包含时间.这样一是可以少一个索引,二是冷热数据容易分离.   3 可以控制ShardingId.比如某一个用户的文章要放在同一个分片内,这样查询效率高,修改也容易.   4 不要太长,最好64bit.使用long比较好操作,如果是96bit,那就要各种移位相当的不方便,还有可能有些组件不能支持这么大的ID.