4. sharding-jdbc源码之分布式ID

阿飞Javaer,转载请注明原创出处,谢谢!

实现动机

传统数据库软件开发中,主键自动生成技术是基本需求。而各大数据库对于该需求也提供了相应的支持,比如MySQL的自增键。 对于MySQL而言,分库分表之后,不同表生成全局唯一的Id是非常棘手的问题。因为同一个逻辑表内的不同实际表之间的自增键是无法互相感知的, 这样会造成重复Id的生成。我们当然可以通过约束表生成键的规则来达到数据的不重复,但是这需要引入额外的运维力量来解决重复性问题,并使框架缺乏扩展性。

目前有许多第三方解决方案可以完美解决这个问题,比如UUID等依靠特定算法自生成不重复键(由于InnoDB采用的B+Tree索引特性,UUID生成的主键插入性能较差),或者通过引入Id生成服务等。 但也正因为这种多样性导致了Sharding-JDBC如果强依赖于任何一种方案就会限制其自身的发展。

基于以上的原因,最终采用了以JDBC接口来实现对于生成Id的访问,而将底层具体的Id生成实现分离出来。

摘自sharding-jdbc分布式主键

sharding-jdbc的分布式ID采用twitter开源的snowflake算法,不需要依赖任何第三方组件,这样其扩展性和维护性得到最大的简化;但是snowflake算法的缺陷(强依赖时间,如果时钟回拨,就会生成重复的ID),sharding-jdbc没有给出解决方案,如果用户想要强化,需要自行扩展;

扩展:美团的分布式ID生成系统也是基于snowflake算法,并且解决了时钟回拨的问题,读取有兴趣请阅读Leaf——美团点评分布式ID生成系统

分布式ID简介

github上对分布式ID这个特性的描述是:Distributed Unique Time-Sequence Generation,两个重要特性是:分布式唯一时间序;基于Twitter Snowflake算法实现,长度为64bit;64bit组成如下:

  • 1bit sign bit.
  • 41bits timestamp offset from 2016.11.01(Sharding-JDBC distributed primary key published data) to now.
  • 10bits worker process id.
  • 12bits auto increment offset in one mills.

分布式ID源码分析

核心源码在sharding-jdbc-core模块中的com.dangdang.ddframe.rdb.sharding.keygen.DefaultKeyGenerator.java中:

public final class DefaultKeyGenerator implements KeyGenerator {
    
    public static final long EPOCH;
    
    // 自增长序列的长度(单位是位时的长度)
    private static final long SEQUENCE_BITS = 12L;
    
    // workerId的长度(单位是位时的长度)
    private static final long WORKER_ID_BITS = 10L;
    
    private static final long SEQUENCE_MASK = (1 << SEQUENCE_BITS) - 1;
    
    private static final long WORKER_ID_LEFT_SHIFT_BITS = SEQUENCE_BITS;
    
    private static final long TIMESTAMP_LEFT_SHIFT_BITS = WORKER_ID_LEFT_SHIFT_BITS + WORKER_ID_BITS;
    
    // 位运算计算workerId的最大值(workerId占10位,那么1向左移10位就是workerId的最大值)
    private static final long WORKER_ID_MAX_VALUE = 1L << WORKER_ID_BITS;
    
    @Setter
    private static TimeService timeService = new TimeService();
    
    private static long workerId;
    
