ShardingJDBC实践
背景
因业务高速发展,线下渠道订单量高速增长,订单量每天50w+,据此计算,一个月的订单量有1500w+,一年就有1.8亿数据。
这已远远超过MySQL单表的存储极限。故急需分表
技术方案
根据自己前几年调研过的技术方案,选择使用ShardingJDBC来实现
各方案优缺点不再此展示,感兴趣的可以自行探索
具体接入代码
我们的ORM工具使用的是mybatis-plus,并且结合了多数据源,所以这次接入是在该背景下接入sharding-jdbc。如果需要单独接入sharding-jdbc可以自己截取部分粘贴到自己项目
依赖:
<dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>sharding-jdbc-spring-boot-starter</artifactId> <version>4.1.1</version></dependency><dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.3.4</version></dependency><dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <version>3.3.1</version></dependency>
yml配置:
spring: shardingsphere: datasource: names: moatkon_sharding moatkon_sharding: url: jdbc:mysql://ip:port/moatkon_db?allowMultiQueries=true&serverTimezone=Asia/Shanghai&useSSL=false&connectTimeout=2000&socketTimeout=60000 #开发 username: xxxx password: xxxx type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver sharding: tables: table1: actual-data-nodes: moatkon_sharding.table1_$->{0..127} table-strategy: hint: algorithm-class-name: com.moatkon.MoatkonShardingHintAlgorithm table2: actual-data-nodes: moatkon_sharding.table2_$->{0..127} table-strategy: hint: algorithm-class-name: com.moatkon.MoatkonShardingHintAlgorithm props: sql.show: false max.connections.size.per.query: 50 # 这个可以解决启动慢的问题,在后续的版本,sharding-jdbc优化了此问题,在4.1.1需要配置这个参数
datasource: dynamic: primary: moatkon_ds strict: false datasource: moatkon_ds: url: jdbc:mysql://ip:port/moatkon_db?allowMultiQueries=true&serverTimezone=Asia/Shanghai&useSSL=false&connectTimeout=2000&socketTimeout=60000 username: xxxxx password: xxxxx driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource druid: initial-size: 40 min-idle: 40 max-active: 100 max-wait: 10000 wall: multi-statement-allow: true druid: time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 validation-query: SELECT 1 FROM DUAL
Spring配置:
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;import com.baomidou.dynamic.datasource.provider.AbstractDataSourceProvider;import com.baomidou.dynamic.datasource.provider.DynamicDataSourceProvider;import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty;import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration;import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties;import org.apache.shardingsphere.shardingjdbc.jdbc.adapter.AbstractDataSourceAdapter;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.SpringBootConfiguration;import org.springframework.boot.autoconfigure.AutoConfigureBefore;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Lazy;import org.springframework.context.annotation.Primary;import javax.annotation.Resource;import javax.sql.DataSource;import java.util.Map;
@Configuration@AutoConfigureBefore({DynamicDataSourceAutoConfiguration.class,SpringBootConfiguration.class})public class DataSourceConfiguration { // 分表数据源名称 private static final String SHARDING_DATA_SOURCE_NAME = "sharding";
//动态数据源配置项 @Autowired private DynamicDataSourceProperties properties;
/** * shardingjdbc有四种数据源,需要根据业务注入不同的数据源 * * <p>1. 未使用分片, 脱敏的名称(默认): shardingDataSource; * <p>2. 主从数据源: masterSlaveDataSource; * <p>3. 脱敏数据源:encryptDataSource; * <p>4. 影子数据源:shadowDataSource */ @Lazy @Resource(name = "shardingDataSource") AbstractDataSourceAdapter shardingDataSource;
@Bean public DynamicDataSourceProvider dynamicDataSourceProvider() { Map<String, DataSourceProperty> datasourceMap = properties.getDatasource(); return new AbstractDataSourceProvider() { @Override public Map<String, DataSource> loadDataSources() { Map<String, DataSource> dataSourceMap = createDataSourceMap(datasourceMap); // 将 shardingjdbc 管理的数据源也交给动态数据源管理 dataSourceMap.put(SHARDING_DATA_SOURCE_NAME, shardingDataSource); return dataSourceMap; } }; }
/** * 将动态数据源设置为首选的 * 当spring存在多个数据源时, 自动注入的是首选的对象 * 设置为主要的数据源之后,就可以支持shardingjdbc原生的配置方式了 */ @Primary @Bean public DataSource dataSource(DynamicDataSourceProvider dynamicDataSourceProvider) { DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource(); dataSource.setPrimary(properties.getPrimary()); dataSource.setStrict(properties.getStrict()); dataSource.setStrategy(properties.getStrategy()); dataSource.setProvider(dynamicDataSourceProvider); dataSource.setP6spy(properties.getP6spy()); dataSource.setSeata(properties.getSeata()); return dataSource; }}
当且仅当需要使用分表时才需要切换数据源:
@DS(DataSourceConfiguration.SHARDING_DATA_SOURCE_NAME)
@DS
是dynamic-datasource-spring-boot-starter的注解,用于切换动态数据源
分表策略
因为线下渠道的用户有非会员和会员。所以我们的分表策略是,非会员按照年月归档,会员用户按照用户id分表。我们期望的是会员用户可以通过用户ID和订单号都可以定位到具体的分表,所以我们对自己内部订单号做了一些设计。
规则是: 雪花算法 + 用户ID(后3位)
通过此设计就可以达到通过用户ID和订单号都可以定位到具体分表
分表算法
因为我们的使用场景需要按年月归档非会员订单和按照用户维度进行分表,所以我们采用了灵活性非常之高的强制分片算法,实现HintShardingAlgorithm即可。
com.moatkon.MoatkonShardingHintAlgorithm
import com.google.common.collect.Lists;import java.util.ArrayList;import java.util.Collection;import java.util.Date;import java.util.List;import java.util.Optional;import lombok.extern.slf4j.Slf4j;import org.apache.shardingsphere.api.hint.HintManager;import org.apache.shardingsphere.api.sharding.hint.HintShardingAlgorithm;import org.apache.shardingsphere.api.sharding.hint.HintShardingValue;import org.springframework.stereotype.Component;
@Slf4j@Componentpublic class MoatkonShardingHintAlgorithm implements HintShardingAlgorithm<MoatkonHit> {
@Override public Collection<String> doSharding(Collection<String> collection, HintShardingValue<MoatkonHit> hintShardingValue) { // 分表结果 ArrayList<String> shardingResult = Lists.newArrayList();
String logicTableName = hintShardingValue.getLogicTableName(); Collection<MoatkonHit> values = hintShardingValue.getValues();
Optional<MoatkonHit> first = values.stream().findFirst(); if (!first.isPresent()) { throw new RuntimeException( String.format("强制分片无数据,请检查使用方式!!! table:%s", logicTableName)); }
MoatkonHit moatkonHit = first.get(); // 获取订单号 String orderNo = moatkonHit.getOrderNo();
// 获取用户Id后三位 String userIdLast3 = ShardingHelper.fetchLast3UserId(orderNo); if ("NaN".equals(userIdLast3)) { // NOT_MEMBER_ORDER_SUFFIX 非会员订单号的后三位是”NaN“,表示非会员 Date createTime = moatkonHit.getCreateTime(); String position = ShardingHelper.archiveByYearMonth(createTime); // 输出年月的字符串,例如 202401,202412之类的 shardingResult.add(logicTableName + "_" + position); // 拼接物理表名
return shardingResult; }
String position = ShardingHelper.splitByUserIdLast3Str(userIdLast3); shardingResult.add(logicTableName + "_" + position); return shardingResult; }
private static final List<String> tableList = Lists.newArrayList("table1","table2");
/** * 有使用强制分表的地方,使用这个方法可以使用该算法。 注意: 这是使用了强制分片策略才会这么使用。如果是使用其他分表策略则不需要,按照官方文档接入即可 */ public static void setHintManager(HintManager hintManager, MoatkonHit moatkonHit) { for (String table : tableList) { hintManager.addTableShardingValue(table, moatkonHit); } }}
数据倾斜问题
主要是采用 Hash一致性算法 + 虚拟表 来解决。
为什么采用虚拟表?
避免数据倾斜问题,如果出现了数据倾斜问题,而又没有采用虚拟表,大概率会将所有的数据都重新跑一遍新的分表算法(这个新的分表算法如果还没有采用虚拟表,估计也只能支撑一段时间,并不能彻底解决问题),这个成本太大了
实现原理
算法实现
1024张虚拟表,128张物理表
public static String splitByUserIdLast3Str(String userId) { // 分片键hash 散列 HashCode hashCode = Hashing.murmur3_128().hashString(userId, StandardCharsets.UTF_8);
//传入数据主键id(分片片键)和集群中机器数量buckets,返回一个固定的数字,表示数据应当落在第几个机器(虚拟表)上。 int virtualTable = Hashing.consistentHash(hashCode, 1024); // 一致性hash算法
// 计算物理表 int physicsTable = virtualTable / (1024 / 128); // 1024个环节/128表示可 物理表可使用的范围 8个表冗余,每8个表对应一个虚拟表,即步长
return String.valueOf(physicsTable); // 返回映射到的物理表 }
网站当前构建日期: 2025.02.25