在业务数据量比较大,数据生命周期比较长,无法用归档的方式分离冷热数据的场景,会经常用到分库分表,使用分库分表涉及到的分片算法,大部分使用取模:

比如一条金融资产数据 financial_asset总是跨多个账期,在整个账单内是不能归档的,其分库分表是按照user_id 或者 cutomer_id 取模来定位具体哪个库哪个表,这个时候就会运用到取模运算:

定位表:financial_asset_${customer_id % table_size}定位库:financial_asset_${customer_id % table_size % db_size}

对这样的取模运算有没有更好的解决方案呢?答案是有,下面我们看看 HashMap 、Disruptor 是如何计算取模进行优化的:

HashMap:

java二进制匹配_ci

Disruptor 的取模算法

java二进制匹配_十进制_02

求余操作优化,都是 用 类似:index & (size - 1) 取代 index % size 的,但是这样计算对size有个要求:

    size 必须是 2 的整数次幂

HashMap 默认initialCapacity

下面分析一下 为什么必须是2 的整数次幂:

对于一个正整数 n ,如果是2的整数次幂,其形式如下:

如果一个数是2的整数次幂(2的k次幂),则其二进制表示中,符号位必定是0 (表示正数),第 k+1 位为 1 ,其余全部为 0,如下所示:

java二进制匹配_十进制_03

则 n - 1 的二进制变为:

0   ……    1 0  0  0…… 0  :末尾k个0,第k+ 1 位为0,前面 都是 0

根据 1&* = * , 0 & * = 0的特性,

对于任意一个正整数 a 其二进制表示为:

0   ……    1 *  * * …… *  :大于 n ,* 为 0 或者 1

0   ……    0 0 1 * …… *  :小于 n ,* 为 0 或者 1

0   ……    1 0 0 0 …… 0  :等于n

由于 a % n 取模,则大于 n 的部分可以舍去掉, 因为 :a % n = ( x * n + z  ) % n  =  z

去掉就相当于 K 位之前全部变为 0 即可,0 & * 可以做到;

现在考虑小于等n的情况,就是相当于取 k 位以及之后所有为的二进制表示的数即可,由于

 n-1 正好将 第k+1位 变为0 ,其 k一直到最右边的0位(非符号位的最低位)全部变为 1 

0   ……    1 0  0  0 …… 0   :n

0   ……    0 1  1  1 …… 1     :n - 1

0   ……    * *  *  * …… *    :a

0   ……    0 *  *  * …… *    :a & (n-1) = a % n

    如:

十进制8, 二进制表示为:0000 1000十进制7, 二进制表示为:0000 0111小于8 :十进制6, 二进制表示为:0000 0110十进制7, 二进制表示为:0000 01116&(8-1)  二进制表示为:0000 0110 => 6 = 6 % 8大于8 : 十进制54,二进制表示为:0011 0110十进制7, 二进制表示为:0000 011154&(8-1) 二进制表示为:0000 0110 => 6 = 54 % 8

对 % 与 & 做循环20万次,做简单的性能测试:

int db_size = 8;int table_size = 128;int a = 0;long start = System.currentTimeMillis();for (int i = 0; i < 200000; i++) {            a = i % db_size % table_size;        }long end = System.currentTimeMillis();        System.out.println("use % ,spend time = " + (end - start));        start = System.currentTimeMillis();for (int i = 0; i < 200000; i++) {            a = (i & (db_size - 1)) & (table_size - 1);        }        end = System.currentTimeMillis();        System.out.println("use & ,spend time = " + (end - start));

运行3次结果如下:

use % ,spend time = 5use & ,spend time = 1use % ,spend time = 12use & ,spend time = 1use % ,spend time = 7use & ,spend time = 1

& 的效率更好:

求余操作本身也是一种高耗费的操作, 所以在Disruptor中 ringbuffer的size设成2的n次方, 可以利用位操作来高效实现求余。要找到数组中当前序号指向的元素,可以通过mod操作,正常通过sequence mod array length = array index,优化后可以通过:sequence & (array length-1) = array index实现。比如一共有8槽,3&(8-1)=3,HashMap也是用这个方式来定位数组元素的,这种方式比取模的速度更快。

给定一个正整数 n ,如何求一个大于n 或者 小于 n 的2的整数次幂的数?下面提供JDK中 Integer 和 HashMap 中的方法,供大家参考:

一、传入一个正整数i,返回小于等于这个数字的一个2的幂次方数:

Integer 类的方法highestOneBit:

/**     传入一个数字,它将返回小于等于这个数字的一个2的幂次方数     * @since 1.5     */public static int highestOneBit(int i) {// HD, Figure 3-1        i |= (i >>  1);        i |= (i >>  2);        i |= (i >>  4);        i |= (i >>  8);        i |= (i >> 16);        return i - (i >>> 1);    }public static void main(String[] args) {        System.out.println(Integer.highestOneBit(9));         //输出 8 = 2 的 3 次方        System.out.println(Integer.highestOneBit(29));        //输出 16= 2 的 4 次方        System.out.println(Integer.highestOneBit(-5));        //输出 Integer.MIN_VALUE 负数其实无解    }

如上文分析所述:

1、如果一个数是2的整数次幂(2的k次幂),则其二进制表示中,符号位必定是0 (表示正数),第 k+1 位为 0 ,其余全部为 0,如下所示:

java二进制匹配_十进制_03

    如:

十进制8, 二进制表示为:0000 1000十进制16,二进制表示为:0001 0000十进制32,二进制表示为:0010 0000

highestOneBit 方法的目的就是 将 一个整数 

0   ……    1 *  *  * …… * (* 可以为0或1 ),变为

0   ……    1 0  0 0 …… 0

如何变,可以按下面的思路展开:

0   ……    1 *  * * …… * ,先将改数字后面的 * 全部变为1 ,得结果a,过程如下:

0   ……    1 1  1  1 …… 1 ,=>a,然后a无符号右移1位,变为 a>>>1 => b 为

0   ……    0 1 1  1 …… 1 ,=>b , a - b 的结果即为解

0   ……    1 0 0 0 …… 0 , => a-b, 于是问题的求解变为:

0   ……    1 *  * * …… * ,对于给定的该数字,如何才能变为 下面的数字

0   ……    1 1  1  1 …… 1 ,步凑如下:

0   ……    1 *  * * …… * ,右移1位的结果:

0   ……    0 1  * * …… * ,  与上面的数求 位或,结果为:

0   ……    1 1  * * …… * ,  这样至少得到2个1在一起,再右移2位,结果为

0   ……    0 0 1 1 …… * ,   与上面的数求 位或,结果为:

0   ……    1  1 1 1 …… * ,   这样至少得到4个1在一起,再右移4位,结果为

0   ……    00001111..* ,   与上面的数求 位或,结果为:

0   ……    1111 1111 .. * ,   这样至少有8个1 在一起,... .... 

.. ... .. 等等 

按这个规律(考虑):

    右移1位,这个整数的二进制表示中,紧贴着最高位为1 的,可以有连续2个1在一起;

右移2位,这个整数的二进制表示中,紧贴着最高位为1 的,可以有连续4个1在一起;

右移4位,这个整数的二进制表示中,紧贴着最高位为1 的,可以有连续8个1在一起;

右移8位,这个整数的二进制表示中,紧贴着最高位为1 的,可以有连续16个1在一起;

右移16位,这个整数的二进制表示中,紧贴着最高位为1 的,可以有连续32个1在一起;

至此结束,因为 int 类型总计只有32位;

代码实现如下:

i |= (i >>  1);//第1步i |= (i >>  2);//第2步i |= (i >>  4);//第3步i |= (i >>  8);//第4步i |= (i >> 16);//第5步return i - (i >>> 1);

注意:以上假设 最高位 1 特别靠近符号位。

       如果 最高位1 距离 符号位 比较远,则早早(不用走到第5步就已经完成)就能获取 0……1**** ,* 表示全部是1,距离符号位较近,比如第31位为1 ,则只有5步全部完毕,才能获取 0……1**** ,* 表示全部是1 的形式。

二、传入一个正整数cap,返回大于等于(并且最接近)这个数字的一个2的幂次方数,最大不能超过(1<<30):

参考HashMap的方法:

/**     * Returns a power of two size for the given target capacity.     */  static final int tableSizeFor(int cap) {        int n = cap - 1;        n |= n >>> 1;        n |= n >>> 2;        n |= n >>> 4;        n |= n >>> 8;        n |= n >>> 16;        return (n < 0) ? 1 :         (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;    }

什么情况下,HashMap会用到该函数?

java二进制匹配_System_05

以 HashMap 初始化时为例,初始化时候 根据 用户端传入的容量初始化构造函数时会,检查客户传入的 initialCapacity ,

java二进制匹配_十进制_06

HashMap的 容量 capacity 必须是 2 的整数次幂,;如果用户传入的 

initialCapacity 不是 2 的整数次幂,HashMap 会自动重新计算,我们debug下:

比如:

HashMap map = new HashMap<>(10);

但是数量不是 10 , 而是 16 

java二进制匹配_java二进制匹配_07

如果直接初始化为16:

HashMap map = new HashMap<>(16);

java二进制匹配_java二进制匹配_08