    // EPOCH就是起始时间,从2016-11-01 00:00:00开始的毫秒数
    static {
        Calendar calendar = Calendar.getInstance();
        calendar.set(2016, Calendar.NOVEMBER, 1);
        calendar.set(Calendar.HOUR_OF_DAY, 0);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.MILLISECOND, 0);
        EPOCH = calendar.getTimeInMillis();
    }
    
    private long sequence;
    
    private long lastTime;
    
    /**
     * 得到分布式唯一ID需要先设置workerId,workId的值范围[0, 1024)
     * @param workerId work process id
     */
    public static void setWorkerId(final long workerId) {
        // google-guava提供的入参检查方法:workerId只能在0~WORKER_ID_MAX_VALUE之间;
        Preconditions.checkArgument(workerId >= 0L && workerId < WORKER_ID_MAX_VALUE);
        DefaultKeyGenerator.workerId = workerId;
    }
    
    /**
     * 调用该方法,得到分布式唯一ID
     * @return key type is @{@link Long}.
     */
    @Override
    public synchronized Number generateKey() {
        long currentMillis = timeService.getCurrentMillis();
        // 每次取分布式唯一ID的时间不能少于上一次取时的时间
        Preconditions.checkState(lastTime <= currentMillis, "Clock is moving backwards, last time is %d milliseconds, current time is %d milliseconds", lastTime, currentMillis);
        // 如果同一毫秒范围内,那么自增,否则从0开始
        if (lastTime == currentMillis) {
            // 如果自增后的sequence值超过4096,那么等待直到下一个毫秒
            if (0L == (sequence = ++sequence & SEQUENCE_MASK)) {
                currentMillis = waitUntilNextTime(currentMillis);
            }
        } else {
            sequence = 0;
        }
        // 更新lastTime的值,即最后一次获取分布式唯一ID的时间
        lastTime = currentMillis;
        // 从这里可知分布式唯一ID的组成部分;
        return ((currentMillis - EPOCH) << TIMESTAMP_LEFT_SHIFT_BITS) | (workerId << WORKER_ID_LEFT_SHIFT_BITS) | sequence;
    }
    
    // 获取下一毫秒的方法:死循环获取当前毫秒与lastTime比较,直到大于lastTime的值;
    private long waitUntilNextTime(final long lastTime) {
        long time = timeService.getCurrentMillis();
        while (time <= lastTime) {
            time = timeService.getCurrentMillis();
        }
        return time;
    }
}

获取workerId的三种方式

sharding-jdbc的sharding-jdbc-plugin模块中,提供了三种方式获取workerId的方式,并提供接口获取分布式唯一ID的方法--generateKey(),接下来对各种方式如何生成workerId进行分析;

HostNameKeyGenerator

  1. 根据hostname获取,源码如下(HostNameKeyGenerator.java):
/**
 * 根据机器名最后的数字编号获取工作进程Id.如果线上机器命名有统一规范,建议使用此种方式.
 * 例如机器的HostName为:dangdang-db-sharding-dev-01(公司名-部门名-服务名-环境名-编号)
 * ,会截取HostName最后的编号01作为workerId.
 *
 * @author DonneyYoung
 **/
 static void initWorkerId() {
    InetAddress address;
    Long workerId;
    try {
        address = InetAddress.getLocalHost();
    } catch (final UnknownHostException e) {
        throw new IllegalStateException("Cannot get LocalHost InetAddress, please check your network!");
    }
    // 先得到服务器的hostname,例如JTCRTVDRA44,linux上可通过命令"cat /proc/sys/kernel/hostname"查看;
    String hostName = address.getHostName();
    try {
        // 计算workerId的方式:
        // 第一步hostName.replaceAll("\\d+$", ""),即去掉hostname后纯数字部分,例如JTCRTVDRA44去掉后就是JTCRTVDRA
        // 第二步hostName.replace(第一步的结果, ""),即将原hostname的非数字部分去掉,得到纯数字部分,就是workerId
        workerId = Long.valueOf(hostName.replace(hostName.replaceAll("\\d+$", ""), ""));
    } catch (final NumberFormatException e) {
        throw new IllegalArgumentException(String.format("Wrong hostname:%s, hostname must be end with number!", hostName));
    }
    DefaultKeyGenerator.setWorkerId(workerId);
}

IPKeyGenerator

  1. 根据IP获取,源码如下(IPKeyGenerator.java):
/**
 * 根据机器IP获取工作进程Id,如果线上机器的IP二进制表示的最后10位不重复,建议使用此种方式
 * ,列如机器的IP为192.168.1.108,二进制表示:11000000 10101000 00000001 01101100
 * ,截取最后10位 01 01101100,转为十进制364,设置workerId为364.
 */
