7 种分布式全局 ID 生成策略,你更爱哪种?

上了微服务之后,很多原本很简单的问题现在都变复杂了,例如全局 ID 这事!

最近工作中刚好用到这块内容,于是调研了市面上几种常见的全局 ID 生成策略,稍微做了一下对比,供小伙伴们参考。

当数据库分库分表之后,原本的主键自增就不方便继续使用了,需要找到一个新的合适的方案,我的需求就是在这样的情况下提出的。

接下来我们一起来捋一捋。

1. 两种思路

整体上来说,这个问题有两种不同的思路:

  • 让数据库自己搞定

  • Java 代码来处理主键,然后直接插入数据库中即可。

这两种思路又对应了不同的方案,我们一个一个来看。

2. 数据库自己搞定

数据库自己搞定,就是说我在数据插入的时候,依然不考虑主键的问题,希望继续使用数据库的主键自增,但是很明显,原本默认的主键自增现在没法用了,我们必须有新的方案。

2.1 修改数据库配置

数据库分库分表之后的结构如下图(假设数据库中间件用的 MyCat):

此时如果原本的 db1、db2、db3 继续各自主键自增,那么对于 MyCat 而言,主键就不是自增了,主键就会重复,用户从 MyCat 中查询到的数据主键就有问题。

找到问题的原因,那么剩下的就好解决了。

我们可以直接修改 MySQL 数据库主键自增的起始值和步长。

首先我们可以通过如下 SQL 查看与此相关的两个变量的取值:

SHOW VARIABLES LIKE 'auto_increment%'

可以看到,主键自增的起始值和步长都是 1。

起始值好改,在定义表的时候就可以设置,步长我们可以通过修改这个配置实现:

set @@auto_increment_increment=9;

修改后,再去查看对应的变量值,发现已经变了:

此时我们再去插入数据,主键自增就不是每次自增 1,而是每次自增 9 了。

至于自增起始值其实很好设置,创建表的时候就可以设置了。

create table test01(id integer PRIMARY KEY auto_increment,username varchar(255)) auto_increment=8;

既然 MySQL 可以修改自增的起始值和每次增长的步长,现在假设我有 db1、db2 和 db3,我就可以分别设置这三个库中表的自增起始值为 1、2、3,然后自增步长都是 3,这样就可以实现自增了。

但是很明显这种方式不够优雅,而且处理起来很麻烦,将来扩展也不方便,因此不推荐。

2.2 MySQL+MyCat+ZooKeeper

如果大家分库分表工具恰好使用的是 MyCat,那么结合 Zookeeper 也能很好的实现主键全局自增。

MyCat 作为一个分布式数据库中间,屏蔽了数据库集群的操作,让我们操作数据库集群就像操作单机版数据库一样,对于主键自增,它有自己的方案:

  1. 通过本地文件实现

  2. 通过数据库实现

  3. 通过本地时间戳实现

  4. 通过分布式 ZK ID 生成器实现

  5. 通过 ZK 递增方式实现

这里我们主要来看方案 4。

配置步骤如下:

  • 首先修改主键自增方式为 4 ,4 表示使用 zookeeper 实现主键自增。

server.xml

  • 配置表自增,并且设置主键

schema.xml

设置主键自增,并且设置主键为 id 。

  • 配置 zookeeper 的信息

在 myid.properties 中配置 zookeeper 信息:

  • 配置要自增的表

sequence_conf.properties

注意,这里表名字要大写。

  1. TABLE.MINID 某线程当前区间内最小值

  2. TABLE.MAXID 某线程当前区间内最大值

  3. TABLE.CURID 某线程当前区间内当前值

  4. 文件配置的MAXID以及MINID决定每次取得区间,这个对于每个线程或者进程都有效

  5. 文件中的这三个属性配置只对第一个进程的第一个线程有效,其他线程和进程会动态读取 ZK

  • 重启 MyCat 测试

最后重启 MyCat ,删掉之前创建的表,然后创建新表进行测试即可。

这种方式就比较省事一些,而且可扩展性也比较强,如果选择了 MyCat 作为分库分表工具,那么这种不失为一种最佳方案。

前面介绍这两种都是在数据库或者数据库中间件层面来处理主键自增,我们 Java 代码并不需要额外工作。

接下来我们再来看几种需要在 Java 代码中进行处理的方案。

3. Java 代码处理

3.1 UUID

最容易想到的就是 UUID (Universally Unique Identifier) 了, UUID 的标准型式包含 32 个 16 进制数字,以连字号分为五段,形式为 8-4-4-4-12 的 36 个字符,这个是 Java 自带的,用着也简单,最大的优势就是本地生成,没有网络消耗,但是但凡在公司做开发的小伙伴都知道这个东西在公司项目中使用并不多。原因如下:

  1. 字符串太长,对于 MySQL 而言,不利于索引。

  2. UUID 的随机性对于 I/O 密集型的应用非常不友好!它会使得聚簇索引的插入变得完全随机,使得数据没有任何聚集特性。

  3. 信息不安全:基于 MAC 地址生成 UUID 的算法可能会造成 MAC 地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。

因此,UUID 并非最佳方案。

3.2 SNOWFLAKE

雪花算法是由 Twitter 公布的分布式主键生成算法,它能够保证不同进程主键的不重复性,以及相同进程主键的有序性。在同一个进程中,它首先是通过时间位保证不重复,如果时间相同则是通过序列位保证。

同时由于时间位是单调递增的,且各个服务器如果大体做了时间同步,那么生成的主键在分布式环境可以认为是总体有序的,这就保证了对索引字段的插入的高效性。

例如 MySQL 的 Innodb 存储引擎的主键。使用雪花算法生成的主键,二进制表示形式包含 4 部分,从高位到低位分表为:1bit 符号位、41bit 时间戳位、10bit 工作进程位以及 12bit 序列号位。

  • 符号位 (1bit)

预留的符号位,恒为零。

  • 时间戳位 (41bit)

41 位的时间戳可以容纳的毫秒数是 2 的 41 次幂,一年所使用的毫秒数是:365 * 24 * 60 * 60 * 1000。通过计算可知:Math.pow(2, 41) / (365 * 24 * 60 * 60 * 1000L);结果约等于 69.73 年。

ShardingSphere 的雪花算法的时间纪元从 2016 年 11 月 1 日零点开始,可以使用到 2086 年,相信能满足绝大部分系统的要求。

  • 工作进程位 (10bit)

该标志在 Java 进程内是唯一的,如果是分布式应用部署应保证每个工作进程的 id 是不同的。该值默认为 0,可通过属性设置。

  • 序列号位 (12bit)

该序列是用来在同一个毫秒内生成不同的 ID。如果在这个毫秒内生成的数量超过 4096 (2 的 12 次幂),那么生成器会等待到下个毫秒继续生成。

注意:该算法存在 时钟回拨 问题,服务器时钟回拨会导致产生重复序列,因此默认分布式主键生成器提供了一个最大容忍的时钟回拨毫秒数。如果时钟回拨的时间超过最大容忍的毫秒数阈值,则程序报错;如果在可容忍的范围内,默认分布式主键生成器会等待时钟同步到最后一次主键生成的时间后再继续工作。最大容忍的时钟回拨毫秒数的默认值为 0,可通过属性设置。

下面我给出一个雪花算法的工具类,大家可以参考:

public class IdWorker {// 时间起始标记点,作为基准,一般取系统的最近时间(一旦确定不能变动)private final static long twepoch = 1288834974657L;// 机器标识位数private final static long workerIdBits = 5L;// 数据中心标识位数private final static long datacenterIdBits = 5L;// 机器ID最大值private final static long maxWorkerId = -1L ^ (-1L << workerIdBits);// 数据中心ID最大值private final static long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);// 毫秒内自增位private final static long sequenceBits = 12L;// 机器ID偏左移12位private final static long workerIdShift = sequenceBits;// 数据中心ID左移17位private final static long datacenterIdShift = sequenceBits + workerIdBits;// 时间毫秒左移22位private final static long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;private final static long sequenceMask = -1L ^ (-1L << sequenceBits);/* 上次生产id时间戳 */private static long lastTimestamp = -1L;// 0,并发控制private long sequence = 0L;private final long workerId;// 数据标识id部分private final long datacenterId;public IdWorker(){this.datacenterId = getDatacenterId(maxDatacenterId);this.workerId = getMaxWorkerId(datacenterId, maxWorkerId);}/*** @param workerId*            工作机器ID* @param datacenterId*            序列号*/public IdWorker(long workerId, long datacenterId) {if (workerId > maxWorkerId || workerId < 0) {throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));}if (datacenterId > maxDatacenterId || datacenterId < 0) {throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));}this.workerId = workerId;this.datacenterId = datacenterId;}/*** 获取下一个ID** @return*/public synchronized long nextId() {long timestamp = timeGen();if (timestamp < lastTimestamp) {throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));}if (lastTimestamp == timestamp) {// 当前毫秒内,则+1sequence = (sequence + 1) & sequenceMask;if (sequence == 0) {// 当前毫秒内计数满了,则等待下一秒timestamp = tilNextMillis(lastTimestamp);}} else {sequence = 0L;}lastTimestamp = timestamp;// ID偏移组合生成最终的ID,并返回IDlong nextId = ((timestamp - twepoch) << timestampLeftShift)| (datacenterId << datacenterIdShift)| (workerId << workerIdShift) | sequence;return nextId;}private long tilNextMillis(final long lastTimestamp) {long timestamp = this.timeGen();while (timestamp <= lastTimestamp) {timestamp = this.timeGen();}return timestamp;}private long timeGen() {return System.currentTimeMillis();}/*** <p>* 获取 maxWorkerId* </p>*/protected static long getMaxWorkerId(long datacenterId, long maxWorkerId) {StringBuffer mpid = new StringBuffer();mpid.append(datacenterId);String name = ManagementFactory.getRuntimeMXBean().getName();if (!name.isEmpty()) {/** GET jvmPid*/mpid.append(name.split("@")[0]);}/** MAC + PID 的 hashcode 获取16个低位*/return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1);}/*** <p>* 数据标识id部分* </p>*/protected static long getDatacenterId(long maxDatacenterId) {long id = 0L;try {InetAddress ip = InetAddress.getLocalHost();NetworkInterface network = NetworkInterface.getByInetAddress(ip);if (network == null) {id = 1L;} else {byte[] mac = network.getHardwareAddress();id = ((0x000000FF & (long) mac[mac.length - 1])| (0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 6;id = id % (maxDatacenterId + 1);}} catch (Exception e) {System.out.println(" getDatacenterId: " + e.getMessage());}return id;}
}

用法如下:

IdWorker idWorker = new IdWorker(0, 0);
for (int i = 0; i < 1000; i++) {System.out.println(idWorker.nextId());
}

3.3 LEAF

Leaf 是美团开源的分布式 ID 生成系统,最早期需求是各个业务线的订单 ID 生成需求。在美团早期,有的业务直接通过 DB 自增的方式生成 ID,有的业务通过 Redis 缓存来生成 ID,也有的业务直接用 UUID 这种方式来生成 ID。以上的方式各自有各自的问题,因此美团决定实现一套分布式 ID 生成服务来满足需求目前 Leaf 覆盖了美团点评公司内部金融、餐饮、外卖、酒店旅游、猫眼电影等众多业务线。在4C8G VM 基础上,通过公司 RPC 方式调用,QPS 压测结果近 5w/s,TP999 1ms(TP=Top Percentile,Top 百分数,是一个统计学里的术语,与平均数、中位数都是一类。TP50、TP90 和 TP99 等指标常用于系统性能监控场景,指高于 50%、90%、99% 等百分线的情况)。

目前 LEAF 的使用有两种不同的思路,号段模式和 SNOWFLAKE 模式,你可以同时开启两种方式,也可以指定开启某种方式(默认两种方式为关闭状态)。

我们从 GitHub 上 Clone LEAF 之后,它的配置文件在 leaf-server/src/main/resources/leaf.properties 中,各项配置的含义如下:

可以看到,如果使用号段模式,需要数据库支持;如果使用 SNOWFLAKE 模式,需要 Zookeeper 支持。

3.3.1 号段模式

号段模式还是基于数据库,但是思路有些变化,如下:

  1. 利用 proxy server 从数据库中批量获取 id,每次获取一个 segment (step 决定其大小) 号段的值,用完之后再去数据库获取新的号段,可以大大的减轻数据库的压力。

  2. 各个业务不同的发号需求用 biz_tag 字段来区分,每个 biz-tag 的 ID 获取相互隔离,互不影响。

  3. 如果有新的业务需要扩区 ID,只需要增加表记录即可。

如果使用号段模式,我们首先需要创建一张数据表,脚本如下:

CREATE DATABASE leaf
CREATE TABLE `leaf_alloc` (`biz_tag` varchar(128)  NOT NULL DEFAULT '',`max_id` bigint(20) NOT NULL DEFAULT '1',`step` int(11) NOT NULL,`description` varchar(256)  DEFAULT NULL,`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,PRIMARY KEY (`biz_tag`)
) ENGINE=InnoDB;insert into leaf_alloc(biz_tag, max_id, step, description) values('leaf-segment-test', 1, 2000, 'Test leaf Segment Mode Get Id')

这张表中各项字段的含义如下:

  • biz_tag:业务标记(不同业务可以有不同的号段序列)

  • max_id:当前号段下的最大 id

  • step:每次取号段的步长

  • description:描述信息

  • update_time:更新时间

配置完成后,启动项目,访问 http://localhost:8080/api/segment/get/leaf-segment-test 路径(路径最后面的 leaf-segment-test 是业务标记),即可拿到 ID。

可以通过如下地址访问到号段模式的监控页面 http://localhost:8080/cache

号段模式优缺点:

优点

  • Leaf 服务可以很方便的线性扩展,性能完全能够支撑大多数业务场景。

  • ID 号码是趋势递增的 8byte 的 64 位数字,满足上述数据库存储的主键要求。

  • 容灾性高:Leaf 服务内部有号段缓存,即使 DB 宕机,短时间内 Leaf 仍能正常对外提供服务。

  • 可以自定义 max_id 的大小,非常方便业务从原有的 ID 方式上迁移过来。

缺点

  • ID 号码不够随机,能够泄露发号数量的信息,不太安全。

  • DB 宕机会造成整个系统不可用。

3.3.2 SNOWFLAKE 模式

SNOWFLAKE 模式需要配合 Zookeeper 一起,不过 SNOWFLAKE 对 Zookeeper 的依赖是弱依赖,把 Zookeeper 启动之后,我们可以在 SNOWFLAKE 中配置 Zookeeper 信息,如下:

leaf.snowflake.enable=true
leaf.snowflake.zk.address=192.168.91.130
leaf.snowflake.port=2183

然后重新启动项目,启动成功后,通过如下地址可以访问到 ID:

http://localhost:8080/api/snowflake/get/test

3.4 Redis 生成

这个主要是利用 Redis 的 incrby 来实现,这个我觉得没啥好说的。

3.5 Zookeeper 处理

zookeeper 也能做,但是比较麻烦,不推荐。

4. 小结

综上,如果项目中恰好使用了 MyCat,那么可以使用 MyCat+Zookeeper,否则建议使用 LEAF,两种模式皆可。


往期推荐

实战:隐藏SpringBoot中的私密数据!


@Autowired的这些骚操作,你都知道吗?


SpringBoot时间格式化的5种方法!


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/544635.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C# 读取照片的EXIF信息

一、使用 MetadataExtractor 读取 EXIF 信息 1、NuGet 中安装 在 NuGet 中搜索并安装 MetadataExtractor&#xff1b; 2、包信息 我安装后会有两个包&#xff1a;MetadataExtractor 2.0.0 和 XmpCore 5.1.3 3、代码实现 我是创建的 WPF 项目&#xff1a; private void B…

ReentrantLock 中的 4 个坑!

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;JDK 1.5 之前 synchronized 的性能是比较低的&#xff0c;但在 JDK 1.5 中&#xff0c;官方推出一个重量级功能 Lock&#x…

SpringBoot 中 4 大核心组件,你了解多少?

Spring Boot 中的 4 大组件分别是&#xff1a;Spring Boot Starter、Spring Boot Autoconfigure、Spring Boot CLI 以及 Spring Boot actuator&#xff0c;接下来&#xff0c;我们分别来看他们的使用和作用。1.Spring Boot Starter1.1 Starter的应用示例<dependency><…

双重检查锁,原来是这样演变来的,你了解吗

最近在看Nacos的源代码时&#xff0c;发现多处都使用了“双重检查锁”的机制&#xff0c;算是非常好的实践案例。这篇文章就着案例来分析一下双重检查锁的使用以及优势所在&#xff0c;目的就是让你的代码格调更加高一个层次。同时&#xff0c;基于单例模式&#xff0c;讲解一下…

WakaTime 记录你的时间(Moana 自动同步信息客户端)

X、写在前面 代码界有一神器&#xff0c;可以记录敲代码的时间&#xff0c;项目名称&#xff0c;编译器等信息&#xff0c;可以极大的满足程序员的虚荣心&#xff0c;它就是 WakaTime 网站链接 WakaTime 可以记录敲代码时间&#xff0c;和具体编辑的文件等信息&#xff0c;并…

图解:为什么非公平锁的性能更高?

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;在 Java 中 synchronized 和 ReentrantLock 默认使用的都是非公平锁&#xff0c;而它们采用非公平锁的原因都是一致的&#…

死锁的 4 种排查工具 !

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone死锁&#xff08;Dead Lock&#xff09;指的是两个或两个以上的运算单元&#xff08;进程、线程或协程&#xff09;&#xff0c;都在等待…

【HM】第2课:JavaScript基础

<pre>day02第一天的内容&#xff1a;*html标签里面的表单标签*html标签里面的表格标签思维导图1、JavaScript的简介* 什么是JavaScript&#xff1a;js是一个基于对象和事件驱动的语言&#xff0c;应用客户端。**基于对象&#xff1a;在java里面如果使用对象需要创建&…

你没有见过的 7 种 for 循环优化,超好用!

来源&#xff1a;blog.csdn.net/csdn_aiyang/article/details/75162134我们都经常使用一些循环耗时计算的操作&#xff0c;特别是for循环&#xff0c;它是一种重复计算的操作&#xff0c;如果处理不好&#xff0c;耗时就比较大&#xff0c;如果处理书写得当将大大提高效率&…

死锁终结者:顺序锁和轮询锁!

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone死锁&#xff08;Dead Lock&#xff09;指的是两个或两个以上的运算单元&#xff08;进程、线程或协程&#xff09;&#xff0c;都在等待…

轮询锁使用时遇到的问题与解决方案!

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone当我们遇到死锁之后&#xff0c;除了可以手动重启程序解决之外&#xff0c;还可以考虑是使用顺序锁和轮询锁&#xff0c;这部分的内容可以…

16 条 yyds 的代码规范

作者 | 涛姐涛哥链接 | cnblogs.com/taojietaoge/p/11575376.html背景&#xff1a;如何更规范化编写Java 代码的重要性想必毋需多言&#xff0c;其中最重要的几点当属提高代码性能、使代码远离Bug、令代码更优雅。一、MyBatis 不要为了多个查询条件而写 1 1当遇到多个查询条件…

C# 导出word文档及批量导出word文档(3)

在初始化WordHelper时&#xff0c;要获取模板的相对路径。获取文档的相对路径多个地方要用到&#xff0c;比如批量导出时要先保存文件到指定路径下&#xff0c;再压缩打包下载&#xff0c;所以专门写了个关于获取文档的相对路径的类。 1 #region 获取文档的相对路径2 pub…

再见收费的 XShell,我改用国产良心工具!

使用或维护Linux系统的都知道&#xff0c;我们日常对服务器的操作&#xff0c;一般都会借助SSH工具远程登录到服务器之后进行操作。常用的SSH工具有不少&#xff0c;比如&#xff1a;Xshell、Putty、SSH Secure Shell Client、secureCRT等等。我使用过其中两种secureCRT和Xshel…

全球六大国际域名解析量统计报告(6月25日)

IDC评述网&#xff08;idcps.com&#xff09;06月29日报道&#xff1a;根据DailyChanges公布的实时数据显示&#xff0c;在2015年6月25日&#xff0c;全球六大国际域名解析量总量持续攀升至153,246,819个&#xff0c;环比6月16日&#xff0c;净增46,078个&#xff0c;涨幅增大3…

Windows 创建符号链接

一、场景分析 1.环境变量 在Windows系统配置 环境变量 的时候&#xff0c;经常会遇到以下 路径 情况&#xff1a; C:\Program Files C:\Program Files (x86)\Common Files2.异常情况 这种路径中&#xff0c;存在空格字符&#xff0c;在一些程序调用时&#xff0c;可能出现异…

1.3w字,一文详解死锁!

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone死锁&#xff08;Dead Lock&#xff09;指的是两个或两个以上的运算单元&#xff08;进程、线程或协程&#xff09;&#xff0c;都在等待…

PHP与ThinkPHP读写文件

2019独角兽企业重金招聘Python工程师标准>>> 使用php将数据写入到指定的文件 $str"<?php return".var_export($phiz,true)."?>"; file_put_contents(./Data/phiz.php); 使用php读取指定的文件 …

【图解】透彻Java线程状态转换

大家好&#xff0c;我是阿星&#xff0c;好久不见&#xff0c;欢迎来到Java并发编程系列番外篇线程状态转换&#xff0c;内容通俗易懂&#xff0c;请放心食用。线程状态先来个开场四连问Java线程状态有几个&#xff1f;Java线程状态是如何转换&#xff1f;Java线程状态转换什么…

CentOS7安装Hadoop2.7完整流程

2019独角兽企业重金招聘Python工程师标准>>> 1、环境&#xff0c;3台CentOS7&#xff0c;64位&#xff0c;Hadoop2.7需要64位Linux&#xff0c;CentOS7 Minimal的ISO文件只有600M&#xff0c;操作系统十几分钟就可以安装完成&#xff0c; Master 192.168.0.182 Slav…