最近公司正好在做数据库迁移从oracle到mysql,因为之前oracle主键是使用的 SYS_GUID() 这个oracle提供的函数来生成全球唯一的标识符(原始值)由16个字节组成。

不过由于mysql默认使用的InnoDB存储引擎采用的聚簇索引,使用uuid对写性能有一定的影响。而且为了后续分库分表考虑,也不宜采用数据库自增,因此就考虑到需要使用一种可以支持分布式递增且全局唯一的Id生成算法。经过调研,雪花算法是 Twitter 开源的一种生成分布式全局唯一ID的经典算法,且能保证整体上按照时间递增。

在查看了网上大部分关于雪花算法的资料后,关于雪花算法的解读网上多如牛毛,大多抄来抄去。我发现这些教程大多有两点问题:

  1. 只是解读官方算法原理,没有解决 机器ID(5位)和数据中心ID(5位)的配置问题,分布式部署如何保证配置唯一。

  2. 都是Demo需要实例化对象,没有形成开箱即用的工具类,不能直接结合项目使用。

本文旨在完善上面存在的两点问题,希望可以帮助和我一样准备在项目使用 SnowFlake 算法生成数据库主键的小伙伴。

概述


SnowFlake算法生成id的结果是一个64bit大小的整数,它的结构如下图:

由于在Java中64bit的整数是long类型,所以在Java中SnowFlake算法生成的id就是long来存储的。

SnowFlake可以保证:

  • 所有生成的id按时间趋势递增

  • 整个分布式系统内不会产生重复id(因为有datacenterId和workerId来做区分)

Talk is cheap, show you the code


针对文章开头提出的两个问题,笔者的解决方案是,workId使用服务器IP生成,dataCenterId使用hostName生成,这样可以最大限度防止10位机器码重复,但是由于两个ID都不能超过32,只能取余数,还是难免产生重复,但是实际使用中,hostName和IP的配置一般连续或相近,只要不是刚好相隔32位,就不会有问题,况且,hostName和IP同时相隔32的情况更加是几乎不可能的事,平时做的分布式部署,一般也不会超过100台容器。

上面的方法可以零配置使用雪花算法,雪花算法10位机器码的设定理论上可以有1024个节点,生产上使用docker配置一般是一次编译,然后分布式部署到不同容器,不会有不同的配置。这里提供几种可以完全避免产生重复的方案,可以使用redis自增,在应用启动的时候去获取分配机器码。也可以使用zk下发机器码,将机器对应的机器码存储在zk的永久节点下,每次启动获取。不过这两种方案都是需要配置开发的,在生产部署机器少以及并发不太大的情况下,使用本文提供的方案即可。后续如果真有问题,会采用这种依赖中间件下发机器码的方案,到时候在进行补充。

完整代码如下:

import org.apache.commons.lang3.RandomUtils;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import java.net.Inet4Address;

import java.net.UnknownHostException;

public class SnowflakeIdWorker{

private static final Logger LOGGER = LoggerFactory.getLogger(SnowflakeIdWorker.class);

/** 工作机器ID(0~31) */

private long workerId;

/** 数据中心ID(0~31) */

private long dataCenterId;

/** 毫秒内序列(0~4095) */

private long sequence = 0L;

public SnowflakeIdWorker(long workerId, long dataCenterId){

// sanity check for workerId

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));

}

LOGGER.info("worker starting. timestamp left shift = {}, dataCenter id bits = {}, worker id bits = {}, sequence bits = {}, workerid = {}, dataCenterId = {}",

timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId,dataCenterId);

this.workerId = workerId;

this.dataCenterId = dataCenterId;

}

/**初始时间戳*/

private long twepoch = 1577808000000L;

/**长度为5位*/

private long workerIdBits = 5L;

private long datacenterIdBits = 5L;

/** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */

private long maxWorkerId = -1L ^ (-1L << workerIdBits);

/** 支持的最大数据标识id,结果是31 */

private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);

/** 序列在id中占的位数 */

private long sequenceBits = 12L;

/** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */

private long sequenceMask = -1L ^ (-1L << sequenceBits);

//工作id需要左移的位数,12位

private long workerIdShift = sequenceBits;

//数据id需要左移位数 12+5=17位

private long datacenterIdShift = sequenceBits + workerIdBits;

//时间戳需要左移位数 12+5+5=22位

private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

//上次时间戳,初始值为负数

private long lastTimestamp = -1L;

private static SnowflakeIdWorker idWorker;

static {

idWorker = new SnowflakeIdWorker(getWorkId(),getDataCenterId());

}

