本文源码:GitHub·点这里 || GitEE·点这里

一、全局ID简介

在实际的开发中,几乎所有的业务场景产生的数据,都需要一个唯一ID作为核心标识,用来流程化管理。比如常见的:

  • 订单:order-id,查订单详情,物流状态等;
  • 支付:pay-id,支付状态,基于ID事务管理;

如何生成唯一标识,在普通场景下,一般的方法就可以解决,例如:

  1. import java.util.UUID;
  2. public class UuidUtil {
  3. public static String getUUid() {
  4. UUID uuid = UUID.randomUUID();
  5. return String.valueOf(uuid).replace("-","");
  6. }
  7. }

这个方法可以解决绝大部分唯一ID需求的场景业务,但是网上各种UUID重复场景的描述帖,说的好像该API不好用。

絮叨一句:说一个真实使用的业务场景,大概是半年近3000万的数据流水,用的就是UUID的API,暂时未捕捉到ID重复的问题,仅供参考。

二、雪花算法

1、概念简介

Twitter公司开源的分布式ID生成算法策略,生成的ID遵循时间的顺序。

  • 1为位标识,始终为0,不可用;
  • 41位时间截,存储时间截的差值(当前时间截-开始时间截);
  • 10位的机器标识,10位的长度最多支持部署1024个节点;
  • 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒产生4096个ID序号;

SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高。

2、编码实现

工具类中很多可以自定义的,比如起始时间,机器ID配置等。

  1. /**
  2. * 雪花算法ID生成
  3. */
  4. public class SnowIdWorkerUtil {
  5. // 开始时间截 (2020-01-02)
  6. private final long timeToCut = 1577894400000L;
  7. // 机器ID所占的位数
  8. private final long workerIdBits = 2L;
  9. // 数据标识ID所占的位数
  10. private final long dataCenterIdBits = 8L;
  11. // 支持的最大机器ID,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)
  12. private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
  13. // 支持的最大数据标识ID,结果是31
  14. private final long maxDataCenterId = -1L ^ (-1L << dataCenterIdBits);
  15. // 序列在ID中占的位数
  16. private final long sequenceBits = 12L;
  17. // 机器ID向左移12位
  18. private final long workerIdShift = sequenceBits;
  19. // 数据标识ID向左移17位(12+5)
  20. private final long dataCenterIdShift = sequenceBits + workerIdBits;
  21. // 时间截向左移22位(5+5+12)
  22. private final long timestampLeftShift = sequenceBits + workerIdBits + dataCenterIdBits;
  23. // 生成序列的掩码
  24. private final long sequenceMask = -1L ^ (-1L << sequenceBits);
  25. // 工作机器ID(0~31)
  26. private long workerId;
  27. // 数据中心ID(0~31)
  28. private long dataCenterId;
  29. // 毫秒内序列(0~4095)
  30. private long sequence = 0L;
  31. // 上次生成ID的时间截
  32. private long lastTimestamp = -1L;
  33. /**
  34. * 构造函数
  35. * @param workerId 工作ID (0~31)
  36. * @param dataCenterId 数据中心ID (0~31)
  37. */
  38. public SnowIdWorkerUtil (long workerId, long dataCenterId) {
  39. if (workerId > maxWorkerId || workerId < 0) {
  40. throw new IllegalArgumentException("workerId 不符合条件");
  41. }
  42. if (dataCenterId > maxDataCenterId || dataCenterId < 0) {
  43. throw new IllegalArgumentException("dataCenterId 不符合条件");
  44. }
  45. this.workerId = workerId;
  46. this.dataCenterId = dataCenterId;
  47. }
  48. public synchronized String nextIdVar(){
  49. return String.valueOf(nextId());
  50. }
  51. /**
  52. * 线程安全,获得下一个ID
  53. */
  54. private synchronized long nextId() {
  55. long timestamp = timeGen();
  56. // 如果当前时间小于上一次ID生成的时间戳,抛出异常
  57. if (timestamp < lastTimestamp) {
  58. throw new RuntimeException(String.format(
  59. "时间顺序异常,时间差(上次时间-现在)=%d",
  60. lastTimestamp - timestamp));
  61. }
  62. // 如果是同一时间生成的,则进行毫秒内序列
  63. if (lastTimestamp == timestamp) {
  64. sequence = (sequence + 1) & sequenceMask;
  65. //毫秒内序列溢出
  66. if (sequence == 0) {
  67. //阻塞到下一个毫秒,获得新的时间戳
  68. timestamp = tilNextMillis(lastTimestamp);
  69. }
  70. } else {
  71. // 时间戳改变,毫秒内序列重置
  72. sequence = 0L;
  73. }
  74. // 上次生成ID的时间截
  75. lastTimestamp = timestamp;
  76. // 移位并通过或运算拼到一起组成64位的ID
  77. return ((timestamp - timeToCut) << timestampLeftShift)
  78. | (dataCenterId << dataCenterIdShift)
  79. | (workerId << workerIdShift) | sequence;
  80. }
  81. /**
  82. * 阻塞,获得新的时间戳
  83. */
  84. private long tilNextMillis(long lastTimestamp) {
  85. long timestamp = timeGen();
  86. while (timestamp <= lastTimestamp) {
  87. timestamp = timeGen();
  88. }
  89. return timestamp;
  90. }
  91. /**
  92. * 返回当前时间节点
  93. */
  94. private long timeGen() {
  95. return System.currentTimeMillis();
  96. }
  97. public static void main(String[] args) {
  98. // 参数在实际业务下需要配置管理
  99. SnowIdWorkerUtil idWorker = new SnowIdWorkerUtil(1, 1);
  100. for (int i = 0; i < 100; i++) {
  101. String id = idWorker.nextIdVar();
  102. System.out.println(id+" "+id.length()+"位");
  103. }
  104. }
  105. }

