1. 背景
在关系数据库中,当单个库的负载、连接数、并发数等达到数据库的最大上限时,就得考虑做数据库和表的拆分。如一个简单的电商数据库,在业务初期,为了快速验证业务模式,把用户、商品、订单都放到一个数据库中,随着业务的发展及用户量的增长,单数据库逐渐不能支撑业务(MySQL中单记录容量超过1K时,单表数据量建议不超过一千万条),这时就得考虑把数据库和表做出拆分。
2. 垂直拆分
所谓垂直拆分,就是将数据库及表由一个拆分为多个,即垂直拆分为用户数据库、商品数据库和订单数据库(垂直拆库),订单表可以垂直拆分为订单基本信息表,订单收货地址表、订单商品表(垂直拆表)等,每一个表里保存了一个订单的一部分数据
![](https://img.haomeiwen.com/i13194828/600df8ed2d036c81.png)
3. 水平拆分
所谓垂直拆分,就是说在垂直拆库/表后,单库/表容量还是太大,那就将一个库、一个表水平扩展为多个库,多个表,每一个拆分后的表中保存的依然是一个订单的完整信息。如电商数据库,我们按水平拆分数据库和表后,每一个拆分后的数据库表与现有未拆分前的都保持一致。
![](https://img.haomeiwen.com/i13194828/54cde423c50dde2c.png)
4. 拆分键
随着业务的发展及用户量的增长,分库分表是必然的选择,但是分库分表后,容易遇到的问题,就是拆分键的设计。
一般情况下,拆分键的选取遵循以什么维度进行查询就选取该维度为拆分键。如:订单表就以订单号作为拆分键,商品表就以商品编号作为拆分键。拆分键选取后,对于一些非拆分键的单条件查询,我们需要怎么支持呢?
4.1 等值法
对于非拆分键的单条件查询,对这一个单条件的赋值,可以将其值与拆分键保持一致。比如在电商场景中,用户下订单后,需要通过物流给用户把商品送到用户手上。对于用户来说仅能看到订单信息,订单上展示的物流信息用户也是通过订单号查询而来;但对于物流系统来说,其系统里的业务主键(拆分键)是运单号,此时,运单号如果和订单号相同,即可完美解决这一问题。订单表和运单表的基本数据模型如下:
订单表
![](https://img.haomeiwen.com/i13194828/f36e87bc0d5f2a1c.png)
运单表
![](https://img.haomeiwen.com/i13194828/cda4a19c68b4bd26.png)
在订单表中,拆分键order_id与运单表中的拆分键waybill_code值相同,当按订单号查询运单表里的运单信息时,可以直接查询拆分键waybill_code获取订单对应的运单信息。
小公司可以直接这样设计,因为订单和运单都是同一个部门处理,但是稍微大点的公司就不太可能用这种方案,因为需要跨部门协作,不同部门不同业务,很难使用同一个单号来当订单号和运单号,通常是会有个流水号作为全链路唯一。
4.2 索引法
对于常用的非拆分键,我们可以将其与拆分键之间建立一个索引关系,当按该条件进行查询时,先查询对应的拆分键,再通过拆分键查询对应的数据信息。
![](https://img.haomeiwen.com/i13194828/8d2b3a8075d1e004.png)
当查询用户(user001)的下单记录时,通过用户编码先查询索引表,查询出user001的所有下单的订单号(10001),再通过订单号查询订单表获取用户的订单信息;同理,根据运单号(Y00232)查询订单信息时,在索引表里先查询到对应的订单号,再根据订单号查询对应的订单信息。
缺点:
- 需要先从索引表反查一次;
- 这张索引表会很大,有时候索引表也需要分表,
4.3 基因法
基因法是对上面等值法的优化,等值法要求不同表的拆分键值保持一致,但是实际上不同业务表的拆分键值是有其业务意义的。
但是我们可以让不同拆分键存在相同规则的部分,且这部分用来做拆分键,从而定位库表。
4.1 理论基础
如果我们对一个10进制的数字按10取模,取模的结果与这串数的前面所有位都没有任何关系,最后1位决定取模结果:
![](https://img.haomeiwen.com/i13194828/9ca9fa3df659aef7.png)
同理,按100(102)取模,最后2位决定取模结果,按1000(103)取模,最后3位决定取模结果:
![](https://img.haomeiwen.com/i13194828/d0bb462154d0d0f6.png)
同理:一个二进制的值,按2^n取模,也是最后n位决定取模结果:
![](https://img.haomeiwen.com/i13194828/62f1755facf84499.png)
那么我们在生成订单号的时候,只要把order_no二进制的最后(n+1)位的二进制数设置为user_id的最后(n)位,那么我们对user_id/order_no取余都能得到相同的结果了。(原理:比n位高的值,都是b数的倍数,取余时直接归零,所以取余就是取二进制最后n位)
4.2 应用
假如我们现在现有16个库,128个表,定位的流程是hash(user_id)% 16 定位库的位置, hash(user_id)% 128 定位表的位置;
所以在我们生成订单号时,先对user_id提取一个基因, 也就是二进制最后 7位,然后这个最后7位二进制也作为order_no的二进制最后7位,这样就能保证order_no的路由结果与user_id完全一致;
通过基因法,我们可以通过非拆分键也能快速定位到具体的表。
4.3 代码实现
备注:这个demo有点问题
public static void main(String[] args) {
SnowFlakeIdGenerator snowFlakeIdGenerator = new SnowFlakeIdGenerator();
for (int i = 1; i < 1000; i++) {
//生成userId和提取基因
long userId = snowFlakeIdGenerator.generateId();
String userIdGene = fetchGene(userId, 128);
//利用基因生成orderId
long rawOrderId = snowFlakeIdGenerator.generateId();
Long orderId = generateWithGene(rawOrderId, userIdGene);
System.out.println("userId: " + userId + ",余数:" + userId % 128);
System.out.println("orderId: " + rawOrderId + ",余数:" + orderId % 128);
}
}
/**
* 抽取基因
*
* @param id id
* @param index 16/128 需要取余的数
* @return {@link String}
*/
public static String fetchGene(Long id, Integer index) {
return String.format("%07d", Integer.valueOf(Long.toBinaryString(id % index)));
}
/**
* 根据基因,生成id
*
* @param rawId 原始id
* @param binarySuffix 二进制后缀
* @return {@link Long}
*/
public static Long generateWithGene(Long rawId, String binarySuffix) {
String s = Long.toBinaryString(rawId);
String substring = s.substring(0, s.length() - binarySuffix.length() + 1);
String newBinaryString = substring + binarySuffix;
return Long.parseLong(newBinaryString, 2);
}
分库分表后,我们通常会采用雪花算法来生成分片键,比如订单号,商品编号,
我们在雪花算法的图下加入了订单id生成的示意图, 比如我们有16个库,128张表,则需要截取二进制订单id的最后LOG(128,2)=7位,作为分库/分表基因。
![](https://img.haomeiwen.com/i13194828/b484da78feae513f.png)
然后对订单id用hash生成60位,加上从用户id那边获取的4位基因,形成最终的订单id。其他业务id也使用相同的办法处理。
分库/分表策略时,直接设定使用该id进行水平切分。由于所有业务都有相同的最后4位,这样sharding时都会进入相同的库/表
网友评论