/**

* 获得下一个ID (该方法是线程安全的)

* @return SnowflakeId

*/

public synchronized long nextId() {

long timestamp = timeGen();

//获取当前时间戳如果小于上次时间戳,则表示时间戳获取出现异常

if (timestamp < lastTimestamp) {

LOGGER.error("clock is moving backwards. Rejecting requests until : {}.", lastTimestamp);

throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds",

lastTimestamp - timestamp));

}

//获取当前时间戳如果等于上次时间戳(同一毫秒内),则在序列号加一;否则序列号赋值为0,从0开始。

if (lastTimestamp == timestamp) {

sequence = (sequence + 1) & sequenceMask;

// 毫秒内序列溢出

if (sequence == 0) {

timestamp = tilNextMillis(lastTimestamp);

}

} else {

sequence = 0;

}

//将上次时间戳值刷新

lastTimestamp = timestamp;

/**

* 返回结果:

* (timestamp - twepoch) << timestampLeftShift) 表示将时间戳减去初始时间戳,再左移相应位数

* (datacenterId << datacenterIdShift) 表示将数据id左移相应位数

* (workerId << workerIdShift) 表示将工作id左移相应位数

* | 是按位或运算符,例如:x | y,只有当x,y都为0的时候结果才为0,其它情况结果都为1。

* 因为各部分只有相应位上的值有意义,其它位上都是0,所以将各部分的值进行 | 运算就能得到最终拼接好的id

*/

return ((timestamp - twepoch) << timestampLeftShift) |

(dataCenterId << datacenterIdShift) |

(workerId << workerIdShift) |

sequence;

}

/**

* 阻塞到下一个毫秒,直到获得新的时间戳

* @param lastTimestamp 上次生成ID的时间截

* @return 当前时间戳

*/

private long tilNextMillis(long lastTimestamp) {

long timestamp = timeGen();

while (timestamp <= lastTimestamp) {

timestamp = timeGen();

}

return timestamp;

}

/**

* 返回以毫秒为单位的当前时间

* @return 当前时间(毫秒)

*/

private long timeGen(){

return System.currentTimeMillis();

}

private static Long getWorkId(){

try {

String hostAddress = Inet4Address.getLocalHost().getHostAddress();

char[] chars = hostAddress.toCharArray();

int sums = 0;

for(int b : chars){

sums += b;

}

return (long)(sums % 32);

} catch (UnknownHostException e) {

// 如果获取失败,则使用随机数备用

return RandomUtils.nextLong(0,31);

}

}

private static Long getDataCenterId(){

try {

char[] chars = Inet4Address.getLocalHost().getHostName().toCharArray();

int sums = 0;

for (int i: chars) {

sums += i;

}

return (long)(sums % 32);

} catch (UnknownHostException e) {

// 如果获取失败,则使用随机数备用

return RandomUtils.nextLong(0,31);

}

}

/**

* 静态工具类

*

* @return

*/

public static Long generateId(){

long id = idWorker.nextId();

return id;

}

/** 测试 */

public static void main(String[] args) {

System.out.println(System.currentTimeMillis());

long startTime = System.nanoTime();

for (int i = 0; i < 50000; i++) {

long id = SnowflakeIdWorker.generateId();

LOGGER.info("id = {}",id);

}

LOGGER.info((System.nanoTime()-startTime)/1000000+"ms");

}

}

扩展


在理解了这个算法之后,其实还有一些扩展的事情可以做:

  1. 理论上41位记录时间戳可以表示69年,而时间戳是从1970年开始算,对于现在来说1970到2019这段时间内的毫秒数已经用不上了,因此可以设置一个初始时间参照点(一般设置为id生成器开始使用的时间),计算时间戳差值(当前时间截 - 开始时间截)。这样就可以扩展时间戳使用的范围。

  2. 解密id,由于id的每段都保存了特定的信息,所以拿到一个id,应该可以尝试反推出原始的每个段的信息。反推出的信息可以帮助我们分析。比如作为订单,可以知道该订单的生成日期,负责处理的数据中心等等。

  3. 完善算法中生成机器id的策略,进一步采用zk分发或redis自增等,实现完全无碰撞的id生成。

  4. 根据自己业务修改每个位段存储的信息。算法是通用的,可以根据自己需求适当调整每段的大小以及存储的信息。


参考资料:

由于算法中大量采用了位运算,如果不太了解的朋友可以参考这篇解析

https://segmentfault.com/a/1190000011282426

推荐阅读

  • 为什么阿里巴巴规定禁止超过三张表join?

  • 必知必会-存储器层次结构

  • 每日一道算法题-leetcode189.旋转数组

点个在看吧,证明你还爱我