三、自定义实现

还有一种常见的实现思路,基于数据库的自增主键ID,不过基于这个原理,却有各种不同的实现策略。

简单表结构:

  1. CREATE TABLE `du_temp_id` (
  2. `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
  3. `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  4. PRIMARY KEY (`id`)
  5. ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='主键ID临时表';

1、基于主键

这种模式的原理比较单调,向临时表写入一条记录,借助MySQL生成的唯一主键ID,然后拿出来稍微处理一下,作为各种业务场景的唯一ID使用。

  1. @Service
  2. public class TempIdServiceImpl implements TempIdService {
  3. @Resource
  4. private TempIdMapper tempIdMapper ;
  5. @Override
  6. public List<String> getIdList() {
  7. List<String> idList = new ArrayList<>() ;
  8. TempIdEntity tempIdEntity = new TempIdEntity ();
  9. tempIdEntity.setCreateTime(new Date());
  10. for (int i = 0 ; i < 10 ; i++){
  11. tempIdMapper.insert(tempIdEntity);
  12. idList.add(UuidUtil.getNoId(8,Long.parseLong(tempIdEntity.getId().toString()))) ;
  13. }
  14. return idList ;
  15. }
  16. }

问题点:如果作为ID生成的临时表所在的MySQL服务宕掉,那可能会影响整个业务流程,造成雪崩效应。

2、高可用集群

单服务如果不能安稳的支撑业务需求,很自然集群模式就该上场了。提供多台MySQL服务[A,B,C],处理策略也不止一种:

  • 库设置主键自增策略

例如A库[1,4,7],B库[2,5,8],C库[3,6,9],基于不同自增规则,生成统一的自增唯一标识。

  • 生成ID做分库标识

这种先把ID生成,然后不同的数据库生成的ID给一个不同的标识,例如UIDA,UIDB,UIDC。

  1. @Service
  2. public class TempIdServiceImpl implements TempIdService {
  3. @Resource
  4. private TempIdMapper tempIdMapper ;
  5. @Override
  6. public List<String> getRouteIdList() {
  7. List<String> idList = new ArrayList<>() ;
  8. TempIdEntity tempIdEntity = new TempIdEntity ();
  9. tempIdEntity.setCreateTime(new Date());
  10. for (int i = 0 ; i < 2 ; i++){
  11. tempIdMapper.insertA(tempIdEntity);
  12. idList.add(UuidUtil.getRouteId("UID-A",10,
  13. Long.parseLong(tempIdEntity.getId().toString()))) ;
  14. tempIdMapper.insertB(tempIdEntity);
  15. idList.add(UuidUtil.getRouteId("UID-B",10,
  16. Long.parseLong(tempIdEntity.getId().toString()))) ;
  17. tempIdMapper.insertC(tempIdEntity);
  18. idList.add(UuidUtil.getRouteId("UID-C",10,
  19. Long.parseLong(tempIdEntity.getId().toString()))) ;
  20. }
  21. return idList ;
  22. }
  23. }

结果样例:

UID-A00001,UID-B00001,UID-C00001

UID-A00002,UID-B00002,UID-C00002

3、ID样式优化

从数据获取的ID基本是一个自增的整数序列,可以提供一个格式美化工具方法。

  1. public class UuidUtil {
  2. private static final String ZERO = "00000000000";
  3. private static final String PREFIX = "UID";
  4. public static String getNoId(int length,Long id){
  5. String idVar = String.valueOf(id) ;
  6. if (idVar.length()>length){
  7. return PREFIX+idVar ;
  8. } else {
  9. int gapLen = length-idVar.length()-PREFIX.length() ;
  10. return PREFIX+ZERO.substring(0,gapLen)+idVar ;
  11. }
  12. }
  13. public static String getRouteId(String route,Integer length,Long id){
  14. String idVar = String.valueOf(id) ;
  15. if (idVar.length()>length){
  16. return route+idVar ;
  17. } else {
  18. int gapLen = length-idVar.length()-route.length() ;
  19. return route+ZERO.substring(0,gapLen)+idVar ;
  20. }
  21. }
  22. }

基于不同的策略,把ID格式为统一的位数。

4、性能问题

如果在高并发的业务场景下,实时基于MySQL去生成唯一ID容易产生性能瓶颈,当然其他方法也可能产生这个问题。可以在系统空闲时间批量生成一批,放入缓存中,在使用的时候直接从缓存层取出即可。

四、源代码地址

  1. GitHub·地址
  2. https://github.com/cicadasmile/data-manage-parent
  3. GitEE·地址
  4. https://gitee.com/cicadasmile/data-manage-parent

推荐阅读:数据和架构管理

序号 标题
A01 数据源管理:主从库动态路由,AOP模式读写分离
A02 数据源管理:基于JDBC模式,适配和管理动态数据源
A03 数据源管理:动态权限校验,表结构和数据迁移流程
A04 数据源管理:关系型分库分表,列式库分布式计算
C01 架构基础:单服务.集群.分布式,基本区别和联系

架构设计 | 分布式业务系统中,全局ID生成策略的更多相关文章

  1. 分布式高并发下全局ID生成策略

    数据在分片时,典型的是分库分表,就有一个全局ID生成的问题.单纯的生成全局ID并不是什么难题,但是生成的ID通常要满足分片的一些要求:   1 不能有单点故障.   2 以时间为序,或者ID里包含时间 ...

  2. 高并发环境下全局id生成策略

    解决方案: 基于Redis的全局id生成策略:(推荐此方法) 基于雪花算法的全局id生成: https://www.cnblogs.com/kobe-qi/p/8761690.html 基于zooke ...

  3. 分布式系统下的全局id生成策略分析

    对于分布式系统而言,意味着会有很多个instance会并发的生成很多业务数据,比如订单.不同的机房.不同的机器.不同的应用实例会同时生成.所以,如何生成一个好用的全局id并不是一个简单的uuid就能够 ...

  4. 架构设计 | 分布式系统调度,Zookeeper集群化管理

    本文源码:GitHub·点这里 || GitEE·点这里 一.框架简介 1.基础简介 Zookeeper基于观察者模式设计的组件,主要应用于分布式系统架构中的,统一命名服务.统一配置管理.统一集群管理 ...

  5. 聊聊业务系统中投递消息到mq的几种方式

    背景 电商中有这样的一个场景: 下单成功之后送积分的操作,我们使用mq来实现 下单成功之后,投递一条消息到mq,积分系统消费消息,给用户增加积分 我们主要讨论一下,下单及投递消息到mq的操作,如何实现 ...

  6. 数据库分库分表(一)常见分布式主键ID生成策略

    主键生成策略 系统唯一ID是我们在设计一个系统的时候常常会遇见的问题,下面介绍一些常见的ID生成策略. Sequence ID UUID GUID COMB Snowflake 最开始的自增ID为了实 ...

  7. 可实现的全局唯一有序ID生成策略

    在博客园搜素全局唯一有序ID,罗列出来的文章大致讲述了以下几个问题,常见的生成全局唯一id的常见方法 :使用数据库自动增长序列实现 : 使用UUID实现:  使用redis实现: 使用Twitter的 ...

  8. 图解Janusgraph系列-分布式id生成策略分析

    JanusGraph - 分布式id的生成策略 大家好,我是洋仔,JanusGraph图解系列文章,实时更新~ 本次更新时间:2020-9-1 文章为作者跟踪源码和查看官方文档整理,如有任何问题,请联 ...

  9. 全局ID生成--雪花算法

    分布式ID常见生成策略: 分布式ID生成策略常见的有如下几种: 数据库自增ID. UUID生成. Redis的原子自增方式. 数据库水平拆分,设置初始值和相同的自增步长. 批量申请自增ID. 雪花算法 ...

随机推荐

  1. [noip模拟]难缠的值周生<宽搜>

    难缠的值周生 [问题描述] 小 P 上学总是迟到,迟到了以后常常会被值周生发现.被值周生发现就会给他所在的班级扣分,被扣了分不免要挨班主任的训,这令小 P 很不爽.不过,聪明的他经过观察发现,值周生通 ...

  2. 深入解读ES6系列(二)

    ES6函数 哈喽小伙伴们,爱说'废'话的Z又回来了,欢迎来到Super IT曾的博客时间,上一节说了es6的历史,变量,以及闭包,这一节我们继续我们知识的海洋,一起奋斗不秃头!不足的欢迎提问留言. 今 ...

  3. python爬虫之requests的基础使用

    1.先安装requests库,打开cmd,输入:pip install requests

  4. linux下zip/unzip详解

    linux下zip_unzip详解 命令列表:zip    -q (quiet)    -r (recursive)    -0(level0-level9)    -e (encrypt)    - ...

  5. Hadoop (六):MapReduce基本使用

    MapReduce原理 背景 因为如果要对海量数据进行计算,计算机的内存可能会不够. 因此可以把海量数据切割成小块多次计算. 而分布式系统可以把小块分给多态机器并行计算. MapReduce概述 Ma ...

  6. 微信小程序template富文本插件image宽度被js强制设置

    这段时间一直做微信小程序,过程中遇到了一个问题,这个问题一直没有得到完美的解决. 问题描述: 在Web编程中经常会引入template插件,这个插件是封装好,我们通常的做法是直接引入,配置简单,好用, ...

  7. 路由与交换,cisco路由器配置,浮动静态路由

    设置浮动静态路由的目的就是为了防止因为一条线路故障而引起网络故障.言外之意就是说浮动静态路由实际上是主干路由的备份.例如下图: 假如我们设路由器之间的串口(seria)为浮动静态路由(管理距离为100 ...

  8. C语言 文件操作(一)

    #include<stdio.h> int main(){          FILE *fp = fopen("f:\\lanyue.txt","r&quo ...

  9. asap异步执行实现原理

    目录 为什么分析asap asap概述 asap源码解析-Node版 参考 1.为什么分析asap 在之前的文章 async和await是如何实现异步编程? 中的浅谈Promise如何实现异步执行小节 ...

  10. 【网络编程01】socket的基础知识-简单网络通信程序

    1.什么是socket socket(套接字),简单来说是IP地址与端口(port)的组合,可以与远程主机的应用程序进行通信.通过IP地址可以确定一台主机,而通过端口则可以确定某一个应用程序.IP+端 ...