主要记录使用shardingsphere-jdbc 5.2.0进行分库,分表,分库加分表,读写分离,自定义复合分片算法等的操作步骤。
概念介绍 基因法
基因法原理 : 对一个数取余2的n次方,那么余数就是这个数的二进制的最后n位数。
十进制数对10的n次幂取余,余数是10进制数的最后n位, 比如
11 % 10,余数 1
122 % 10,余数 2
122 % 100 , 余数 22
同理,对一个数取余2的n次方,余数就是这个数的二进制的最后n位数,然后可以再转为10进制。
为什么分片数要是2的n次幂
如: 11 % 4 等于 11 & (4-1)
通常分表会和分库一起进行,比如需要分成2个库8个表,分表和分库的数量都是2的n次幂,可以实现均匀分布,8个表均分到2个库中,每个库4个表。
第三个也是最重要的原因,可以减少扩容时迁移的数据量,只需要迁移一半。
比如 原来分2个表,order_0,order_1
userId % 2 = 0 ,对应 order_0
userId % 2 = 1 , 对应 order_1
此时扩容到4个表
userId % 4 = 0 ,对应 order_0
userId % 4 = 1 , 对应 order_1
userId % 4 = 2 ,对应 order_2
userId % 4 = 3 , 对应 order_3
只需要迁移原来在 order_0, order_1 中,且 userId % 4 >=2 数据就行。
userId % 4 = 2的数据, 从 order_0 迁移到 order_2
userId % 4 = 3的数据, 从order_1 迁移到 order_3
具体配置和操作 以电商系统中用户的订单表为例。
电商系统中,常见的业务场景是通过userId查询用户的订单列表,因此使用userId作为分片键,查询时可以快速定位到数据库和表。
但是也会有通过订单号进行查询的情况,默认情况下,非分片键的查询,需要在所有分片上进行查询,然后对结果进行聚合,这样效率非常低,日志中sql如下
1 2 3 Actual SQL: ds0 ::: SELECT user_id,order_id,address_id,status FROM t_order_0 WHERE order_id = ? UNION ALL SELECT user_id,order_id,address_id,status FROM t_order_1 WHERE order_id = ? :::
此时可以考虑新增一个订单号和userId的映射关系表即索引表。
索引表法 将订单号和userId的映射关系,单独保存到一个表中,通过订单号进行查询时,先从索引表中查询到对应的userId,然后在查询条件中加上userId,这样只需要2步就可以查询出结果。
1 2 3 select user_id from order_user_relation where order_id = xxx;select * from t_order where order_id = xxx and user_id = xxx;
为了提升查询效率,可以在加上一层分布式缓存,将映射关系保存到redis中。但是这样会带来一定的问题,比如数据库和缓存的存储量增大。
基因法自定义复合分片算法 为了解决聚合查询或者索引表的问题,业界一般采用基因法来将分片键的信息,保存到非分片键中,比如取userId的后4位,拼接到order_id后面。
假设订单号的规则为 : 时间戳 + 随机数 + 用户id后四位。这样可以从订单号的后四位中截取到用户id的基因,可以定位到具体的分片位置,一次查询就可以查到对应的数据。
比如淘宝的订单号,后6位都是一样的,大概率也是用户id的后6位。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 @Override public Collection<String> doSharding (Collection availableTargetNames, ComplexKeysShardingValue complexKeysShardingValue) { int count = availableTargetNames.size(); if ((count & (count - 1 )) != 0 ) { log.warn("分片数不是2的整数次幂,当前分片数 {}" , count); throw new IllegalArgumentException ("分片数量不是2的整数次幂,当前数量:" + count); } ArrayList list = new ArrayList (availableTargetNames); List<String> result = new ArrayList <>(); log.debug("availableTargetNames的值 {}" , JSON.toJSONString(availableTargetNames)); log.debug("complexKeysShardingValue {}" , JSON.toJSONString(complexKeysShardingValue)); Map<String, Collection<Comparable<?>>> nameAndValueMap = complexKeysShardingValue.getColumnNameAndShardingValuesMap(); Collection<Comparable<?>> userIdValueCollection = nameAndValueMap.get(userIdKey); if (!CollectionUtils.isEmpty(userIdValueCollection)) { userIdValueCollection.stream().findFirst().ifPresent(o -> { Long index = (Long) o % count; log.debug("以userid {} 对 分片数{} 取余的结果为 {}" , o, count, index); result.add(String.valueOf(list.get(index.intValue()))); }); } else { Collection<Comparable<?>> orderIdCollection = nameAndValueMap.get(orderIdKey); orderIdCollection.stream().findFirst().ifPresent(comparable -> { String orderId = String.valueOf(comparable); String subString = orderId.substring(orderId.length() - userIdSuffixLength); int index = Integer.parseInt(subString) % count; log.debug("orderId中基因 {} 对 分片数{} 取余的结果为 {}" , subString, count, index); result.add(String.valueOf(list.get(index))); }); } return result; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 spring.shardingsphere.mode.type =Standalone spring.shardingsphere.mode.repository.type =JDBC spring.shardingsphere.datasource.names =ds0 spring.shardingsphere.datasource.ds0.jdbc-url =jdbc:mysql://localhost:3306/shop_ds_0?serverTimezone=Asia/Shanghai&useSSL=false&useUnicode=true&characterEncoding=UTF-8 spring.shardingsphere.datasource.ds0.type =com.zaxxer.hikari.HikariDataSource spring.shardingsphere.datasource.ds0.driver-class-name =com.mysql.jdbc.Driver spring.shardingsphere.datasource.ds0.username =root spring.shardingsphere.datasource.ds0.password =root spring.shardingsphere.rules.sharding.broadcast-tables =t_address spring.shardingsphere.rules.sharding.tables.t_order.actual-data-nodes =ds0.t_order_$->{0..1} spring.shardingsphere.rules.sharding.tables.t_order.table-strategy.complex.sharding-columns =user_id,order_id spring.shardingsphere.rules.sharding.tables.t_order.table-strategy.complex.sharding-algorithm-name =t_order_complex_custom_algorithm spring.shardingsphere.rules.sharding.sharding-algorithms.t_order_complex_custom_algorithm.type =CLASS_BASED spring.shardingsphere.rules.sharding.sharding-algorithms.t_order_complex_custom_algorithm.props.strategy =complex spring.shardingsphere.rules.sharding.sharding-algorithms.t_order_complex_custom_algorithm.props.algorithmClassName =com.bsd.xxyp.sharding.config.GeneComplexKeysShardingAlgorithm
参考文章
大众点评订单系统分库分表实践
基于Apache ShardingSphere的核心业务分库分表实践
DIY 3 种分库分表分片算法
实战:Springboot3+ShardingSphere5.2.1生产级分库分表实现
聊聊分库分表后非Sharding Key查询的三种方案
一些补充的知识点-MySQL大表分库分表基因法
看完这一篇,ShardingSphere-jdbc 实战再也不怕了
SpringBoot 2 种方式快速实现分库分表,轻松拿捏
分布式数据库:如何正确选择分片键?
FAQ整理
点评的文章中,userid后4位,为什么说最大是8192个表
按照上面的规范,分片数必须是2的n次幂,4位数的最大值是9999,假设分片数是 2的14次幂 16384,4位数字对16384取余的结果范围是[0, 9999], 实际计算出来需要的分片数是10000个,不是2的n次幂,不满足分片数规则。 所以最大只能取 8192 个
点评的方式和基因法的区别。
点评文章中userid和orderid的取模都是用的userid的后四位,所以最大是8192个。基因法里面,通过userid计算分片是按照userid % 2^n,按照订单id计算分片 = orderId.substring(length-4) % 2^n, 是使用订单号的后四位进行计算,所以userid后四位的最大分片数是16个。