0级事故因使用斐波那契散列,做数据库路由算法
作者:小傅哥
博客:https://bugstack.cn
源码:https://github.com/fuzhengwei/java-algorithms
沉淀、分享、成长,让自己和他人都能有所收获!
2022年,大促备战进行中。为了保障系统在流量洪峰下可以稳定运行,其中一条是需要对分库分表的数据库进行扩容,以满足更大体量的业务承载能力。
但扩容计划并不顺利,因为扩容后的数据并没有如预期散列到不同的库表,而是集中到了数据库的少部分表。扩容失败。经检查,因为这个数据库路由算法并不是公司统一使用的算法,而是研发自己基于斐波那契散列实现的。
那为什么使用斐波那契散列会出现这样的问题,是斐波那契算法问题?接下来小傅哥就带着大家一起从数学的角度分析下。 一、关于斐波那契
斐波那契的历史
斐波那契数列出现在印度数学中,与梵文韵律有关。在梵语诗歌传统中,人们对列举所有持续时间为 2 单位的长 (L) 音节与 1 单位持续时间的短 (S) 音节并列的模式很感兴趣。用给定的总持续时间计算连续 L 和 S 的不同模式会产生斐波那契数:持续时间m单位的模式数量是F(m + 1)。
斐波那契数列可以由递归关系定义
F0 = 0,F1 = 1,Fn = Fn-1 + Fn-2
F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12 F13 F14 F15 F16 F17 F18 F19 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181从 F2 开始任意一位都是前两位之和。 从 F2 开始任意一位与前一位相比的比值,都无限趋近于 (√ 5 - 1)/2 = 0.618 因此基于黄金分割的计算应用,也被称为斐波那契应用。
那这个就是斐波那契的基本定义和特性,并且基于这样的特性在计算机科学中,斐波那契常用于;伪随机数生成、AVL二叉树、最大公约数、合并排序算法等。
而大部分程序员包括小傅哥最开始意识到斐波那契的应用则来自于,Java 源码 ThreadLocal 中 HASH_INCREMENT = 0x61c88647 这样一个常量的定义。因为这用作数据散列的特殊值 0x61c88647 就是基于黄金分割点计算得来的,公式: (1L << 32) - (long) ((1L << 32) * (Math.sqrt(5) - 1))/2 。
那么既然 ThreadLocal 是基于斐波那契散列计算的下标索引,那为啥数据库路由算法不能使用同样的方式计算散列索引呢?因为通过验证可以得知,斐波那契散列并不满足 严格的雪崩标准(SAC) 。接下来小傅哥就带着大家一起来使用数据验证下。 二、斐波那契计算
斐波那契数列可以通过循环、递归以及封闭式表达式(比奈公式) 的方式进行计算。读者可在单元测试中验证:https://github.com/fuzhengwei/java-algorithms 1. 循环计算public double fibonacci(int n) { double currentVal = 1; double previousVal = 0; if (n == 1) return 1; int iterationsCounter = n - 1; while (iterationsCounter > 0) { currentVal += previousVal; previousVal = currentVal - previousVal; iterationsCounter -= 1; } return currentVal; } 2. 递归计算public int fibonacciRecursion(int n) { if (n == 1 || n == 2) { return 1; } else { return (fibonacciRecursion(n - 1) + fibonacciRecursion(n - 2)); } } 3. 比奈公式public double fibonacciClosedForm(long position) { int maxPosition = 75; if (position < 1 || position > maxPosition) { throw new RuntimeException("Can"t handle position smaller than 1 or greater than 75"); } double sqrt = Math.sqrt(5); double phi = (1 + sqrt) / 2; return Math.floor((Math.pow(phi, position)) / sqrt + 0.5); }
封闭式表达式 :与由具有常数系数的线性递归定义的每个序列一样,斐波那契数具有封闭形式的表达式。它被称为比奈公式,以法国数学家雅克·菲利普·玛丽·比内命名,尽管亚伯拉罕·德·莫弗和丹尼尔·伯努利已经知道它。三、散列函数分类
散列函数(英语:Hash function)又称散列算法、哈希函数,是一种将任意大小的数据映射到固定大小值的计算方式。散列函数计算结果被称为散列值、散列码,也就是对应的 HashMap 中哈希桶的索引以及数据库中库表的路由信息。
例如在 Java 中对数据的散列算法:HashMap 用到的是一次扰动函数下的哈希散列、ThreadLocal 用到的斐波那契散列。而通常数据库路由组件用到的是整数模除法散列,这也是实践中最简单和最常用的方法之一。
接下来就给大家介绍这几种常用的散列算法,其他更多散列可以参考 HashFunction
1. 除法散列
在用来设计散列函数的除法散列法中,通过取 K 除以 M 的余数,将关键字 K 映射到 M 个槽中的某一个位置上,即散列函数为:h(K) = K mod M 表格大小通常是 2 的幂。
另外除法散列的一个显着缺点是除法在大多数现代架构(包括 x86)上都是微编程的,并且可能比乘法慢 10 倍。 2. 乘法散列
乘法散列法整体包含两步: 用关键字k乘上常数 A(0,并去除kA的小数部分 用m乘以这个值,再取结果的底 floor 公式: h(K)=Math.floor[m(aK mod 1)]
步骤 : 假设某计算机的字长为 ww 位,而 kk 正好可容于一个字中 (k<2wk<2w) 现在选取范围 [0,2w] 内的任意数值 ss,k×sk×s 即可用R1·2w+R0R1·2w+R0 来表示因此 (k·A)mod1=k·s/2w(k·A)mod1=k·s/2w 就是将k×sk×s 整体向右平移 ww 位,此时R0R0即为小数部分再乘以 2m2m 相当于左移 mm 位,散列值 h(k)h(k) 为 R0R0 的前 m 位。
乘法散列只需要单个整数乘法和右移,使其成为计算速度最快的哈希函数之一。但乘法散列可能会在变更计算因子后,较高值的输入位不会影响较低值的输出位,问题体现在元素分散不均,不满足严格的雪崩标准。所以通常在会进行异或操作
乘法散列容易受到导致扩散不良的"常见错误"的影响——较高值的输入位不会影响较低值的输出位。在乘法步骤对此进行校正之前,输入上的变换将保留的最高位的跨度向下移动,并将它们异或或加到键上。所以在输入上的变换将保留的最高位的跨度向下移动,并将它们异或操作或者加到键上。例如 HashMap 的扰动函数。 3. 斐波那契散列
其实斐波那契散列是一种特殊形式的乘法散列,只不过它的乘法因子选择的是一个黄金分割比例值,所以叫做斐波那契散列。
斐波那契散列的特性在于将"大数映射到小数"的计算结果在表空间上是均匀分布的,且计算满足乘法散列效率高。 那为什么并不能使用它作为数据库路由算法呢? 四、雪崩标准测试
在数据库路由实现方面,通常我们都是使用整数模除法散列求模的方式进行元素的索引计算。那既然乘法散列效率高,斐波那契散列分散均匀,为什么不使用这样的方式处理数据库路由算法呢?
在检索的资料中并没有一个专门的文章来说明这一事项,这也倒置很多在学习过 HashMap、ThreadLocal 源码的研发人员尝试把这两种源码中的乘法散列算法搬到数据库路由算法中使用。在保证每次扩容数据库表都是2的次幂的情况下,并没有出现什么样的问题。那么对于这样情况下,是否隐藏着什么潜在的风险呢?
那么为了证实斐波那契散列是否可以用在数据库路由散列算法中,我们可以尝试使用 严格雪崩标准(SAC) 进行验证测试。
那么什么是 严格雪崩标准( SAC ) ,在密码学中,雪崩效应是密码算法的理想属性,通常是分组密码和密码散列函数,其中如果输入发生轻微变化(例如,翻转单个位),输出会发生显着变化(例如,50%输出位翻转)
SAC 建立在完整性和雪崩的概念之上,由 Webster 和 Tavares 于 1985 年引入。SAC 的高阶概括涉及多个输入位。满足最高阶 SAC 的最大非线性函数,也称为"完全非线性"函数。
简单来说,当我们对数据库从8库32表扩容到16库32表的时候,每一个表中的数据总量都应该以50%的数量进行减少。这样才是合理的。
好,那么接下来我们就来做下雪崩测试; 准备10万个单词用作样本数据。 对比测试除法散列、乘法散列、斐波那契散列。 基于条件1、2,对数据通过不同的散列算法分两次路由到8库32表和16库32表中,验证每个区间内数据的变化数量,是否在50%左右。 准备一个 excel 表,来做数据的统计计算。
测试代码 public Map> hashFunction(int dbCount, int tbCount, Long hashIncrementVal, int hashType) { int size = dbCount * tbCount; System.out.print("库数:" + dbCount + " 表数:" + tbCount + " 总值:" + size + " 幂值:" + Math.log(size) / Math.log(2)); int HASH_INCREMENT = (int) ((null == hashIncrementVal ? size : hashIncrementVal) * (Math.sqrt(5) - 1) / 2); System.out.print(" 黄金分割:" + HASH_INCREMENT + "/" + size + " = " + (double) HASH_INCREMENT / size); Map> map = new ConcurrentHashMap<>(); Set words = FileUtil.readWordList("/Users/fuzhengwei/1024/github/java-algorithms/logic/src/main/java/math/fibonacci/103976个英语单词库.txt"); System.out.println(" 单词总数:" + words.size() + "r "); for (String word : words) { int idx = 0; switch (hashType) { // 散列:斐波那契散列 int idx = (size - 1) & (word.hashCode() * HASH_INCREMENT + HASH_INCREMENT); case 0: idx = (word.hashCode() * HASH_INCREMENT) & (size - 1); break; // 散列:哈希散列 + 扰动函数 case 1: idx = (size - 1) & (word.hashCode() ^ (word.hashCode() >>> 16)); break; // 散列:哈希散列 case 2: idx = (size - 1) & (word.hashCode()/* ^ (word.hashCode() >>> 16)*/); break; // 散列:整数求模 case 3: idx = Math.abs(word.hashCode()) % size; break; } // 计算路由索引 int dbIdx = idx / tbCount + 1; int tbIdx = idx - tbCount * (dbIdx - 1); // 保存路由结果 if (map.containsKey(dbIdx)) { Map dbCountMap = map.get(dbIdx); if (dbCountMap.containsKey(tbIdx)) { dbCountMap.put(tbIdx, dbCountMap.get(tbIdx) + 1); } else { dbCountMap.put(tbIdx, 1); } } else { Map dbCountMap = new HashMap<>(); dbCountMap.put(tbIdx, 1); map.put(dbIdx, dbCountMap); } } return map; } 整个方法的目的在于得出不同的哈希算法,对10万个单词散列到指定的分库分表中,所体现的结果。 1. 斐波那契散列1.1 最小黄金分割
斐波那契散列也是乘法散列的一种体现形式,只不过它选择了一个黄金分割点作为乘积因子。例如 ThreadLocal 中的 0x61c88647 。但如果说我们只是按照一个指定范围长度内做黄金分割计算,并拿这个结果当成乘法散列的因子,那么10万单词将不会均匀的散列到8个库,32张表内。如图:@Test public void test_hashFunction_0_hash_null() { Map> map = fibonacci.hashFunction(8, 32, null, 0); Set keys = map.keySet(); for (Integer key : keys) { Collection values = map.get(key).values(); for (Integer v : values) { System.out.print(v + " "); } System.out.println(); } } 库数:8 表数:32 总值:256 幂值:8.0 黄金分割:2147483647/256 = 8388607.99609375 单词总数:103976
如果你的斐波那契散列值是根据库表的值进行黄金切割的,那么在最初的库表范围较小的阶段,将有部分区域无法使用。这是因为得到的黄金分割点的二进制值没法覆盖整个区域,也就做不到合适的乘法散列计算。参考:https://bugstack.cn/md/algorithm/logic/math/2022-10-30-bits.html - 《程序员数学:位运算》 1.2 最小黄金分割
基于最小黄金分割的计算,是没法做到均匀散列的。所以你看到的 ThreadLocal 默认就给你一个 0x61c88647 而不是随着扩容长度实时计算的切割值。好那么我们接下来也使用这个值来做计算,看看8库到16库后,数据的雪崩结果。@Test public void test_hashFunction_0() { Map> map = fibonacci.hashFunction(8, 32, 1L << 32, 0); Set keys = map.keySet(); for (Integer key : keys) { Collection values = map.get(key).values(); for (Integer v : values) { System.out.print(v + " "); } System.out.println(); } } 分别测试 dbCount = 8、dbCount = 16 库数:8 表数:32 总值:512 幂值:9.0 黄金分割:2147483647/512 = 4194303.998046875 单词总数:103976 库数:16 表数:32 总值:512 幂值:9.0 黄金分割:2147483647/512 = 4194303.998046875 单词总数:103976
从8库扩到16库以后,满足50%数据变化的,只有2库2表和3库20表。其他数据变化都不满足严格的雪崩测试。 1.3 任意扩容库表
通常情况下做分库分表会考虑到以后的扩容操作,那如果说按照2的次幂扩容第一次是8库32表,之后是16库32表,在之后32库32表。那么这样扩容下去,其实是扛不住的。所以大多数时候希望是从8库扩到9库,而不是一下翻倍。那我们来测试下9库32表,斐波那契散列的分散效果。 Map> map = fibonacci.hashFunction(9, 32, 1L << 32, 0); Set keys = map.keySet(); for (Integer key : keys) { Collection values = map.get(key).values(); for (Integer v : values) { System.out.print(v + " "); } System.out.println(); } } 库数:9 表数:32 总值:512 幂值:9.0 黄金分割:2147483647/512 = 4194303.998046875 单词总数:103976
因为9库不满足2的次幂,也就没法直接乘法散列。所以相当于斐波那契散列失效了。这如果是线上的生产环境,将发生灾难性的事故。 2. 整数求模散列2.1 基础散列计算
整数求模以数据库表总数为除数,与哈希值的绝对值进行除法散列计算。一般在数据库路由中非常常用。另外如果根据用户ID做散列路由,但由于ID长度波动范围较大,则可以按照指定长度统一切割后使用。 @Test public void test_hashFunction_3() { Map> map = fibonacci.hashFunction(8, 32, null, 3); Set keys = map.keySet(); for (Integer key : keys) { Collection values = map.get(key).values(); for (Integer v : values) { System.out.print(v + " "); } System.out.println(); } } 分别测试 dbCount = 8、dbCount = 16 库数:8 表数:32 总值:512 幂值:9.0 黄金分割:2147483647/512 = 4194303.998046875 单词总数:103976 库数:16 表数:32 总值:512 幂值:9.0 黄金分割:2147483647/512 = 4194303.998046875 单词总数:103976
在使用除法散列方式后,满足50%数据变化的有5个表。看着并不多,但这相当于是斐波那契散列下的3倍。同时其他表数据接近50%的也要大于斐波那契散列。 2.2 任意扩容计算
接下来我们任意从8库扩容到9库,看看数据的变化。 @Test public void test_hashFunction_3() { Map> map = fibonacci.hashFunction(9, 32, null, 3); Set keys = map.keySet(); for (Integer key : keys) { Collection values = map.get(key).values(); for (Integer v : values) { System.out.print(v + " "); } System.out.println(); } } 库数:9 表数:32 总值:512 幂值:9.0 黄金分割:2147483647/512 = 4194303.998046875 单词总数:103976
103976 / (9 * 32) ≈ 361,那么也就说扩容后的数据,基本在361范围波动,就满足了均匀散列的目的。所以在数据库散列算法中,除法散列是较靠谱且稳定的。 五、常见面试题散列算法有哪些种? HashMap、ThreadLocal、数据库路由都是用了什么散列算法? 乘法散列为什么要用2的幂值作为每次的扩容条件? 你有了解过 0x61c88647 是怎么计算的吗?斐波那契散列的使用场景是什么? The Fibonacci Association:https://en.wikipedia.org/wiki/The_Fibonacci_Association 哈希函数:https://en.wikipedia.org/wiki/Hash_function 斐波那契数:https://en.wikipedia.org/wiki/Fibonacci_number#Mathematics 散列函数:https://zh.wikipedia.org/wiki/%E6%95%A3%E5%88%97%E5%87%BD%E6%95%B8 雪崩效应:https://en.wikipedia.org/wiki/Avalanche_effect Fibonacci Hashing: The Optimization that the World Forgot (or: a Better Alternative to Integer Modulo):https://probablydance.com/2018/06/16/fibonacci-hashing-the-optimization-that-the-world-forgot-or-a-better-alternative-to-integer-modulo/ 斐波那契数:https://en.wikipedia.org/wiki/Fibonacci_number#Relation_to_the_golden_ratio C++ 中具有面向对象设计模式的数据结构和算法:https://book.huihoo.com/data-structures-and-algorithms-with-object-oriented-design-patterns-in-c++/html/page214.html
莫鼎革代表推广生物航煤应用让天更蓝水更绿,让老百姓的餐桌更安全,是我们的努力方向。全国人大代表中国石化镇海炼化公司党委书记莫鼎革建议,加快构建生物航煤全产业链,实现航空运输清洁低碳转型。莫鼎革代表说,推广应
华夏人寿没有了,你的保单还安全吗?近期,有一个重大消息引起了社会的广泛关注华夏人寿即将改名为瑞众人寿。这是一个怎样的故事呢?华夏人寿曾经是中国保险市场上的领军企业,市场份额一度稳居前三。然而,在2020年,华夏人寿
豆粕市场分析及后期展望20230310豆粕阿根廷大幅减产,下方空间有限一美农大幅下调阿根廷大豆产量三月报告中,美国农业部下调阿根廷大豆产量800万吨至3300万吨,下调幅度之大远超市场此前预期的435万吨同时USDA还
华谊兄弟悄悄进军抖音直播带货,股价一度冲击涨停3月10日,华谊兄弟(300027。SZ)冲击涨停。中午休市时,华谊兄弟涨14。18。截至收盘,华谊兄弟涨幅达12。26,报2。93元股。就在涨停前一天,在投资者互动平台向华谊兄弟
特斯拉ModelSX新增烈焰红色车漆售价2。5万元2023年3月10日,特斯拉ModelSModelX全球同步上线全新车漆配色烈焰红(UltraRed),中国区同步这一改动,选装价格2。5万人民币。作为对比,烈焰红较现有的中国红(
笑死!这家自动驾驶公司烧钱90亿,7年却只交了一辆车笔者说过很多次自动驾驶了,几乎都是好消息,不过这次要说点不那么愉快滴。近几年兴起的自动驾驶特别火热,许多传统车企纷纷入局,也有许多初创公司如雨后春笋崛起。然而,顺风时猪能飞起,逆风
数字经济牛市即将拉开大幕,这些AI龙头公司将成为关键推动力!2003年起,成立国资委,央企股改后开始蓬勃发展。2008年,组建住建部,楼市牛市诞生并持续了十年之久。2013年,广电总局的组建,为自媒体热潮开启了大门,并催生了互联网平台的崛起
城投公司能像央企一样改革吗?作者席加省国企改革取得了巨大的成绩,国企改革三年行动已经高质量圆满收官。但总体上看,改革不平衡落实不到位的情况依然存在,有些企业成绩斐然,有一些则进度偏慢,一些老大难的问题还没有完
伊苏10新情报和截图公布可同时操控男女主战斗今日(3月9日)FALCOM公布了伊苏10新情报,介绍了男主角亚特鲁女主角卡嘉以及交叉动作战斗系统。官方还发布男女主角设定图和游戏截图,一起来看看吧!伊苏10玩家可操控亚特鲁和卡嘉
花13。9元收买上万主播,抖音被原神玩明白了作者张知白编辑杨佩汶设计晏谈梦洁自从原神火了之后,我要靠原神买房已经成为了各家游戏主播常说的梗。抖音快手B站,也早已涌入大量想靠着原神赚上一笔的主播,有的咸鱼翻身成为了B站百大UP
日版暗黑4PS实体盘预购开启售价9800日元亚马逊平台暗黑破坏神4日版实体盘的预购现已开启,PS4及PS5版本均为9800日元(约合人民币497元),预购特典包括坐骑光之使者及坐骑铠甲信仰装饰马衣,感兴趣的玩家可以了解一下。