static void initWorkerId() {
    InetAddress address;
    try {
        // 首先得到IP地址,例如192.168.1.108
        address = InetAddress.getLocalHost();
    } catch (final UnknownHostException e) {
        throw new IllegalStateException("Cannot get LocalHost InetAddress, please check your network!");
    }
    // IP地址byte[]数组形式,这个byte数组的长度是4,数组0~3下标对应的值分别是192,168,1,108
    byte[] ipAddressByteArray = address.getAddress();
    // 由这里计算workerId源码可知,workId由两部分组成:
    // 第一部分(ipAddressByteArray[ipAddressByteArray.length - 2] & 0B11) << Byte.SIZE:ipAddressByteArray[ipAddressByteArray.length - 2]即取byte[]倒数第二个值,即1,然后&0B11,即只取最后2位(IP段倒数第二个段取2位,IP段最后一位取全部8位,总计10位),然后左移Byte.SIZE,即左移8位(因为这一部分取得的是IP段中倒数第二个段的值);
    // 第二部分(ipAddressByteArray[ipAddressByteArray.length - 1] & 0xFF):ipAddressByteArray[ipAddressByteArray.length - 1]即取byte[]最后一位,即108,然后&0xFF,即通过位运算将byte转为int;
    // 最后将第一部分得到的值加上第二部分得到的值就是最终的workId
    DefaultKeyGenerator.setWorkerId((long) (((ipAddressByteArray[ipAddressByteArray.length - 2] & 0B11) << Byte.SIZE) + (ipAddressByteArray[ipAddressByteArray.length - 1] & 0xFF)));
}

IPSectionKeyGenerator

  1. 根据IP段获取,源码如下(IPSectionKeyGenerator.java):
/**
 * 浏览 {@link IPKeyGenerator} workerId生成的规则后,感觉对服务器IP后10位(特别是IPV6)数值比较约束.
 * 
 * <p>
 * 有以下优化思路:
 * 因为workerId最大限制是2^10,我们生成的workerId只要满足小于最大workerId即可。
 * 1.针对IPV4:
 * ....IP最大 255.255.255.255。而(255+255+255+255) < 1024。
 * ....因此采用IP段数值相加即可生成唯一的workerId,不受IP位限制。
 * 2.针对IPV6:
 * ....IP最大ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
 * ....为了保证相加生成出的workerId < 1024,思路是将每个bit位的后6位相加。这样在一定程度上也可以满足workerId不重复的问题。
 * </p>
 * 使用这种IP生成workerId的方法,必须保证IP段相加不能重复
 *
 * @author DogFc
 */
static void initWorkerId() {
    InetAddress address;
    try {
        address = InetAddress.getLocalHost();
    } catch (final UnknownHostException e) {
        throw new IllegalStateException("Cannot get LocalHost InetAddress, please check your network!");
    }
    // 得到IP地址的byte[]形式值
    byte[] ipAddressByteArray = address.getAddress();
    long workerId = 0L;
    //如果是IPV4,计算方式是遍历byte[],然后把每个IP段数值相加得到的结果就是workerId
    if (ipAddressByteArray.length == 4) {
        for (byte byteNum : ipAddressByteArray) {
            workerId += byteNum & 0xFF;
        }
        //如果是IPV6,计算方式是遍历byte[],然后把每个IP段后6位(& 0B111111 就是得到后6位)数值相加得到的结果就是workerId
    } else if (ipAddressByteArray.length == 16) {
        for (byte byteNum : ipAddressByteArray) {
            workerId += byteNum & 0B111111;
        }
    } else {
        throw new IllegalStateException("Bad LocalHost InetAddress, please check your network!");
    }
    DefaultKeyGenerator.setWorkerId(workerId);
}

建议

大道至简,强烈推荐HostNameKeyGenerator方式获取workerId,只需服务器按照标准统一配置好hostname即可;这种方案有点类似spring-boot:约定至上;并能够让架构最简化,不依赖任何第三方组件;

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容

  • 目录;(一) 拆分实施策略和示例演示(二) 全局主键生成策略(三) 关于使用框架还是自主开发以及sharding实...
    linking12阅读 10,431评论 1 52
  • Sharding的基本思想其实就是采用分治的思想,要把一个数据库切分成多个部分放到不同的数据库(server)上,...
    jiangmo阅读 9,384评论 0 7
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,598评论 18 399
  • 今日朗诵道德经第57章“以正治国”心得: 当很多问题的出现时,不是因为客观事物,而是因为我们自己,所以出现问题的时...
    周志英阅读 116评论 2 1
  • 文/清风徐来 季风里的故乡 春夏秋冬 有不同的雨歌唱 轻重缓急都成乐章 听雨呢喃滴答 在茅檐...
    清风徐徐来也阅读 602评论 0 0