一、前言
2018年寫過一篇分庫分表的文章《SpringBoot使用sharding-jdbc分庫分表》,但是存在很多不完美的地方比如:
- sharding-jdbc的版本(1.4.2)過低,現(xiàn)在github上的最新版本都是5.3.2了,很多用法和API都過時了。
- 分庫分表配置采用Java硬編碼的方式不夠靈活
- 持久層使用的是spring-boot-starter-data-jpa,而不是主流的mybatis+mybatis-plus+druid-spring-boot-stater
- 沒有支持自定義主鍵生成策略
二、設(shè)計思路
針對上述問題,本人計劃開源一個通用的分庫分表starter,具備以下特性:
- 基于ShardingSphere-JDBC版本4.1.1,官方支持的特性我們都支持
- 支持yaml文件配置,無需編碼開箱即用
- 支持多種數(shù)據(jù)源,整合主流的mybatis
- 支持自定義主鍵生成策略,并提供默認的雪花算法實現(xiàn)
- 支持Nacos配置中心,動態(tài)刷新配置無需重啟
通過查看官方文檔,可以發(fā)現(xiàn)starter的核心邏輯就是獲取分庫分表等配置,然后在自動配置類創(chuàng)建數(shù)據(jù)源注入Spring容器即可。
三、編碼實現(xiàn)
3.1 starter工程搭建
首先創(chuàng)建一個spring-boot-starter工程ship-sharding-spring-boot-starter,不會的小伙伴可以參考以前寫的教程《【SpringBoot】編寫一個自己的Starter》。
創(chuàng)建自動配置類cn.sp.sharding.config.ShardingAutoConfig,并在resources/META-INF/spring.factories文件中配置自動配置類的全路徑。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.sp.sharding.config.ShardingAutoConfig
然后需要在pom.xml文件引入sharding-jbc依賴和工具包guava。
<properties>
<java.version>8</java.version>
<spring-boot.version>2.4.0</spring-boot.version>
<sharding-jdbc.version>4.1.1</sharding-jdbc.version>
</properties>
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-core</artifactId>
<version>${sharding-jdbc.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>
3.2 注入ShardingDataSource
分庫分表配置這塊,為了方便自定義配置前綴,創(chuàng)建ShardingRuleConfigurationProperties類繼承sharding-jbc的YamlShardingRuleConfiguration類即可,代碼如下:
/**
* @author Ship
* @version 1.0.0
* @description:
* @date 2023/06/06
*/
@ConfigurationProperties(prefix = CommonConstants.COMMON_CONFIG_PREFIX + ".config")
public class ShardingRuleConfigurationProperties extends YamlShardingRuleConfiguration {
}
同時sharding-jbc支持自定義一些properties屬性,需要單獨創(chuàng)建類ConfigMapConfigurationProperties
/**
* @Author: Ship
* @Description:
* @Date: Created in 2023/6/6
*/
@ConfigurationProperties(prefix = CommonConstants.COMMON_CONFIG_PREFIX + ".map")
public class ConfigMapConfigurationProperties {
private Properties props = new Properties();
public Properties getProps() {
return props;
}
public void setProps(Properties props) {
this.props = props;
}
}
官方提供了ShardingDataSourceFactory工廠類來創(chuàng)建數(shù)據(jù)源,但是查看其源碼發(fā)現(xiàn)createDataSource方法的參數(shù)是ShardingRuleConfiguration類,而不是YamlShardingRuleConfiguration。
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class ShardingDataSourceFactory {
/**
* Create sharding data source.
*
* @param dataSourceMap data source map
* @param shardingRuleConfig rule configuration for databases and tables sharding
* @param props properties for data source
* @return sharding data source
* @throws SQLException SQL exception
*/
public static DataSource createDataSource(
final Map<String, DataSource> dataSourceMap, final ShardingRuleConfiguration shardingRuleConfig, final Properties props) throws SQLException {
return new ShardingDataSource(dataSourceMap, new ShardingRule(shardingRuleConfig, dataSourceMap.keySet()), props);
}
}
該如何解決配置類參數(shù)轉(zhuǎn)換的問題呢?
幸好查找官方文檔發(fā)現(xiàn)sharding-jdbc提供了YamlSwapper類來實現(xiàn)yaml配置和核心配置的轉(zhuǎn)換
/**
* YAML configuration swapper.
*
* @param <Y> type of YAML configuration
* @param <T> type of swapped object
*/
public interface YamlSwapper<Y extends YamlConfiguration, T> {
/**
* Swap to YAML configuration.
*
* @param data data to be swapped
* @return YAML configuration
*/
Y swap(T data);
/**
* Swap from YAML configuration to object.
*
* @param yamlConfiguration YAML configuration
* @return swapped object
*/
T swap(Y yamlConfiguration);
}
ShardingRuleConfigurationYamlSwapper就是YamlSwapper的其中一個實現(xiàn)類。
于是,ShardingAutoConfig的最終代碼如下:
/**
* @author Ship
* @version 1.0.0
* @description:
* @date 2023/06/06
*/
@AutoConfigureBefore(name = CommonConstants.MYBATIS_PLUS_CONFIG_CLASS)
@AutoConfigureAfter(name = "com.alibaba.cloud.nacos.NacosConfigAutoConfiguration")
@Configuration
@EnableConfigurationProperties(value = {ShardingRuleConfigurationProperties.class, ConfigMapConfigurationProperties.class})
@Import(DataSourceHealthConfig.class)
public class ShardingAutoConfig {
private Map<String, DataSource> dataSourceMap = new HashMap<>();
@RefreshScope
@ConditionalOnMissingBean
@Bean
public DataSource shardingDataSource(@Autowired ShardingRuleConfigurationProperties configurationProperties,
@Autowired ConfigMapConfigurationProperties configMapConfigurationProperties,
@Autowired Environment environment) throws SQLException {
setDataSourceMap(environment);
ShardingRuleConfigurationYamlSwapper yamlSwapper = new ShardingRuleConfigurationYamlSwapper();
ShardingRuleConfiguration shardingRuleConfiguration = yamlSwapper.swap(configurationProperties);
return ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingRuleConfiguration, configMapConfigurationProperties.getProps());
}
private void setDataSourceMap(Environment environment) {
dataSourceMap.clear();
String names = environment.getProperty(CommonConstants.DATA_SOURCE_CONFIG_PREFIX + ".names");
for (String name : names.split(",")) {
try {
String propertiesPrefix = CommonConstants.DATA_SOURCE_CONFIG_PREFIX + "." + name;
Map<String, Object> dataSourceProps = PropertyUtil.handle(environment, propertiesPrefix, Map.class);
// 反射創(chuàng)建數(shù)據(jù)源
DataSource dataSource = DataSourceUtil.getDataSource(dataSourceProps.get("type").toString(), dataSourceProps);
dataSourceMap.put(name, dataSource);
} catch (ReflectiveOperationException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
利用反射創(chuàng)建數(shù)據(jù)源,就可以解決支持多種數(shù)據(jù)源的問題。
添加@RefreshScope注解是為了在使用配置中心(如Nacos)時,修改了分庫分表配置時,自動重新生成新的數(shù)據(jù)源Bean,避免項目重啟。其動態(tài)刷新配置實現(xiàn)原理,有興趣的可以查看這篇文章https://developer.aliyun.com/article/1238511。
3.3 自定義主鍵生成策略
sharding-jdbc提供了UUID和Snowflake兩種默認實現(xiàn),但是自定義主鍵生成策略更加靈活,方便根據(jù)自己的需求調(diào)整,接下來介紹如何自定義主鍵生成策略。
因為我們也是用的雪花算法,所以可以直接用sharding-jdbc提供的雪花算法類,KeyGeneratorFactory負責(zé)生成雪花算法實現(xiàn)類的實例,采用雙重校驗加鎖的單例模式。
public final class KeyGeneratorFactory {
/**
* 使用shardingsphere提供的雪花算法實現(xiàn)
*/
private static volatile SnowflakeShardingKeyGenerator keyGenerator = null;
private KeyGeneratorFactory() {
}
/**
* 單例模式
*
* @return
*/
public static SnowflakeShardingKeyGenerator getInstance() {
if (keyGenerator == null) {
synchronized (KeyGeneratorFactory.class) {
if (keyGenerator == null) {
// 用ip地址當(dāng)作機器id,機器范圍0-1024
Long workerId = Long.valueOf(IpUtil.getLocalIpAddress().replace(".", "")) % 1024;
keyGenerator = new SnowflakeShardingKeyGenerator();
Properties properties = new Properties();
properties.setProperty("worker.id", workerId.toString());
keyGenerator.setProperties(properties);
}
}
}
return keyGenerator;
}
}
雪花算法是由1bit 不用 + 41bit時間戳+10bit工作機器id+12bit序列號組成的,所以為了防止不同節(jié)點生成的id重復(fù)需要設(shè)置機器id,機器id的范圍是0-1024,這里是用IP地址轉(zhuǎn)數(shù)字取模1024來計算機器id,存在很小概率的重復(fù),也可以用redis來生成機器id(參考雪花算法ID重復(fù)問題的解決方案 )。
注意: 雪花算法坑其實挺多的,除了系統(tǒng)時間回溯會導(dǎo)致id重復(fù),單節(jié)點并發(fā)過高也會導(dǎo)致重復(fù)(序列位只有12位代表1ms內(nèi)最多支持4096個并發(fā))。
查看源碼可知自定義主鍵生成器是通過SPI實現(xiàn)的,實現(xiàn)ShardingKeyGenerator接口即可。
package org.apache.shardingsphere.spi.keygen;
import org.apache.shardingsphere.spi.TypeBasedSPI;
/**
* Key generator.
*/
public interface ShardingKeyGenerator extends TypeBasedSPI {
/**
* Generate key.
*
* @return generated key
*/
Comparable<?> generateKey();
}
- 自定義主鍵生成器DistributedKeyGenerator
/**
* @Author: Ship
* @Description: 分布式id生成器,雪花算法實現(xiàn)
* @Date: Created in 2023/6/8
*/
public class DistributedKeyGenerator implements ShardingKeyGenerator {
@Override
public Comparable<?> generateKey() {
return KeyGeneratorFactory.getInstance().generateKey();
}
@Override
public String getType() {
return "DISTRIBUTED";
}
@Override
public Properties getProperties() {
return null;
}
@Override
public void setProperties(Properties properties) {
}
}
- 創(chuàng)建META-INF/services文件夾,然后在文件夾下創(chuàng)建org.apache.shardingsphere.spi.keygen.ShardingKeyGenerator文件,內(nèi)容如下:
cn.sp.sharding.key.DistributedKeyGenerator
- yaml文件配置即可
3.4 遺留問題
Spring Boot會在項目啟動時執(zhí)行一條sql語句檢查數(shù)據(jù)源是否可用,因為ShardingDataSource只是對真實數(shù)據(jù)源進行了封裝,沒有完全實現(xiàn)Datasouce接口規(guī)范,所以會在啟動時報錯DataSource health check failed,為此需要重寫數(shù)據(jù)源健康檢查的邏輯。
創(chuàng)建DataSourceHealthConfig類繼承DataSourceHealthContributorAutoConfiguration,然后重寫createIndicator方法來重新設(shè)置校驗sql語句。
/**
* @Author: Ship
* @Description:
* @Date: Created in 2023/6/7
*/
public class DataSourceHealthConfig extends DataSourceHealthContributorAutoConfiguration {
private static String validQuery = "SELECT 1";
public DataSourceHealthConfig(Map<String, DataSource> dataSources, ObjectProvider<DataSourcePoolMetadataProvider> metadataProviders) {
super(dataSources, metadataProviders);
}
@Override
protected AbstractHealthIndicator createIndicator(DataSource source) {
DataSourceHealthIndicator healthIndicator = (DataSourceHealthIndicator) super.createIndicator(source);
if (StringUtils.hasText(validQuery)) {
healthIndicator.setQuery(validQuery);
}
return healthIndicator;
}
}
最后使用@Import注解來注入
@AutoConfigureBefore(name = CommonConstants.MYBATIS_PLUS_CONFIG_CLASS)
@Configuration
@EnableConfigurationProperties(value = {ShardingRuleConfigurationProperties.class, ConfigMapConfigurationProperties.class})
@Import(DataSourceHealthConfig.class)
public class ShardingAutoConfig implements EnvironmentAware {
四、測試
假設(shè)有個訂單表數(shù)據(jù)量很大了需要分表,為了方便水平擴展,根據(jù)訂單的創(chuàng)建時間分表,分表規(guī)則如下:
t_order_${創(chuàng)建時間所在年}_${創(chuàng)建時間所在季度}
訂單表結(jié)構(gòu)如下
CREATE TABLE `t_order_2022_3` (
`id` bigint(20) unsigned NOT NULL COMMENT '主鍵',
`order_code` varchar(32) DEFAULT NULL COMMENT '訂單號',
`create_time` bigint(20) NOT NULL COMMENT '創(chuàng)建時間',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
- 創(chuàng)建數(shù)據(jù)庫my_springboot,并創(chuàng)建8張訂單表t_order_2022_1至t_order_2023_4
- 創(chuàng)建SpringBoot項目ship-sharding-example,并添加mybatis等相關(guān)依賴
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.1</version>
<exclusions>
<exclusion>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>cn.sp</groupId>
<artifactId>ship-sharding-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
- 創(chuàng)建訂單實體Order和OrderMapper,代碼比較簡單省略
- 自定義分表算法需要實現(xiàn)PreciseShardingAlgorithm和RangeShardingAlgorithm接口的方法,它倆區(qū)別如下
接口 | 描述 |
---|---|
PreciseShardingAlgorithm | 定義等值查詢條件下的分表算法 |
RangeShardingAlgorithm | 定義范圍查詢條件下的分表算法 |
創(chuàng)建算法類MyTableShardingAlgorithm
/**
* @Author: Ship
* @Description:
* @Date: Created in 2023/6/8
*/
@Slf4j
public class MyTableShardingAlgorithm implements PreciseShardingAlgorithm<Long>, RangeShardingAlgorithm<Long> {
private static final String TABLE_NAME_PREFIX = "t_order_";
@Override
public String doSharding(Collection<String> availableTableNames, PreciseShardingValue<Long> preciseShardingValue) {
Long createTime = preciseShardingValue.getValue();
if (createTime == null) {
throw new ShipShardingException("創(chuàng)建時間不能為空!");
}
LocalDate localDate = DateUtils.longToLocalDate(createTime);
final String year = localDate.getYear() + "";
Integer quarter = DateUtils.getQuarter(localDate);
for (String tableName : availableTableNames) {
String dateStr = tableName.replace(TABLE_NAME_PREFIX, "");
String[] dateArr = dateStr.split("_");
if (dateArr[0].equals(year) && dateArr[1].equals(quarter.toString())) {
return tableName;
}
}
log.error("分表算法對應(yīng)的表不存在!");
throw new ShipShardingException("分表算法對應(yīng)的表不存在!");
}
@Override
public Collection<String> doSharding(Collection<String> availableTableNames, RangeShardingValue<Long> rangeShardingValue) {
//獲取查詢條件中范圍值
Range<Long> valueRange = rangeShardingValue.getValueRange();
// 上限值
Long upperEndpoint = valueRange.upperEndpoint();
// 下限值
Long lowerEndpoint = valueRange.lowerEndpoint();
List<String> tableNames = Lists.newArrayList();
for (String tableName : availableTableNames) {
String dateStr = tableName.replace(MyTableShardingAlgorithm.TABLE_NAME_PREFIX, "");
String[] dateArr = dateStr.split("_");
String year = dateArr[0];
String quarter = dateArr[1];
Long[] minAndMaxTime = DateUtils.getMinAndMaxTime(year, quarter);
Long minTime = minAndMaxTime[0];
Long maxTime = minAndMaxTime[1];
if (valueRange.hasLowerBound() && valueRange.hasUpperBound()) {
// between and
if (minTime.compareTo(lowerEndpoint) <= 0 && upperEndpoint.compareTo(maxTime) <= 0) {
tableNames.add(tableName);
}
} else if (valueRange.hasLowerBound() && !valueRange.hasUpperBound()) {
if (maxTime.compareTo(lowerEndpoint) > 0) {
tableNames.add(tableName);
}
} else {
if (upperEndpoint.compareTo(minTime) > 0) {
tableNames.add(tableName);
}
}
}
if (tableNames.size() == 0) {
log.error("分表算法對應(yīng)的表不存在!");
throw new ShipShardingException("分表算法對應(yīng)的表不存在!");
}
return tableNames;
}
}
- 在application.yaml上添加數(shù)據(jù)庫配置和分表配置
spring:
application:
name: ship-sharding-example
mybatis-plus:
base-package: cn.sp.sharding.dao
mapper-locations: classpath*:/mapper/*Mapper.xml
configuration:
#開啟自動駝峰命名規(guī)則(camel case)映射
map-underscore-to-camel-case: true
#延遲加載,需要和lazy-loading-enabled一起使用
aggressive-lazy-loading: true
lazy-loading-enabled: true
#關(guān)閉一級緩存
local-cache-scope: statement
#關(guān)閉二級級緩存
cache-enabled: false
ship:
sharding:
jdbc:
datasource:
names: ds0
ds0:
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://127.0.0.1:3306/my_springboot?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useSSL=false
username: root
password: 1234
initial-size: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=2000;druid.mysql.usePingMethod=false
config:
binding-tables: t_order
tables:
t_order:
actual-data-nodes: ds0.t_order_${2022..2023}_${1..4}
# 配置主鍵生成策略
key-generator:
type: DISTRIBUTED
column: id
table-strategy:
standard:
sharding-column: create_time
# 配置分表算法
precise-algorithm-class-name: cn.sp.sharding.algorithm.MyTableShardingAlgorithm
range-algorithm-class-name: cn.sp.sharding.algorithm.MyTableShardingAlgorithm
- 現(xiàn)在可以進行測試了,首先寫一個單元測試測試數(shù)據(jù)插入情況。
@Test
public void testInsert() {
Order order = new Order();
order.setOrderCode("OC001");
order.setCreateTime(System.currentTimeMillis());
orderMapper.insert(order);
}
運行testInsert()方法,打開t_order_2023_2表發(fā)現(xiàn)已經(jīng)有了一條訂單數(shù)據(jù)
并且該數(shù)據(jù)的create_time是1686383781371,轉(zhuǎn)換為時間為2023-06-10 15:56:21,剛好對應(yīng)2023年第二季度,說明數(shù)據(jù)正確的路由到了對應(yīng)的表里。
然后測試下數(shù)據(jù)查詢情況
@Test
public void testQuery(){
QueryWrapper<Order> wrapper = new QueryWrapper<>();
wrapper.lambda().eq(Order::getOrderCode,"OC001");
List<Order> orders = orderMapper.selectList(wrapper);
System.out.println(JSONUtil.toJsonStr(orders));
}
運行testQuery()方法后可以在控制臺看到輸出了訂單報文,說明查詢也沒問題。文章來源:http://www.zghlxwxcb.cn/news/detail-479769.html
[{"id":1667440550397132802,"orderCode":"OC001","createTime":1686383781371}]
五、總結(jié)
本文代碼已經(jīng)上傳到github,后續(xù)會把ship-sharding-spring-boot-starter上傳到maven中央倉庫方便使用,如果覺得對你有用的話希望可以點個贊讓更多人看到??文章來源地址http://www.zghlxwxcb.cn/news/detail-479769.html
到了這里,關(guān)于分庫分表用這個就夠了的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!