您的位置:首页 > 其它

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

2019-09-05 00:04 225 查看
原文链接:http://www.cnblogs.com/vandusty/p/11462585.html

一、背景

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

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
,默认每次
ID
1

优点:

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

2.3 MySQL多实例主键自增

这个方案就是解决

MySQL
的单点问题,在
auto_increment
基本上面,设置
step
步长

如上,每台的初始值分别为

1
,
2
,
3
...
N
,步长为
N
(这个案例步长为
4

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

2.4 基于Redis实现

  • 单机:

    Redis
    incr
    函数在单机上是原子操作,可以保证唯一且递增。

  • 集群:单机

    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_tag
    id
    ,现在的
    max_id
    0
    step=1000
    ;
  3. 【生成
    ID
    服务】把
    max_id
    step
    返回给【用户服务】;并且把
    max_id
    更新为
    max_id = max_id + step
    ,即更新为
    1000
    ;
  4. 【用户服务】获得
    max_id=0
    step=1000
    ;
  5. 这个用户服务可以用
    ID=【max_id + 1,max_id+step】
    区间的
    ID
    ,即为
    【1,1000】
    ;
  6. 【用户服务】会把这个区间保存到
    jvm
    中;
  7. 【用户服务】需要用到
    ID
    的时候,在区间
    【1,1000】
    中依次获取
    ID
    ,可采用
    AtomicLong
    中的
    getAndIncrement
    方法;
  8. 如果把区间的值用完了,再去请求【生产

    ID
    服务】接口,获取到
    max_id
    1000
    ,即可以用
    【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. 当前获取
    ID
    buffer1
    中,每次获取
    ID
    buffer1
    中获取;
  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. MongoDB
    ID
    :通过
    时间+机器码+pid+inc
    共12个字节,
    4+3+2+3
    的方式最终标识成一个24长度的十六进制字符。

转载于:https://www.cnblogs.com/vandusty/p/11462585.html

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: