上了微服务之后,很多原本很简单的问题现在都变复杂了,例如全局 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,两种模式皆可。

有道无术,术可成;有术无道,止于术

欢迎大家关注Java之道公众号

好文章,我在看❤️

七种分布式全局 ID 生成策略,你更爱哪种?相关推荐

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

    上了微服务之后,很多原本很简单的问题现在都变复杂了,例如全局 ID 这事! 最近工作中刚好用到这块内容,于是调研了市面上几种常见的全局 ID 生成策略,稍微做了一下对比,供小伙伴们参考. 当数据库分库 ...

  2. 架构设计 | 分布式业务系统中,全局ID生成策略

    本文源码:GitHub·点这里 || GitEE·点这里 一.全局ID简介 在实际的开发中,几乎所有的业务场景产生的数据,都需要一个唯一ID作为核心标识,用来流程化管理.比如常见的: 订单:order ...

  3. 微服务架构分布式全局唯一ID生成策略及算法

    全局唯一的 ID 几乎是所有系统都会遇到的刚需.这个 id 在搜索, 存储数据, 加快检索速度 等等很多方面都有着重要的意义.工业上有多种策略来获取这个全局唯一的id,针对常见的几种场景,我在这里进行 ...

  4. 集群高并发情况下如何保证分布式唯一全局ID生成

    作者:轻狂书生 blog.csdn.net/LookForDream_/article/details/109355335 前言 系统唯一ID是我们在设计一个系统的时候常常会遇见的问题,也常常为这个问 ...

  5. 面试官:高并发下,如何保证分布式唯一全局 ID 生成?

    欢迎关注方志朋的博客,回复"666"获面试宝典 前言 系统唯一ID是我们在设计一个系统的时候常常会遇见的问题,也常常为这个问题而纠结. 这篇文章就是给各位看官提供一个生成分布式唯一 ...

  6. delphi 获取webbrowser文本框id内数值_分布式 ID 生成策略

    对于系统中的一组数据而言,必不可少地对应有唯一标识.简单的单体应用可以使用数据库的自增 ID 作为唯一标识.而在复杂的分布式系统中,就需要一些特定的策略去生成对应的分布式 ID. 常见的项目中 ID ...

  7. Reids实战——优惠券秒杀(全局唯一ID生成策略)

    1 全局唯一ID生成策略 每个店铺都可以发布优惠券: 当用户抢购时,就会生成订单并保存到tb_voucher_order这张表中,而订单表如果使用数据库自增ID就存在一些问题: 1. id的规律性太明 ...

  8. asp按时间自动递增编号_Java秒杀系统实战系列-分布式唯一ID生成订单编号

    本文是"Java秒杀系统实战系列文章"的第七篇,在本文中我们将重点介绍 "在高并发,如秒杀的业务场景下如何生成全局唯一.趋势递增的订单编号",我们将介绍两种方法 ...

  9. [转]【分布式系统】唯一ID生成策略总结

    文章目录 全局唯一id介绍 全局唯一id特点: 常见全局唯一id生成策略 1.数据库自增长序列或字段生成id 2.UUID 3.Redis生成ID 4.zookeeper生成ID 5.Twitter的 ...

最新文章

  1. BZOJ4866 Ynoi2017由乃的商场之旅(莫队)
  2. android 自定义actionbar前面有一块空白,解决Android V7后自定义Toolbar、ActionBar左侧有空白问题...
  3. 010_Redis的发布和订阅
  4. Linux系统网络基础知识及配置
  5. 让远程协助在局域网里无处不在
  6. android开发学习——Mina框架
  7. 重置Oracle密码
  8. transformers Tokenizer
  9. 软件测试经典面试题(一)给你一个水杯如何测试
  10. 层次分析法模型及代码
  11. 《Splunk智能运维实战》——第1章 游戏时间——导入数据 1.1 简介
  12. php ajax 域名查询,PHP+AJAX 域名查询 PHP 源代码
  13. 如何通过系统信息查看 Mac 上的显示刷新率?
  14. 关于fftshift引发的问题与思考
  15. Mkdocs部署静态网页至GitHub pages配置说明(mkdocs.yml)
  16. echarts3 地图应用 给背景地图上色(2)附:世界各大城市经纬度
  17. Spring框架学习 思维导图
  18. 美团/饿了么外卖CPS联盟返利公众号小程序核心源码
  19. 推特营销|推特群推你有无限可能
  20. PUK ACM题目分类

热门文章

  1. mysql 局域网连接失败
  2. war 发布后页面不更新_吐槽 | 都发布一万年了,这游戏还不“更新”?
  3. java for循环前面label_Java 实例
  4. java工程如何跑起来的_你编写的Java代码是咋跑起来的?
  5. freebsd mysql utf8_FreeBSD下安装mysql基本步骤
  6. java 判断请求为 ajax请求_Java后台判断ajax请求及处理过程详解
  7. shell中修改=后的值
  8. (软件工程复习核心重点)第四章总体设计-第四节:描绘软件结构的图形工具
  9. USACO-Section1.4 Prime Cryptarithm (搜索)
  10. Shell整数型变量自增自减的实现方式(+1,-1,++,--)