分布式全局唯一ID

分布式全局唯一ID


(一)UUID

缺点:

  1. UUID太长
  2. UUID无序

(二)MySQL自增ID

  1. 需要单独一个MySQL实例,在这个实例新建一个单独的表
1
2
3
4
5
6
7
8
CREATE DATABASE `SEQID`;

CREATE TABLE SEQID.SEQUENCE_ID (
id bigint(20) unsigned NOT NULL auto_increment,
stub char(10) NOT NULL default '',
PRIMARY KEY (id),
UNIQUE KEY stub (stub)
) ENGINE=MyISAM;

stub没有实际意义,只是为了方便插入数据,只有插入数据才能产生自增ID。

  1. 可以使用下列语句生成并获取到一个自增ID
1
2
3
4
bigin;
replace into SEQUENCE_ID(stub) VALUES ('anyword');
select last_insert_id();
commit;

replace into优于insert into的地方:
replace into会先看是否存在和stub指定值一样的数据,如果存在则先deleteinsert,如果不存在则直接insert

缺点

  1. 业务系统每需要一个ID,都会请求同一个数据库,单数据库压力大、性能低
  2. 单点故障,一旦数据库宕机,整个系统都受影响

优化

1.MySQL双主模式

  1. 使用两台MySQL实例,做如下配置:
1
2
3
4
5
6
7
# 第一台MySQL实例
set @@auto_increment_offset = 1; # 初始值
set @@auto_increment_increment = 2; # 步长

# 第二台MySQL实例
set @@auto_increment_offset = 2; # 初始值
set @@auto_increment_increment = 2; # 步长
  1. 分布式应用随机去两个MySQL实例获取ID
优点
  • MySQL实例宕机,另一台MySQL实例可以继续工作,不会影响整个系统,避免了单点故障
缺点
  • 不利于扩容

2.号段模式

  1. 创建MySQL表,用于存储当前最大ID
1
2
3
4
5
6
CREATE TABLE id_generator (
id int(10) NOT NULL,
current_max_id bigint(20) NOT NULL COMMENT '当前最大ID',
increment_step int(10) NOT NULL COMMENT '号段长度',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  1. 应用程序从MySQL获取ID时,批量获取多个ID(称为一个“号段”)并进行缓存(可以是:①JVM缓存在本地 / ②Redis缓存)/③MyCat缓存。这样应用程序只需要在本地自增并返回,不需要每次请求数据库,只有缓存号段用完才去数据库获取下一号段。
  2. 可以在表中增加一个version字段,保证获取ID号段时的线程安全性
1
2
update id_generator set current_max_id=#{newMaxId},
version=version + 1 where version=#{version}

双主模式号段模式可以结合使用。


(三)雪花算法(snow-flake

思路:

  1. 保证生成分布式ID的每台机器在每ms内生成不一样的ID
  2. 分布式ID固定是一个long类型的数字,一个long64bit。由于Long.MAX_VALUE = 2 ^ 63 - 1 = 9223372036854775807,也就是说long类型最多19位,用一个19位的String就可以保存。

    64 bit - snowflake id = 1 bit - 标志位 + 41 bit - 时间戳 + 10 bit - 工作机器ID + 12 bit - 序列号

  • 第一个1 bit标识部分,在Java中由于long最高位是符号位,正数0,负数1,一般生成的ID都是正数,所以固定是0;

  • 时间戳部分占41 bit,是一个ms级的时间,一般不会存储当前时间,而是时间戳差值(当前时间 - 固定开始时间),41位的时间戳可以使用69年((1 << 41) / (365 * 24 * 60 * 60 * 1000) = 69 年);

    当前时间戳应该使用应用启动时间对应的ms数原子递增,不能使用System.currentTimeMillis(),因为后者如果机器时间回调,可能出现重复ID。

  • 工作机器ID占据10 bit,这部分比较灵活,比如前5位作为数据中心机房标识,后5位作为单机房机器标识,可以部署1024个节点

  • 序列号部分占12 bit,支持同一ms内同一节点可以生成4096个ID。序列号可以从MySQL获取自增ID,可以参考上述的“MySQL序列号 + JVM/Redis/MyCat缓存”方案。


(四)Redis

  • 利用Redisincr原子命令实现ID的自增和返回,但是需要持久化
1
2
3
4
# 设置ID初始值
set seq_id 1
# 自增 & 返回,是原子操作
incr seq_id

缺点

  1. 如果使用的RDB持久化方式:RDB持久化会丢失宕机期间到上一次快照存储期间的数据,所以可能出现重复ID
  2. 如果使用的AOF持久化方式:不会出现重复ID,但是会由于incr命令太多导致重启数据恢复时间过长

(五)应用:短URL(短网址)服务

百度短网址 —— https://dwz.cn/
比如输入http://www.dragonbaby308.com/,生成https://dwz.cn/SyQc17kp,二者指向地址都是一样的。

优点

  1. 短信或其他各类社交平台(比如微博)都会有字数限制,使用短链接可以减少字数
  2. 比起一堆不明所以的参数,使用短链接更加简洁友好,同时不暴露参数会更安全

原理

HTTP 301/302重定向

  • 将长URL转为短URL
  • 用户访问短URL,会通过HTTP 301/302重定向,访问到对应的长URL

长URL如何转短URL?

  • 建立一个发号器,每次有一个新的长URL进来,就将短网址 + 1然后返回,同时需要记录下长URL和短URL的映射关系(可以通过MySQL进行持久化存储)

    比如第一个URL返回www.dwz.cn/1
    第二个URL则返回www.dwz.cn/2

  • 使用这种方式,同一个长URL多次请求返回的短URL是不同的

    即便是百度短网址,也没有实现同一个长URL多次请求返回相同的短URL,因为这样的代价太大,并且没有什么意义。
    百度短网址会保存1年的映射关系。

存在的问题:

其实发号器究根结底,也就是一种 分布式唯一ID,分布式唯一ID中MySQL发号的方案存在的高并发、单点故障问题,短URL发号器也同样会有。

1.如果直接存储在MySQL,高并发时库压力大怎么办?
  1. 加一层Redis热点缓存
  2. 采用MySQL号段 + 缓存(JVM缓存/Redis缓存)的形式,每次从数据库获取一批号段,在缓存中进行分配,缓存号段用完才去数据库请求新的号段。可以有效减少库压力
2.采用单一MySQL,出现单点故障怎么办?
  1. 采用MySQL双主模式,比如一个MySQL发号器发单号,另一个发双号
  2. 采用雪花算法
-------------本文结束感谢您的阅读-------------

本文标题:分布式全局唯一ID

文章作者:DragonBaby308

发布时间:2019年09月10日 - 22:35

最后更新:2019年11月02日 - 19:52

原始链接:http://www.dragonbaby308.com/Distributed-uniqueId/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

急事可以使用右下角的DaoVoice,我绑定了微信会立即回复,否则还是推荐Valine留言喔( ఠൠఠ )ノ
0%