事件起因

6月初我负责的商家数据某功能由于业务原因导致数据量不断上涨,当时使用的MySQL单表已经难以提供高效的查询,因此基于商家维度(商家主键ID)对它进行了分表,问题得以解决。

6月中旬我沉浸在无止尽的需求之中,边做边发出感慨:进大厂又能怎么样呢,还不是CRUD,面试造火箭罢了

此时,架构师笑了笑,于是乎有了本篇文章。

 

我的方式:取模水平分割

亮架构:说一下你上次分表的实现方案吧,Kerwin。

我:我是参照其他系统的方式进行的,利用商家主键Id(商家系统自增的Long型主键)对100取模,由此计算出它应该落在哪一张表,唯一的变动就是这个环节以及数据迁移了。

大概思路如下:

// 业务层代码...
String tableName = "orgcode_datas_" + orgcode % 100;
String insertUserSql = "INSERT INTO " + tableName + " VALUES ('" ... "');";
复制代码

亮架构:所以你采用的就是最简单的方式,而且还把取模逻辑耦合在了业务层吗?

我:是啊,其他系统也都是这么做的,这样搞最快,最安全。

亮架构:你的系统用的是MyBatis对吧,你稍微调研一下就知道,我们可以借助MyBatis的拦截器实现刚刚业务代码中耦合取模的那部分逻辑,虽然本质上没有高明多少,但让它的数据操作底层变得更加清晰明了了,这不就是一种进步吗?

我(略感羞愧):你说的有道理,回去我研究研究。

亮架构:再来看另一个问题,你说了这次分表涉及到数据迁移,你现在分了100个表,万一以后还不够用怎么办?难道要再全部迁移一遍吗?

我(非常羞愧):啊这个,应该不会不够用吧...

 

一致性哈希方案

亮架构:Kerwin,你面试的时候有没有被问到过Redis的一致性哈希问题

我(来劲了):有,Redis中引入了一致性Hash算法。该算法对2^32 取模,将Hash值空间组成虚拟的圆环,整个圆环按顺时针方向组织,每个节点依次为0、1、2...2^32-1,之后将每个服务器进行Hash运算,确定服务器在这个Hash环上的地址,确定了服务器地址后,对数据使用同样的Hash算法,将数据定位到特定的Redis服务器上。如果定位到的地方没有Redis服务器实例,则继续顺时针寻找,找到的第一台服务器即该数据最终的服务器位置。

相关文章:「查缺补漏」巩固你的Redis知识体系(笑)

image-20210704155443807.png

亮架构:Redis的这个知识点你知道,MySQL的你就不知道了?如果我让你把简单取模法更换为一致性哈希方案,你该怎么做?

我(忐忑):如果参考Redis的话,我会这么做:

  1. 从表的命名开始,尽量取名为取模后相对均匀的名称,不以连续后缀结尾了
  2. 我需要找一个碰撞相对较少的哈希算法
  3. 取模的逻辑需要修改为两步,首先是建立数据表落点(即上图中的蓝球)并存储,再计算数据的落点(即红球),然后动态的计算它应该归属到哪一张表即可

亮架构:差不多,命名的部分可以再思考思考,但是你知道你这么做会遇到什么问题吗?提示:和Redis一样的问题。

我(信心满满):这个我知道,可能会出现数据过于集中的问题,这是由于数据和服务器分布不均匀导致的,例如上图中的情况,归属到一号服务器(顺时针第一个蓝球)的概率太大了,这个简单,我们只需要增加虚拟节点,或者从命名上考虑让它尽量平铺,均匀就好啦。

亮架构:孺子可教也,你说说看,这个方式和你刚刚的方式耗费的精力,优劣对比吧。

我(不好意思):从精力上讲,一致性哈希方案多了一步二次计算位置的动作,但是按您说的,如果我们在拦截器中去实现这一步动作的话,整体都是无感知的,只是多了一个方法而已,而从扩展性来讲,那就好多了,无论是增加节点还是减少节点,我们只需要迁移的对应的数据节点即可。

Tips:如下图中,如果我们增加了第四个服务器节点,第四个服务器节点的数据来源只可能是第一个服务器节点

image-20210704162853406.png

 

Range方式分表

亮架构:那我再考考你,假如让你去订单组,对订单快照或者说订单流水进行分表,你怎么做?

我(思考状):订单流水信息有一个明显的不同点在于它的主键Id是一个真正的无意义自增长整型,刚才提到的方式是可行且通用的,但是针对这种特殊场景,我会采用分段分表的方式,比如 1 - 100万为表1,100万01到200万为表2,以此类推,反正这个表只有插入和按Id查询,至于按订单人查询的功能,其他模块就满足了,不用我考虑。

我(瞬间反应过来):不对啊,那订单流水Id基于什么获取呢?我们都要分表了,难道用自增Id?

亮架构:你能想到这一点还挺好的,现在有很多分布式算法都可以解决这种问题,比如Twitter的分布式自增ID算法(Snowflake)、Redis的incr命令、百度的uid-generator算法等等,很好,你也有一点程序员的样子了,知道反问了。

 

把上述知识总结抽象,你能得到什么?

亮架构:你刚才提到了很关键的一点,你说你之所以用最简单的水平分割方式去处理,是因为其他系统的代码也都是这么写的?

我:是的,所以我就把它们复制粘贴,改了一下。

亮架构:OK,你看,无论是简单的水平取模方式,还是一致性哈希,亦或是Range方式,它的本质都是:SQL解析 =》SQL重写 =》 路由 =》 数据库执行 =》 结果返回

不同点在于,几个方案的路由策略不一样而已,你把这些东西抽象成可配置化的解决方案,放到咱们的Lib库中,以后别人再用的时候是不是就没那么麻烦了?

我:是的,既省时间,又省精力,只要咱们都用的是Mybatis和MySQL就行。

亮架构(扶额):你再想想...

我(思考状):对,是我狭隘了,我们刚刚所说的策略根本不用在乎是什么数据库,是什么ORM框架,无论是MongoDB也好,MySQL也罢,它真正的流程就那么几步,我只需要依赖Spring体系,获取到数据库连接信息,然后重写SQL为它分配路由即可。

亮架构(笑了笑,起身走了):孺子可教也,你还觉得每天都是CRUD了吗?明明有一个研究和学习的机会放在你的面前,是你自己选择了最简单的方式,因为你既追求速度,又害怕出问题,所以你用了和前人一样的方式来解决这个问题,工作中很难遇到造火箭的机会,但是造个自行车,哪怕是车轮,也是挺好的吧,认真学习,以后要抓住机会啊。