在业务数据量比较大,数据生命周期比较长,无法用归档的方式分离冷热数据的场景,会经常用到分库分表,使用分库分表涉及到的分片算法,大部分使用取模:
比如一条金融资产数据 financial_asset总是跨多个账期,在整个账单内是不能归档的,其分库分表是按照user_id 或者 cutomer_id 取模来定位具体哪个库哪个表,这个时候就会运用到取模运算:
定位表:financial_asset_${customer_id % table_size}定位库:financial_asset_${customer_id % table_size % db_size}
对这样的取模运算有没有更好的解决方案呢?答案是有,下面我们看看 HashMap 、Disruptor 是如何计算取模进行优化的:
HashMap:
Disruptor 的取模算法
求余操作优化,都是 用 类似:index & (size - 1) 取代 index % size 的,但是这样计算对size有个要求:
size 必须是 2 的整数次幂
HashMap 默认initialCapacity
下面分析一下 为什么必须是2 的整数次幂:
对于一个正整数 n ,如果是2的整数次幂,其形式如下:
如果一个数是2的整数次幂(2的k次幂),则其二进制表示中,符号位必定是0 (表示正数),第 k+1 位为 1 ,其余全部为 0,如下所示:
则 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,如下所示:
如:
十进制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会用到该函数?
以 HashMap 初始化时为例,初始化时候 根据 用户端传入的容量初始化构造函数时会,检查客户传入的 initialCapacity ,
HashMap的 容量 capacity 必须是 2 的整数次幂,;如果用户传入的
initialCapacity 不是 2 的整数次幂,HashMap 会自动重新计算,我们debug下:
比如:
HashMap map = new HashMap<>(10);
但是数量不是 10 , 而是 16
如果直接初始化为16:
HashMap map = new HashMap<>(16);