ShardingSphere分库分表 查询时如何避免全路由?详解【附属表法】与【基因法】

133 阅读4分钟

全路由

在分库分表中,如果 SQL 没有携带 分片键,ShardingSphere 无法知道数据落在哪个库 / 表,就会执行 全路由 ,即对所有分片执行查询,再合并结果。

假设用户表 user 按 user_id 分片,有 2 个库(db0 ~ db1),每个库 4 张表(t0 ~ t3)。

如果我们想要根据手机号(mobile)或者邮箱(email)字段进行查询,由于当前分库分表设user_id为分片键,mobile与email没有设置分片键,因此当查询这两个时会发生全路由:

db0.t0 db0.t1 db0.t2 db0.t3 

db1.t0 db1.t1 db1.t2 db1.t3 

可以看到,系统会对每一个库的每一张表进行查询,然后再聚合结果,这会导致:

  • 性能急剧下降(查询所有库表)
  • 网络开销大
  • 数据库压力大

在分库分表的情况下,数据并发量本身已经很大,我们应该尽量减轻数据库的压力,提高数据库的性能是我们进行业务开发的重中之重。因此在这里我提出两个方法来解决分库分表时的全路由问题。

附属表法

  • 新建一张附属表(也叫映射表、路由表),专门存储非分片键分片键的映射关系。
  • 查询时先查附属表,拿到分片键,再去对应的分片查询主表。
  • 附属表通常不大,可以不分片,或者按非分片键的哈希分片。

以 user 表为例,可以在原user表基础上新建user_mobile和user_email表作为附属表,分别以手机号和邮箱号字段为主键,其中对应着userID。附属表根据主表也新建对应的分表,分别以手机号和邮箱号为分片键。

这样,假设根据手机号查询的时候,会查询以手机号为主键的附属表,又因为附属表以手机号为分片键,因此可以避免全路由。拿到userID后再回到主表进行查询。

优点

  • 避免全路由
  • 实现简单,适合非分片键查询频率不高的场景
  • 可扩展性强,再需要新的查询字段时只需要新建一个表就可以了

缺点

  • 需要维护附属表的一致性
  • 附属表可能成为热点(如果不分片)
  • 多一次查询开销
  • 如果要查询的条件过多,或者主表数据量过大,容易导致分表占用的空间过大,空间损耗

基因法

  • 非分片键中 “嵌入” 分片键信息(基因),这样即使查询条件不包含分片键,也能解析出分片信息。
  • 常见做法:手机号、邮箱等在生成时就与 user_id 建立关联,比如取 user_id 的哈希值作为部分内容。

假设分片规则是 user_id % 分表数量n

  • 注册时生成 user_id(如 123456)
  • 计算分片值:123456 % n
  • 生成手机号时,将分片值嵌入到手机号的某几位(log2 n):
  • 将原手机号的后log2 n位替换为user_id得到的分片值的后log2 n 位,然后再对这两个数对n取模,会发现得到的结果是一样的。

因此利用基因法,查询手机号时:

  • 从手机号中解析出分片值(基因)
  • 构造路由,只查询对应分片
  • 由于手机号和userID对Log2 n取模的结果是一样的,因此可以认为手机号带了userID的基因,当对手机号取模求值的时候,得出的值就可以认为是对应的userID。
  • 所以手机号查询可以转为UserID查询,避免了全路由的情况

优点

  • 无需附属表,一次查询即可定位分片
  • 性能高

缺点

  • 需要提前设计非分片键的生成规则
  • 对历史数据不友好
  • 嵌入规则需保证不会冲突
  • 在雪花算法中,可能会产生id重复的情况

针对雪花算法改进

​编辑

假设存在一种情况,在超高的并发下,在同一毫秒,同一台机器,生成两个id,那么这两个id唯一的区别 就是序列号相差1,如果这时我们使用了基因法,分成32张表,也就是取把雪花算法二进制的后5位进行基因替换,两个id就会发生重复。

解决方法

根据分片表的数据计算出log2n的对数,将序列号左移相应的对数,然后剩下的位数替换为userId % 分片表数量 的基因。

缺点

替换后的雪花算法,在一毫秒内,最多能支持2^(12 - log2 n)的数量的id不会重复。

因此,分片数量越多,基因法改良的雪花算法一毫秒所能生成的不重复的订单越少。