java怎样生成32位全是整形的主键_你肯定会需要的分布式Id生成算法雪花算法(Java)...相关推荐

  1. java怎样生成32位全是整形的主键_用java生成32位全球唯一的id编号

    GUID是一个128位长的数字,一般用16进制表示.算法的核心思想是结合机器的网卡.当地时间.一个随即数来生成GUID.从理论上讲,如果一台机器每秒产生10000000个GUID,则可以保证(概率意义 ...

  2. php md5 32 大写,编写生成32位大写和小写字符的md5的函数

    package nicetime.com.practise; import java.security.MessageDigest; /** * MD5加密是JAVA应用中常见的算法,请写出两个MD5 ...

  3. 面试官:你会几种分布式 ID 生成方案???

    1. 为什么需要分布式 ID 对于单体系统来说,主键 ID 常用主键自动的方式进行设置.这种 ID 生成方法在单体项目是可行的,但是对于分布式系统,分库分表之后就不适应了.比如订单表数据量太大了,分成 ...

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

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

  5. 美团开源分布式ID生成系统——Leaf源码阅读笔记(Leaf的号段模式)

    Leaf 最早期需求是各个业务线的订单ID生成需求.在美团早期,有的业务直接通过DB自增的方式生成ID,有的业务通过redis缓存来生成ID,也有的业务直接用UUID这种方式来生成ID.以上的方式各自 ...

  6. Bootstrap4+MySQL前后端综合实训-Day06-PM【MD5加码-生成32位md5码、ResultData.java、分页查询用户数据、添加用户按钮的实现】

    [Bootstrap4前端框架+MySQL数据库]前后端综合实训[10天课程 博客汇总表 详细笔记][附:实训所有代码] 目录 MD5加码 生成32位md5码 ResultData.java 分页查询 ...

  7. 32位大写 md5 php_编写生成32位大写和小写字符的md5的函数

    package nicetime.com.practise; import java.security.MessageDigest; /** * MD5加密是JAVA应用中常见的算法,请写出两个MD5 ...

  8. ms sql 主键自动生成32位guid

    因同步数据业务需要,主键被设定为varchar(32),而自动生成的newid()是36位的,需要将中间的横线去掉,才合适.为此写如下标量函数: CREATE FUNCTION get_32guid ...

  9. 生成32位,16进制的UUID

    <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...

最新文章

  1. 图灵奖得主LeCun:不需要监督的AI才是未来!
  2. SAP MM ME56不能为审批后的PR分配供应源?
  3. java 字符串常用函数_Java学习(5)——字符串常用函数
  4. redshift 数据仓库_您如何使用Amazon Redshift Spectrum访问“暗数据”
  5. 可以编写html的文件吗,我可以使用HTML5/JS编写文件吗?
  6. Hadoop HIVE 安装配置(单机集群)
  7. Tencent笔试题收集
  8. web端(js)极光IM获取消息记录时,如果是图片类型,如何通过media_id获取到图片的真实路径?
  9. List转JSON格式方法
  10. 计划策略(planning strategy)
  11. java 随机抽取数组内容_工具类:随机抽取数组或集合中的几个不重复元素
  12. 正则表达式 -验证身份证号
  13. 离散数学-2 命题逻辑等值演算
  14. 如何才能做好短线交易?这三点你要知道!
  15. 聊一聊微博新知博主这件事,看看赚钱方式有哪些?
  16. 推挽变换器漏感电压尖峰
  17. avr单片机流水灯程序c语言,AVR单片机学习C语言的流水灯验证
  18. 图文并茂的PCA教程
  19. vs.net 不积跬步无以至千里
  20. 《从青铜学到王者》Python数据分析工程师之Numpy计算与文件加载 04

热门文章

  1. 小米新机将搭载鸿蒙,小米新機將搭載鴻蒙係統?還得等鴻蒙進一步的消息!
  2. linux efi不要boot目录,linux – 找不到efi目录:grub-install的问题
  3. Eclipse 打开文件出现乱码情况总结
  4. java 中 Object XML 互转,最终选择Xstream
  5. 计算机网络与应用周林 课后题,阅读下面文章,完成
  6. mysql数据库备份工具expdb,使用expdp完成自动备份数据库案例以及遇到的问题
  7. #地形剖面图_高中地理——每日讲1题(地形剖面图、河流水的补给、河流丁坝)...
  8. 能表示分数的计算机,分数计算器的实现
  9. 功率谱有什么用_马达品牌不同,功率一样,变频器互相不能用,是什么原因
  10. stm32中断优先级_关于STM32 (Cortex-M3) 中NVIC的分析(转)