解决方案:分布式ID生成


关键词

  • UUID,32位
  • 数据库自增ID
  • 中间件(redis,原子操作 INCR和INCRBY)
  • Snowflake,64 bit,自增排序(强依赖机器时钟)

一个ID一般来说有下面几种要素:

  • 全局唯一:不能出现重复的ID号,既然是唯一标识,这是最基本的要求。
  • 趋势递增:确保生成的ID是对于某个用户或者业务是按一定的数字有序递增的。
  • 高可用性:确保任何时候都能正确的生成ID。
  • 带时间:ID里面包含时间,一眼扫过去就知道哪天的交易。

一、UUID

Java自带的生成UUID的方式就能生成一串唯一随机32(32个16进制数字)长度数据,而且够我们用N亿年,全球唯一。

优点:

  • 简单,代码方便。
  • 生成ID性能非常高:本地生成,没有网络消耗。
  • 全球唯一,在遇见数据迁移,系统数据合并,或者数据库变更等情况下,可以从容应对。

缺点:

  • 不易于存储:UUID太长,16字节128位,通常以36长度的字符串表示,很多场景不适用。

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

  • ID作为主键时在特定的环境会存在一些问题,比如做DB主键的场景下,UUID就非常不适用:
    ① MySQL官方有明确的建议主键要尽量越短越好[4],36个字符长度的UUID不符合要求。

    All indexes other than the clustered index are known as secondary indexes. In InnoDB, each record in a secondary index contains the primary key columns for the row, as well as the columns specified for the secondary index. InnoDB uses this primary key value to search for the row in the clustered index.* If the primary key is long, the secondary indexes use more space, so it is advantageous to have a short primary key*.

    ② 对MySQL索引不利:如果作为数据库主键,在InnoDB引擎下,UUID的无序性可能会引起数据位置频繁变动,严重影响性能。

这是一种简单的生成方式,简单,高效,但在一般业务系统中我还没见过有这种生成方式。

二、数据库自增ID

数据库主键设置自增序号,以一定的趋势自增,以保证主键ID的唯一性。

以MySQL举例,利用给字段设置 auto_increment_increment 和auto_increment_offset 来保证ID自增,每次业务使用下列SQL读写MySQL得到ID号。

优点:

  • 非常简单,利用现有数据库系统的功能实现,成本小,有DBA专业维护。
  • ID号单调自增,可以实现一些对ID有特殊要求的业务。

缺点:

  • 依赖DB,当DB异常时整个系统不可用,属于致命问题。配置主从复制可以尽可能的增加可用性,但是数据一致性在特殊情况下难以保证。主从切换时的不一致可能会导致重复发号。
  • ID发号性能瓶颈限制在单台MySQL的读写性能

三、中间件

当使用数据库来生成ID性能不够要求的时候,我们可以尝试使用Redis来生成ID。这主要依赖于Redis是单线程的,所以也可以用于生成全局唯一的ID。可以用Redis的原子操作 INCR和INCRBY来实现

可以使用Redis集群来获取更高的吞吐量。假如一个集群中有5台Redis A,B,C,D,E。可以初始化每台Redis的值分别是1,2,3,4,5,然后步长都是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集群也可以防止单点故障的问题。使用Redis来生成每天从0开始
的流水号比较适合。比如订单号=日期+当日自增长号。可以每天在Redis中
生成一个Key,使用INCR进行累加。

优点:

  • 不依赖于数据库,灵活方便,且性能优于数据库。
  • 数字ID天然排序,对分页或者需要排序的结果很有帮助。

缺点:

  • 如果系统中没有Redis,还需要引入新的组件,增加系统复杂度。
  • 即使已经有了Redis组件,但生成ID的高频率访问对单线程的Redis性能势必也会造成影响。
  • 网络传输造成性能下降。

还可以利用像Zookeeper中的znode数据版本来生成序列号,及MongoDB的ObjectId等,这种利用中间件的做法不是很推荐。

四、开源算法Snowflake

