snowflake做为一个轻量级的分布式id生成算法,已经被广泛使用,大致原理如下:

中间10位工作机器id(即:workerId),从图上可以知道,最多2^10次方,即1024台机器

最右侧12位序列号,2^12次方,即:4096

理论上,如果部署1024台机器,1ms内最多可生成1024*4096 = 4194304(约400万) 个id ,大多数应用场景中已经足够了。

根据这个思路,有很多语言版本的实现,下面是java版本:

public class SnowFlake {

    /**
* 起始的时间戳
*/
private final static long START_STMP = 1480166465631L; /**
* 每一部分占用的位数
*/
private final static long SEQUENCE_BIT = 12; //序列号占用的位数
private final static long MACHINE_BIT = 10; //机器标识占用的位数 /**
* 每一部分的最大值
*/
private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT); /**
* 每一部分向左的位移
*/
private final static long MACHINE_LEFT = SEQUENCE_BIT;
private final static long TIMESTMP_LEFT = SEQUENCE_BIT + MACHINE_BIT; private long machineId; //机器标识
private long sequence = 0L; //序列号
private long lastStmp = -1L;//上一次时间戳 public SnowFlake(long machineId) {
if (machineId > MAX_MACHINE_NUM || machineId < 0) {
throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");
}
this.machineId = machineId;
} /**
* 产生下一个ID
*
* @return
*/
public synchronized long nextId() {
long currStmp = getNewstmp();
if (currStmp < lastStmp) {
throw new RuntimeException("Clock moved backwards. Refusing to generate id");
} if (currStmp == lastStmp) {
//相同毫秒内,序列号自增
sequence = (sequence + 1) & MAX_SEQUENCE;
//同一毫秒的序列数已经达到最大
if (sequence == 0L) {
currStmp = getNextMill();
}
} else {
//不同毫秒内,序列号置为0
sequence = 0L;
} lastStmp = currStmp; return (currStmp - START_STMP) << TIMESTMP_LEFT //时间戳部分
| machineId << MACHINE_LEFT //机器标识部分
| sequence; //序列号部分
} private long getNextMill() {
long mill = getNewstmp();
while (mill <= lastStmp) {
mill = getNewstmp();
}
return mill;
} private long getNewstmp() {
return System.currentTimeMillis();
} public static void main(String[] args) {
SnowFlake snowFlake = new SnowFlake(0); for (int i = 0; i < (1 << 12); i++) {
System.out.println(snowFlake.nextId());
} }
}

结合前面提到的原理可知,集群部署环境下每台机器的应用启动时,初始化SnowFlake应该指定集群内唯一的workerId,否则如果每个机器上的workerId都一样,就有可能生成重复的id(即:相当于集群中,只有一个workerId,这样同1ms内,最多也就生成4096个id,这在高并发业务系统中,是很容易达到的)。

很多朋友都知道,机器上的ip可以转换成int数据,很容易想到,由于每台机器的ip不同(至少同1集群中不会重复),将ip转换出来的数字,对worker上限总数取模(注:worker总数只要小于1024即可,比如假设集群部署的机器,不会超过512台,就可以指定worker总数为 512),用这个取模的结果做为workerId似乎是一个不错的选择(事实上有的项目就是这么干的),上线后,大概率也能平稳运行。

但是!现在很多项目都是跑在云上(或k8s集群中),分布式环境中容器出现问题被重启是不可避免的,而且机器重启后通常ip也会变化。可能有一天会突然发现,snowflake生成的id出现了重复,但是代码并没有做过任何变更!

隐患就在于上面提到的ip取模算法,先给出ip转换成int的方法(网上copy来的):

public class IpUtils {

