8.ShardingJDBC多种分片策略
8.1.Sharding-Jdbc执行流程
- 长:SQL解析 -> SQL优化 -> SQL路由 -> SQL改写 -> SQL执行 -> 结果归并 ->返回结果
- 短:解析->路由->改写->执行->结果归并
8.2.精准分片算法《分表》
StandardShardingStrategy(标准分片策略)
- 只支持【单分片键】,提供PreciseShardingAlgorithm和RangeShardingAlgorithm两个分片算法
- PreciseShardingAlgorithm 精准分片 是必选的,用于处理=和IN的分片
- RangeShardingAlgorithm 范围分片 是可选的,用于处理BETWEEN AND分片
- 如果不配置RangeShardingAlgorithm,如果SQL中用了BETWEEN AND语法,则将按照全库路由处理,性能下降
代码案例
@Component public class CustomTablePreciseShardingAlgorithm implements PreciseShardingAlgorithm<Long> { /** * @param dataSourceNames 数据源集合 * 在分库时值为所有分片库的集合 dataSourceNames * 分表时为对应分片库种所有分片表的集合 tableNames * @param preciseShardingValue 分片属性 * logicTableName:逻辑表名 * columnName:分片健(字段) * value:从SQL中解析出的分片健的值 * @return */ @Override public String doSharding(Collection<String> dataSourceNames, PreciseShardingValue<Long> preciseShardingValue) { for (String dataSourceName : dataSourceNames) { //从preciseShardingValue中拿出分片健,和表的个数取模,确定落在哪个表中 String value = preciseShardingValue.getValue() % dataSourceNames.size() + ""; if (dataSourceName.endsWith(value)){ return dataSourceName; } } throw new IllegalArgumentException(); } }
配置文件
spring.application.name=sharding-jdbc server.port=8080 spring.shardingsphere.props.sql.show=true # 配置数据源 spring.shardingsphere.datasource.names=ds0,ds1 # 配置ds0库 spring.shardingsphere.datasource.ds0.type=com.zaxxer.hikari.HikariDataSource spring.shardingsphere.datasource.ds0.driver-class-name=com.mysql.cj.jdbc.Driver spring.shardingsphere.datasource.ds0.jdbc-url=jdbc:mysql://192.168.159.101:3306/shop_order_0?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true spring.shardingsphere.datasource.ds0.username=root spring.shardingsphere.datasource.ds0.password=123456 spring.shardingsphere.sharding.tables.product_order.key-generator.column=id spring.shardingsphere.sharding.tables.product_order.key-generator.type=SNOWFLAKE spring.shardingsphere.sharding.tables.product_order.actual-data-nodes=ds0.product_order_$->{0..1} spring.shardingsphere.sharding.tables.product_order.table-strategy.standard.sharding-column=id #配置分片策略类的全路径包名 spring.shardingsphere.sharding.tables.product_order.table-strategy.standard.precise-algorithm-class-name=com.lixiang.strategy.CustomTablePreciseShardingAlgorithm
测试代码
@Test public void testSaveProductOrder(){ Random random = new Random(); for(int i=0;i<10;i++){ ProductOrderDO productOrder = new ProductOrderDO(); productOrder.setCreateTime(new Date()); productOrder.setNickname("李祥:i="+i); productOrder.setOutTradeNo(UUID.randomUUID().toString().substring(0,32)); productOrder.setPayAmount(100.00); productOrder.setState("PAY"); productOrder.setUserId(Long.valueOf(random.nextInt(50))); productOrderMapper.insert(productOrder); } }
8.3.精准分片算法《分库分表》
新增分库策略类
public class CustomDBPreciseShardingAlgorithm implements PreciseShardingAlgorithm<Long> { /** * @param dataSourceNames 数据源集合 * 在分库时值为所有分片库的集合 dataSourceNames * 分表时为对应分片库种所有分片表的集合 tableNames * @param preciseShardingValue 分片属性 * logicTableName:逻辑表名 * columnName:分片健(字段) * value:从SQL中解析出的分片健的值 * @return */ @Override public String doSharding(Collection<String> dataSourceNames, PreciseShardingValue<Long> preciseShardingValue) { for (String dataSourceName : dataSourceNames) { //从preciseShardingValue中拿出分片健,和表的个数取模,确定落在哪个表中 String value = preciseShardingValue.getValue() % dataSourceNames.size() + ""; if (dataSourceName.endsWith(value)){ return dataSourceName; } } throw new IllegalArgumentException(); } }
配置文件
spring.application.name=sharding-jdbc server.port=8080 spring.shardingsphere.props.sql.show=true # 配置数据源 spring.shardingsphere.datasource.names=ds0,ds1 # 配置ds0库 spring.shardingsphere.datasource.ds0.type=com.zaxxer.hikari.HikariDataSource spring.shardingsphere.datasource.ds0.driver-class-name=com.mysql.cj.jdbc.Driver spring.shardingsphere.datasource.ds0.jdbc-url=jdbc:mysql://192.168.159.101:3306/shop_order_0?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true spring.shardingsphere.datasource.ds0.username=root spring.shardingsphere.datasource.ds0.password=123456 # 配置ds1库 spring.shardingsphere.datasource.ds1.type=com.zaxxer.hikari.HikariDataSource spring.shardingsphere.datasource.ds1.driver-class-name=com.mysql.cj.jdbc.Driver spring.shardingsphere.datasource.ds1.jdbc-url=jdbc:mysql://192.168.159.101:3306/shop_order_1?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true spring.shardingsphere.datasource.ds1.username=root spring.shardingsphere.datasource.ds1.password=123456 spring.shardingsphere.sharding.tables.product_order.key-generator.column=id spring.shardingsphere.sharding.tables.product_order.key-generator.type=SNOWFLAKE spring.shardingsphere.sharding.tables.product_order.actual-data-nodes=ds$->{0..1}.product_order_$->{0..1} #根据用户id分库 spring.shardingsphere.sharding.tables.product_order.database-strategy.standard.sharding-column=user_id spring.shardingsphere.sharding.tables.product_order.database-strategy.standard.precise-algorithm-class-name=com.lixiang.strategy.CustomDBPreciseShardingAlgorithm #根据订单id分表 spring.shardingsphere.sharding.tables.product_order.table-strategy.standard.sharding-column=id spring.shardingsphere.sharding.tables.product_order.table-strategy.standard.precise-algorithm-class-name=com.lixiang.strategy.CustomTablePreciseShardingAlgorithm
测试代码
@Test public void testSaveProductOrder(){ Random random = new Random(); for(int i=0;i<10;i++){ ProductOrderDO productOrder = new ProductOrderDO(); productOrder.setCreateTime(new Date()); productOrder.setNickname("李祥:i="+i); productOrder.setOutTradeNo(UUID.randomUUID().toString().substring(0,32)); productOrder.setPayAmount(100.00); productOrder.setState("PAY"); productOrder.setUserId(Long.valueOf(random.nextInt(50))); productOrderMapper.insert(productOrder); } }
8.4.范围分片算法
RangeShardingAlgorithm范围分片
- 用于处理BETWEEN AND语法,没配置的话会报错Cannot find range sharding strategy in sharding rule。
- 主要是会根据 SQL中给出的分片健值范围值处理分库、分表逻辑。
代码示例
public class CustomRangeShardingAlgorithm implements RangeShardingAlgorithm<Long> { /** * @param dataSourceNames 数据源集合 * 分库时为所有的库的集合 * 分表时为所有的表的集合 * @param rangeShardingValue 范围分片对象 * logicTableName:逻辑表名 * columnName:分片键名 * valueRange:范围对象,包括lower和upper * @return */ @Override public Collection<String> doSharding(Collection<String> dataSourceNames, RangeShardingValue<Long> rangeShardingValue) { Set<String> result = new LinkedHashSet<>(); //between 起始值 Long lower = rangeShardingValue.getValueRange().lowerEndpoint(); //between 结束值 Long upper = rangeShardingValue.getValueRange().upperEndpoint(); //循环范围计算分库逻辑 for (long i = lower; i <= upper; i++) { for (String dataSourceName : dataSourceNames) { if(dataSourceName.endsWith(i % dataSourceNames.size() +"")){ result.add(dataSourceName); } } } return result; } }
配置文件
spring.shardingsphere.sharding.tables.product_order.key-generator.column=id spring.shardingsphere.sharding.tables.product_order.key-generator.type=SNOWFLAKE spring.shardingsphere.sharding.tables.product_order.actual-data-nodes=ds$->{0..1}.product_order_$->{0..1} #根据用户id分库 spring.shardingsphere.sharding.tables.product_order.database-strategy.standard.sharding-column=user_id spring.shardingsphere.sharding.tables.product_order.database-strategy.standard.precise-algorithm-class-name=com.lixiang.strategy.CustomPreciseShardingAlgorithm #根据订单id分表 spring.shardingsphere.sharding.tables.product_order.table-strategy.standard.sharding-column=id spring.shardingsphere.sharding.tables.product_order.table-strategy.standard.precise-algorithm-class-name=com.lixiang.strategy.CustomPreciseShardingAlgorithm #根据订单id进行范围分表 spring.shardingsphere.sharding.tables.product_order.table-strategy.standard..range-algorithm-class-name=com.lixiang.strategy.CustomRangeShardingAlgorithm
测试代码
@Test public void testBetweenSelect(){ productOrderMapper.selectList(new QueryWrapper<ProductOrderDO>().between("id",1L,2L)); }
8.5.复合分片算法
复合分片算法ComplexShardingStrategy
- 提供对SQL语句中的=,in和between and的分片操作,支持【多分片健】
- 由于多分片键之间的关系复杂,Sharding-JDBC并未做过多的封装
- 而是直接将分片键值组合以及分片操作符交于算法接口,全部由应用开发者实现,提供最大的灵活度
代码案例
public class CustomComplexKeysShardingAlgorithm implements ComplexKeysShardingAlgorithm<Long> { /** * * @param dataSourceNames 数据源集合 * @param complexKeysShardingValue 分片属性 * logicTableName:逻辑表名 * columnNameAndShardingValuesMap:多分片键集合 <Map<String,Collection<T></>></>></> * columnNameAndRangeValuesMap:范围策略,<Map<String,Range<T></>></>></> * * @return */ @Override public Collection<String> doSharding(Collection<String> dataSourceNames, ComplexKeysShardingValue<Long> complexKeysShardingValue) { Collection<Long> orderIdValues = this.getShardingValue(complexKeysShardingValue,"id"); Collection<Long> userIdValues = this.getShardingValue(complexKeysShardingValue,"user_id"); List<String> shardingSuffix = new ArrayList<>(); //对两个分片键取模的方式 for (Long userId : userIdValues) { for (Long orderId : orderIdValues) { //拼接的效果,0_0,0_1,1_0,1_1,1去匹配product_order_0_0,product_order_0_1,product_order_1_0,product_order_1_1 String suffix = userId % 2 + "_" + orderId % 2; for (String databaseName : dataSourceNames) { if (databaseName.endsWith(suffix)) { shardingSuffix.add(databaseName); } } } } return null; } /** * shardingValues:分片属性 * logicTableName:逻辑表 * columnNameAndShardingValuesMap 存储多个分片健 包括key-value * value:分片value,66和99 * @param shardingValues * @param key * @return */ private Collection<Long> getShardingValue(ComplexKeysShardingValue<Long> shardingValues, String key) { Collection<Long> valueSet = new ArrayList<>(); Map<String, Collection<Long>> columnNameAndShardingValuesMap = shardingValues.getColumnNameAndShardingValuesMap(); if(columnNameAndShardingValuesMap.containsKey(key)){ valueSet.addAll(columnNameAndShardingValuesMap.get(key)); } return valueSet; } }
配置文件,记得注释其他策略,否则报错 Only allowed 0 or 1 sharding strategy configuration
#复合分片算法,order_id,user_id 同时作为分片健 spring.shardingsphere.sharding.tables.product_order.table-strategy.complex.sharding-columns=user_id,id spring.shardingsphere.sharding.tables.product_order.table-strategy.complex.algorithm-class-name=com.lixiang.strategy.CustomComplexKeysShardingAlgorithm
测试代码
@Test public void testComSelect(){ productOrderMapper.selectList(new QueryWrapper<ProductOrderDO>().eq("id",66L).eq("user_id",99L)); }
8.6.Hint分片算法
Hint分片策略HintShardingStrategy
- Hint分片策略无需配置文件进行配置分片健,分片健值也不再从SQL中解析,外部手动去指定分片健或分片库,让SQL在指定的分库、分表中执行。
- 通过Hint代码指定的方式而非SQL解析的方式分片的策略。
- Hint策略会绕过SQL解析,对于需要指定库表查询以及一些复杂的分片查询,Hint分片策略新跟那个可能会更好。
- 可以指定sql去某个库中某个表进行查询。
代码案例
//分库分表共用一个策略 public class CustomHintShardingAlgorithm implements HintShardingAlgorithm<Long> { /** * * @param dataSourceNames 数据源集合 * 在分库时值为所有分片库的集合 databaseNames * 分表时为对应分片库中所有分片表的集合 tablesNames * @param hintShardingValue 分片属性 * logicTableName:逻辑表 * columnName:分片健(字段),hint策略下为空,"" * value:不在从sql中解析分片的值,而是从 * hintManager.addDatabaseShardingValue("product_order",3L)和 * hintManager.addTableShardingValue("product_order", 8L)中拿值指定数据库,指定表 * @return */ @Override public Collection<String> doSharding(Collection<String> dataSourceNames, HintShardingValue<Long> hintShardingValue) { Collection<String> result = new ArrayList<>(); for (String dataSourceName : dataSourceNames) { for (Long value : hintShardingValue.getValues()) { if(dataSourceName.endsWith(String.valueOf(value % dataSourceNames.size()))){ result.add(dataSourceName); } } } return result; } }
配置文件
spring.application.name=sharding-jdbc server.port=8080 spring.shardingsphere.props.sql.show=true # 配置数据源 spring.shardingsphere.datasource.names=ds0,ds1 # 配置ds0库 spring.shardingsphere.datasource.ds0.type=com.zaxxer.hikari.HikariDataSource spring.shardingsphere.datasource.ds0.driver-class-name=com.mysql.cj.jdbc.Driver spring.shardingsphere.datasource.ds0.jdbc-url=jdbc:mysql://192.168.159.101:3306/shop_order_0?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true spring.shardingsphere.datasource.ds0.username=root spring.shardingsphere.datasource.ds0.password=123456 # 配置ds1库 spring.shardingsphere.datasource.ds1.type=com.zaxxer.hikari.HikariDataSource spring.shardingsphere.datasource.ds1.driver-class-name=com.mysql.cj.jdbc.Driver spring.shardingsphere.datasource.ds1.jdbc-url=jdbc:mysql://192.168.159.101:3306/shop_order_1?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true spring.shardingsphere.datasource.ds1.username=root spring.shardingsphere.datasource.ds1.password=123456 spring.shardingsphere.sharding.tables.product_order.key-generator.column=id spring.shardingsphere.sharding.tables.product_order.key-generator.type=SNOWFLAKE spring.shardingsphere.sharding.tables.product_order.actual-data-nodes=ds$->{0..1}.product_order_$->{0..1} #Hint 分片算法 spring.shardingsphere.sharding.tables.product_order.table-strategy.hint.algorithm-class-name=com.lixiang.strategy.CustomHintShardingAlgorithm spring.shardingsphere.sharding.tables.product_order.database-strategy.hint.algorithm-class-name=com.lixiang.strategy.CustomHintShardingAlgorithm
测试代码
/** * HintManger可以配合AOP切面 */ @Test public void testHint(){ //清除掉历史的规则 HintManager.clear(); //Hint分片策略必须使用HintManager工具类 HintManager hintManager = HintManager.getInstance(); //设置库的分片健,value用于表分片取模 hintManager.addDatabaseShardingValue("product_order",3L); //设置表的分片健,value用于表分片健 hintManager.addTableShardingValue("product_order",8L); //如果在读写分离数据库中,Hint可以强制读主库(主从复制存在一定的延迟) //hintManager.setMasterRouteOnly(); //对应的value只做查询,不做sql解析 productOrderMapper.selectList(new QueryWrapper<ProductOrderDO>().eq("id", 66L)); }
8.7.多种分片策略总结
- 自定义分片策略的优缺点
- 优点:可以根据分片策略代码里面自己拼接真实的数据库、真实的表,灵活控制分片规则。
- 缺点:增加了编码,不规范的sql容易造成全库表扫描,部分sql语法支持不友好。
- 行表达式分片策略InlineShardingStrategy
- 只支持【单分片键】使用Groovy的表达式,提供对SQL语句中的=和IN的分片操作支持。
- 可以通过简单的配置使用,无需自定义分片算法,从而避免繁琐的Java代码开发
product_order_$->{user_id % 8}表示订单表根据user_id模8,而分成8张表,表名称为`prouduct_order_0`到`prouduct_order_7
标准分片策略StandardShardingStrategy
- 只支持【单分片健】,提供PreciseShardingAlgorithm和RangeShardingAlgorithm两个分片算法。
- PreciseShardingAlgorithm精准分片 是必选的,用于处理=和IN的分片。
- RangeShardingAlgorithm范围分片 是可选的,用于处理BETWEEN AND分片。
- 如果不配置RangeShardingAlgorithm,如果SQL中用了BETWEEN AND语法,则将按照全库路由处理,性能下降。
- 复合分片策略ComplexShardingStrategy
- 支持【多分片键】,多分片键之间的关系复杂,由开发者自己实现,提供最大的灵活度。
- 提供对SQL语句中的=, IN和BETWEEN AND的分片操作支持。
- Hint分片策略HintShardingStrategy
- 这种分片策略无需配置分片健,分片健值也不再从 SQL中解析,外部手动指定分片健或分片库,让 SQL在指定的分库、分表中执行。
- 用于处理使用Hint行分片的场景,通过Hint而非SQL解析的方式分片的策略。
- Hint策略会绕过SQL解析的,对于这些比较复杂的需要分片的查询,Hint分片策略性能可能会更好。
9.分库分表常见问题
9.1.分库分表已解决的三大问题
问题一:执行SQL排序、翻页、函数计算问题
- 分库后,数据分布再不同的节点上, 跨节点多库进行查询时,会出现limit分页、order by排序等问题。
- 而且当排序字段非分片字段时,更加复杂了,要在不同的分片节点中将数据进行排序并返回,然后将不同分片返回的结果集进行汇总和再次排序(也会带来更多的CPU/IO资源损耗)。
- 解决方式:
- 业务上要设计合理,利用好PartitionKey,查询的数据分布同个数据节点上,避免 跨节点多库进行查询时。
- sharding-jdbc在结果合并层自动帮我们解决很多问题(流式归并和内存归并)。
- 问题二:数据库全局主键重复问题
- 常规表的id是使用自增id进行实现,分库分表后,由于表中数据同时存在不同数据库中,如果用自增id,则会出现冲突问题
- 解决方式:
- UUID
- 自研发号器 redis
- 雪花算法
问题三:分库分表技术选型问题
- 市场分库分表中间件相对较多,框架各有各的优势与短板,应该如何选择
- 解决方式
- 开源产品:主要是Mycat和ShardingJdbc区别,也是被面试官问比较多的
- 两者设计理念相同,主流程都是SQL解析–>SQL路由–>SQL改写–>结果归并
- sharding-jdbc(推荐)
- 基于jdbc驱动,不用额外的proxy,在本地应用层重写Jdbc原生的方法,实现数据库分片形式
- 是基于 JDBC 接口的扩展,是以 jar 包的形式提供轻量级服务的,性能高
- 代码有侵入性
- Mycat
- 是基于 Proxy,它复写了 MySQL 协议,将 Mycat Server 伪装成一个 MySQL 数据库
- 客户端所有的jdbc请求都必须要先交给MyCat,再有MyCat转发到具体的真实服务器
- 缺点是效率偏低,中间包装了一层
- 代码无侵入性
9.2.跨节点数据库复杂查询
- 数据库切分前,多表关联查询,可以通过sql join进行实现
- 分库分表后,数据可能分布在不同的节点上,sql join带来的问题就比较麻烦
- 不同维度查看数据,利用的partitionKey是不一样的
- 解决方案
- 冗余字段
- 广播表
- NOSQL汇总
- 案例一
- 订单需要用户的基本信息,但是分布在不同库上
- 进行字段冗余,订单表冗余用户昵称、头像
- 案例二
- 订单表 的partionKey是user_id,用户查看自己的订单列表方便
- 但商家查看自己店铺的订单列表就麻烦,分布在不同数据节点
- 订单冗余存储在es上一份
- 业务架构流程
9.3.分库分表分布式事务问题
问题:分库操作带来的分布式事务问题
- 操作内容同时分布在不同库中,不可避免会带来跨库事务问题,即分布式事务
常见分布式事务解决方案
- 2PC 和 3PC
- 两阶段提交, 基于XA协议
- TCC
- Try、Confirm、Cancel
- 事务消息
- 最大努力通知型
分布式事务框架
- X-LCN:支持2PC、TCC等多种模式
- https://github.com/codingapi/tx-lcn
- 更新慢(个人感觉处于停滞状态)
- Seata:支持 AT、TCC、SAGA 和 XA 多种模式
- https://github.com/seata/seata
- 背靠阿里,专门团队推广
- 阿里云商业化产品GTS
- https://www.aliyun.com/aliware/txc
- RocketMq:自带事务消息解决分布式事务
- MQ+本地Task
- 定时任务
9.4.分库分表后二次扩容问题
问题:容量规划,分库分表后二次扩容问题
- 业务发展快,初次分库分表后,满足不了数据存储,导致需要多次扩容,数据迁移问题
- 取决是哪种分库分表规则
- Range范围
- 时间:不用考虑扩容迁移
- 区域:调整分片粒度,需要全量迁移
- Hash取模
- 业务最多的是hash取模分片,因扩分库分表涉及到rehash过程
- 分片数量建议可以成倍扩容策略,只需要【迁移部分数据】即可
- 旧节点的数据,有一半要迁移至一个新增节点中
解决方式
方式一:利用主从同步
- 新增两个数据库 A2、A3 作为从库,设置主从同步关系为:A0=>A2、A1=>A3,
- 开启主从数据同步,早期数据手工同步过去
- 发布新程序,某个时间点开始,利用MQ存储CUD操作
- 关闭数据库实例的主从同步关系
- 校验数据,消费原先MQ存储CUD操作,配置新分片规则和生效
数据校验和修复
- 依赖gmt_modified字段,所以常规数据表都需要加这个字段
- 由数据库自己维护值,根据业务场景,进行修复对应的数据
- 校验步骤
- 开始迁移时间假如是2022-01-01 00:00:00
- 查找 gmt_modified数据校验修复大于开始时间点,就是修改过的数据
- 各个节点的冗余数据进行删除
- 缺点
- 同步的很多数据到最后都需要被删除
- 一定要提前做,越晚做成本越高,因为扩容期间需要存储的数据更多
- 基本都离不开代码侵入,加锁等操作
- 优点
- 利用mysql自带的主从同步能力
- 方案简单,代码量相对少
方式二:停服务
- 对外发布公告,停机迁移
- 严格一致性要求:比如证券、银行部分数据等
- 优点:最方便、且安全
- 缺点
- 会造成服务不可用,影响业务
- 根据停机的时间段,数据校验人员有压力
户查看自己的订单列表方便
- 但商家查看自己店铺的订单列表就麻烦,分布在不同数据节点
- 订单冗余存储在es上一份
- 业务架构流程