Snowflake是Twitter开源的分布式ID生成算法,结果是一个long型的ID。

Snowflake算法描述:指定机器 & 同一时刻 & 某一并发序列,是唯一的,据此可生成一个64 bits的唯一ID(long)。
组成:

  • 第一部分:1bit 符号位,由于都是生成 ID 都是正数,所以第一位统一为0;
  • 第二部分:41 bit 时间戳,单位是毫秒,41 位可以表示的数字多达2^41 - 1,换算成年就是 69 年;
  • 第三部分:5 bit 机房 ID,代表最多支持 32 个机房;
  • 第四部分:5 bit 机器 ID,代表每个机房最多支持 32 台机器;
  • 第五部分:12 bit,记录同一时间(毫秒)内产生的不同 id,也就是说同一毫秒内可以产生4096个不同 id(有序)

SnowFlake 生成的 ID 整体上按照时间自增排序,并且整个分布式系统不会产生 ID 碰撞(由 DataCenterID 和 WorkerID 区分),并且效率较高。内部测试每秒生成 ID 的数量,基本上每秒可以生成 900000~1100000左右。

优点:

  • 毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。
  • 不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。
  • 可以根据自身业务特性分配bit位,非常灵活。

缺点:

  • 强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。

这种方案性能好,在单机上是递增的,但是由于涉及到分布式环境,每台机器上的时钟不可能完全同步,也许有时候也会出现不是全局递增的情况。而且这个该项目在2010就停止维护了,但这个设计思路还是应用于其他各个ID生成器及变种。

使用Snowflake算法:

(1)创建工具类IdWorker.java

package com.dabing.utils;import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.net.NetworkInterface;/*** <p>名称:IdWorker.java</p>* <p>描述:分布式自增长ID</p>* <pre>*     Twitter的 Snowflake JAVA实现方案* </pre>* 核心代码为其IdWorker这个类实现,其原理结构如下,我分别用一个0表示一位,用—分割开部分的作用:* 1||0---0000000000 0000000000 0000000000 0000000000 0 --- 00000 ---00000 ---000000000000* 在上面的字符串中,第一位为未使用(实际上也可作为long的符号位),接下来的41位为毫秒级时间,* 然后5位datacenter标识位,5位机器ID(并不算标识符,实际是为线程标识),* 然后12位该毫秒内的当前毫秒内的计数,加起来刚好64位,为一个Long型。* 这样的好处是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由datacenter和机器ID作区分),* 并且效率较高,经测试,snowflake每秒能够产生26万ID左右,完全满足需要。* <p>* 64位ID (42(毫秒)+5(机器ID)+5(业务编码)+12(重复累加))**/
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*            机房ID*/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;}/*** 阻塞直到下一毫秒* @param lastTimestamp* @return*/private long tilNextMillis(final long lastTimestamp) {long timestamp = this.timeGen();while (timestamp <= lastTimestamp) {timestamp = this.timeGen();}return timestamp;}/*** 生成当前毫秒时间戳* @return*/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;}public static void main(String[] args) {/*IdWorker idWorker=new IdWorker(0,0);for(int i=0;i<10000;i++){long nextId = idWorker.nextId();System.out.println(nextId);}*/}
}

(2)编写测试代码

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

配置分布式ID生成器:
(1)IdWorker.java拷贝到项目中
(2)项目的application.yml添加配置

workerId: 1
datacenterId: 1

(3)修改项目企叮咚类,增加代码

@Value("${workerId}")
private Integer workerId;@Value("${datacenterId}")
private Integer datacenterId;@Bean
public IdWorker idWorker(){return new IdWorker(workerId,datacenterId);
}

(4)在任意Controller类中注入IdWorker,即可直接使用