    // 将127.0.0.1形式的IP地址转换成十进制整数,这里没有进行任何错误处理
public static long ipToLong(String strIp) {
long[] ip = new long[4];
// 先找到IP地址字符串中.的位置
int position1 = strIp.indexOf(".");
int position2 = strIp.indexOf(".", position1 + 1);
int position3 = strIp.indexOf(".", position2 + 1);
// 将每个.之间的字符串转换成整型
ip[0] = Long.parseLong(strIp.substring(0, position1));
ip[1] = Long.parseLong(strIp.substring(position1 + 1, position2));
ip[2] = Long.parseLong(strIp.substring(position2 + 1, position3));
ip[3] = Long.parseLong(strIp.substring(position3 + 1));
return (ip[0] << 24) + (ip[1] << 16) + (ip[2] << 8) + ip[3];
} // 将十进制整数形式转换成127.0.0.1形式的ip地址
public static String longToIP(long longIp) {
StringBuffer sb = new StringBuffer("");
// 直接右移24位
sb.append(String.valueOf((longIp >>> 24)));
sb.append(".");
// 将高8位置0,然后右移16位
sb.append(String.valueOf((longIp & 0x00FFFFFF) >>> 16));
sb.append(".");
// 将高16位置0,然后右移8位
sb.append(String.valueOf((longIp & 0x0000FFFF) >>> 8));
sb.append(".");
// 将高24位置0
sb.append(String.valueOf((longIp & 0x000000FF)));
return sb.toString();
}
}

如果worker总数最大为512,看看下面2个ip,按前面的思路,取模后的结果如何:

long p1 = IpUtils.ipToLong("10.47.130.37");
long p2 = IpUtils.ipToLong("10.96.184.37"); int workerCount = 512; System.out.println(p1 % workerCount);
System.out.println(p2 % workerCount);

将得到2个37,也就是这2台机器生成相同的workerId,所以它俩在并发高的情况下,有就较大概率生成相同的id,而且这个bug还挺难查的,可能机器一重启,又正常了(因为ip变了),如果只是偶尔出现,还会让人误以为是“时钟回拨”问题。

那么,合理的做法应该如何设置workerId呢?可以借助redis,对集群内的机器在应用启动时做一个workerId的全局登记,流程图如下:

注1:因为容器随时可能被销毁,如果机器没了,登记表里的记录就没用了,相当于成了脏数据,所以检查过程中,有一步清理过期记录就是用来干这个的(判断是否过期记录,可借助“登记时间戳”来判断,比如3个月前登记的认为是无效的)

注2:意外情况下,比如启动时正好redis发生故障连不上,可以考虑降级为随机生成1个workerId先用着(视业务场景酌情而定)

最后,顺便提一句,如果考虑到时钟回拨问题,可以使用一些大厂的改进版本,比如百度的uid-generator ,或美团的leaf