解决方案:分布式ID生成相关推荐

  1. 分布式ID生成解决方案——推特雪花算法

    对于某些应用,MySQL里的某个表可能会占用很大的存储空间,甚至让服务器硬盘满了,这时候就会涉及到数据库的分片,把一个数据库进行拆分,通过数据库中间件(MyCat)进行连接. 假设现在我们有三台服务器 ...

  2. 分布式ID生成的9种方法,特好用!

    前言 业务量小于500W或数据容量小于2G的时候单独一个mysql即可提供服务,再大点的时候就进行读写分离也可以应付过来.但当主从同步也扛不住的是就需要分表分库了,但分库分表后需要有一个唯一ID来标识 ...

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

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

  4. Leaf-美团分布式ID生成服务

    Leaf : 美团分布式ID生成服务 There are no two identical leaves in the world.(世界上没有两片相同的树叶.) - 莱布尼茨 现有分布式ID生成方案 ...

  5. 分布式理论分布式ID生成大全

    分布式 ID 介绍 一.何为 ID? 日常开发中,我们需要对系统中的各种数据使用 ID 唯一表示,比如用户 ID 对应且仅对应一个人,商品 ID 对应且仅对应一件商品,订单 ID 对应且仅对应一个订单 ...

  6. 常见分布式ID生成方案

    文章目录 一.为什么要用分布式ID 1.什么是分布式ID 2.那么分布式ID需要满足哪些条件 二. 分布式ID有哪些生成方式 1.基于UUID 2.基于数据库自增ID 3.基于数据库集群模式 4.基于 ...

  7. 雪花算法snowflake分布式id生成原理详解,以及对解决时钟回拨问题几种方案讨论

    文章目录 一.前言 二.雪花算法snowflake 1.基本定义 2.snowflake的优缺点 三.Java代码实现snowflake 1.组装生成id 2.计算最大值的几种方式 3.反解析ID 4 ...

  8. 分布式ID生成方案(二):SnowFlake雪花算法

    雪花算法(SnowFlake) 是twitter公司内部分布式项目采用的ID生成算法,开源后广受国内大厂的好评,在该算法影响下各大公司相继开发出各具特色的分布式生成器. SnowFlake算法生成id ...

  9. 【283期】面试官问:高并发场景下,如何保证全局唯一分布式 ID 生成?

    点击上方"Java精选",选择"设为星标" 别问别人为什么,多问自己凭什么! 下方有惊喜,留言必回,有问必答! 每一天进步一点点,是成功的开始... 前言 系统 ...

最新文章

  1. ***PHP中empty()和isset()的区别
  2. Hibernate中两种获取Session的方式
  3. quasar_Quasar和Akka –比较
  4. 解决 C/C++ 程序执行后控制台一闪而过的方法
  5. 这或许是实现重试最优雅的姿势了!
  6. 4步美化报表教程,这样汇报工作直接拿满分
  7. 单一职责在.NET中
  8. 查询ecshop网站代码排查方法_提升网站访问速度,提升网站访问速度,提升网站访问速度的个人经验分享...
  9. 《Linux命令行大全》:1-6:重定向和管道(很精彩)
  10. 为Autodesk Viewer添加自定义工具条
  11. ibatis 核心原理解析
  12. 不可忽视的IT运维管理
  13. p5.js 入门教程
  14. 智能优化算法:基于Powell优化的鸽群优化算法
  15. 萤石云 android,Android-再次解读萤石云视频
  16. idea安装教程(不是jdr包哦)
  17. vb rs.RecordCount返回0的原因
  18. hr标签---中心线:设置颜色
  19. ceac计算机考试试题,计算机ceac考试复习题12套.doc
  20. java 和 c md5加密_Java与C++实现同样的MD5加密算法

热门文章

  1. vue2 枚举类型转换
  2. Intellij IDE 必知配置
  3. 由IDC机房测试谈主动工作教学实战案例!
  4. [C++]Qt 如何处理密集型耗时的事情(频繁调用QApplication::processEvents)
  5. 【译】Redis喜提新数据结构:Redis Streams
  6. 存储过程与触发器的应用
  7. UML应用:业务内涵的分析抽象amp;表达
  8. webstorm 10.0.4 注册码
  9. xpath的一些测试
  10. IT永远也不可能做到整体外包,这句话是我说的。。。