snowflake算法的workerId问题的更多相关文章

  1. snowflake算法(java版)

     转自:http://www.cnblogs.com/haoxinyue/p/5208136.html 1. 数据库自增长序列或字段 最常见的方式.利用数据库,全数据库唯一. 优点: 1)简单,代码方 ...

  2. twitter的snowflake算法(C#版本)

    转自:http://blog.csdn.net/kinwyb/article/details/50238505 使用twitter的snowflake算法生成唯一ID. 在分布式系统中,需要生成全局U ...

  3. twitter的ID生成器的snowFlake算法的自造版

    snowFlake算法在生成ID时特别高效,可参考:https://segmentfault.com/a/1190000011282426 SnowFlake算法生成id的结果是一个64bit大小的整 ...

  4. 分布式唯一id:snowflake算法思考

    匠心零度 转载请注明原创出处,谢谢! 缘起 为什么会突然谈到分布式唯一id呢?原因是最近在准备使用RocketMQ,看看官网介绍: 一句话,消息可能会重复,所以消费端需要做幂等.为什么消息会重复后续R ...

  5. 基于Twitter的Snowflake算法实现分布式高效有序ID生产黑科技(无懈可击)

    参考美团文档:https://tech.meituan.com/2017/04/21/mt-leaf.html Twitter-Snowflake算法产生的背景相当简单,为了满足Twitter每秒上万 ...

  6. id生成器,分布式ID自增算法(Snowflake 算法)

    接口: /** * id生成器 */ public interface IdGenerator { String next(); } 实现类: /** * 分布式ID自增算法<br/> * ...

  7. 关于snowflake算法生成的ID转换为JS的数字类型由于过大导致JS精度丢失的问题

    JS的数字类型目前支持的最大值为:9007199254740992,一旦数字超过这个值,JS将会丢失精度,导致前后端的值出现不一致. JAVA的Long类型的       最大值为:922337203 ...

  8. 根据twitter的snowflake算法生成唯一ID

    C#版本 /// <summary> /// 根据twitter的snowflake算法生成唯一ID /// snowflake算法 64 位 /// 0---0000000000 000 ...

  9. C# 根据twitter的snowflake算法生成唯一ID

    C# 版算法: using System; using System.Collections.Generic; using System.Linq; using System.Text; using ...

  10. snowflake算法

    snowflake算法思考 缘起 为什么会突然谈到分布式唯一id呢?原因是最近在准备使用RocketMQ,看看官网介绍: 一句话,消息可能会重复,所以消费端需要做幂等.为什么消息会重复后续Rocket ...

随机推荐

  1. bat文件简短

    bat文件 @echo off F: cd\pictures\projectStreet\FloatingShinyKnot-main node server.js cd\ bat静默运行(但会闪一下 ...

  2. LSposed hook(学习分享)

    Xposed模块编写 参考: https://www.52pojie.cn/thread-1740944-1-1.html https://www.52pojie.cn/thread-1748081- ...

  3. RandomAccessFile、FileInputStream、MappedByteBuffer、FileChannel 区别及应用场景

    RandomAccessFile.FileInputStream.MappedByteBuffer.FileChannel 比较 这些类都是Java中用于文件I/O操作的类,但各有特点和适用场景.下面 ...

  4. (Pytorch第二天)CommandNotFoundError: Your shell has not been properly configured to use 'conda activate'.

    我在cmd里执行conda active,是没有问题的,但是在powershell里会报标题的错 按照各种提示以及查的资料,都说执行conda init 或者conda init cmd.exe等诸如 ...

  5. C#之MethodImpl(MethodImplOptions.Synchronized)

    [MethodImpl(MethodImplOptions.Synchronized)] 是 C# 中用于指定方法同步的一个特性,它控制方法的执行方式,确保在多线程环境下某个方法的执行是线程安全的.它 ...

  6. 9 easybr指纹浏览器https代理认证教程

    目的 在高匿名浏览环境中,代理是关键组件之一.相比普通 HTTP 代理,HTTPS 代理(HTTP over TLS) 支持加密传输,在保障隐私.防止中间人攻击方面更具优势. Chromium 浏览器 ...

  7. Spring注解之@Value基于Apollo或者YAML文件为静态变量赋值

    摘要:Spring Boot微服务中,把在Apollo配置中心或者YAML文件里配置的属性赋值给静态变量. 综述   Apollo(阿波罗)是携程框架部门研发的分布式配置中心,能够集中化管理应用不同环 ...

  8. 鲲鹏Arm+麒麟V10,国产化信创 K8s 离线部署保姆级教程

    Rainbond V6 国产化部署教程,针对鲲鹏 CPU + 麒麟 V10 的离线环境,手把手教你从环境准备到应用上线,所有依赖包提前打包好,步骤写成傻瓜式操作指南.别说技术团队了,照着文档一步步来, ...

  9. logback.xml配置分享

    前言 个人习惯,仅供学习参考 功能 日志异步输出 日志每天自动归档一次 日志文件达到一定大小时,也会进行归档 归档时,进行zip压缩 归档的日志放在 yyyy-MM-dd 文件夹中 当归档的日志文件总 ...

  10. 一种通用的arduino通信例程

    本教程将给出一种通用的,适用于arduino(uno和mega等)单片机的一个通信的教程,通过对例程的修改,可以做成任意的符合需求的代码,下面请看. // 函数定义 String detectStri ...