Sharding-JDBC的實踐
基本概念
這幾天在研究分表分庫的方案。綜合了幾種數(shù)據(jù)庫方案。
最后選型Sharding-jdbc。它主要有如下幾個優(yōu)點。
- 支持分布式事務(wù)
- 適用于任何基于Java的ORM框架。
- 對業(yè)務(wù)零侵入。
數(shù)據(jù)分片
數(shù)據(jù)分片是指按照某個維度將存放在單一數(shù)據(jù)庫中的數(shù)據(jù)分散地存放至多個數(shù)據(jù)庫或者表中以達(dá)到提升性能瓶頸以及可用性的效果。數(shù)據(jù)分片有效手段是對關(guān)系型數(shù)據(jù)庫進(jìn)行分庫和分表。分表可以降低每個單表的數(shù)據(jù)閾值,同時還能夠?qū)⒎植际绞聞?wù)轉(zhuǎn)化為本地事務(wù)的。分庫可以有效的分散數(shù)據(jù)庫單點的訪問量。
分片鍵
用于分片的數(shù)據(jù)庫字段,是將數(shù)據(jù)庫(表)進(jìn)行水平拆分的關(guān)鍵字段。例如:將訂單表中的訂單主鍵的尾數(shù)取模分批拿,則訂單主鍵就是分片字段,SQL中如果沒有分片字段,則執(zhí)行全庫路由,性能較差。Sharding-JDBC也支持多個字段進(jìn)行分片。
分片策略和分片算法
Sharding-JDBC 中共有五種分片策略。1、標(biāo)準(zhǔn)分片策略;2、復(fù)合分片策略;3、行表達(dá)式分片策略;4、Hint分片策略;5、不分片策略;對應(yīng)的有4種分片算法,1、精確分片算法;2、范圍分片算法;3、復(fù)合分片算法 ;4、Hint分片算法;
分片算法:
Sharding-JDBC并沒有提供內(nèi)置分片算法,而是通過分片策略將各種場景提煉出來,提供更高層次的抽象,并提供接口讓應(yīng)用開發(fā)者自行實現(xiàn)分片算法。
分片策略:
包含分片鍵和分片算法,由于分片算法的獨立性,將其獨立抽離。真正可用于分片操作的是分片鍵+分片算法,也就是分片策略。
分片策略 | 概念 | 分片算法 | 概念 | 適用場景 |
---|---|---|---|---|
標(biāo)準(zhǔn)分片策略 | 對應(yīng)StandardShardingStrategy,只支持單分片鍵,提供對SQL語句中的=,IN和BETWEEN AND的分片操作支持,提供PreciseShardingAlgorithm和RangeShardingAlgorithm兩個分片算法。 | 精確分片算法 | 對應(yīng)PreciseShardingAlgorithm | 適用于單分片鍵的= 和IN進(jìn)行分片的場景。需要配合StandardShardingStrategy使用 |
標(biāo)準(zhǔn)分片策略 | 范圍分片算法 | 對應(yīng)RangeShardingAlgorithm | 適用于單分片鍵的BETWEEN AND進(jìn)行分片的場景,需要配合StandardShardingStrategy使用 | |
復(fù)合分片策略 | 對應(yīng)ComplexShardingStrategy。聽過對SQL語句中的=,IN的分片操作支持。由于多分片鍵之間的關(guān)系復(fù)雜,因此并未進(jìn)行過多的封裝,而是直接將分片鍵組合以及分片操作符透傳至分片算法 | 復(fù)合分片算法 | 對應(yīng)ComplexKeysShardingAlgorithm | 適用于多分片鍵的情況,需要配合ComplexShardingStrategy使用 |
Hint分片策略 | 對應(yīng)HintShardingStrategy。通過Hint而非SQL解析的方式分片的策略。 | Hint分片算法 | 對應(yīng)HintShardingAlgorithm | 適用于使用Hint分片的場景,需要配合HintShardingStrategy使用 |
Hint分片策略 | 對應(yīng)InlineShardingStrategy。使用Groovy的表達(dá)式,提供對SQL語句中的=和IN的分片操作支持,只支持單分片鍵。對于簡單的分片算法,可以通過簡單的配置使用,從而避免繁瑣的Java代碼開發(fā),如: t_user_$->{u_id % 8} 表示t_user表根據(jù)u_id模8,而分成8張表,表名稱為t_user_0到t_user_7。 |
Sharding-JDBC與SpringBoot整合策略
總體說明
本實例是結(jié)合相關(guān)項目來的,在該項目中訂單id(orders_id)是一個核心的熱點字段。然后,訂單號是帶有日期的,所以,本次分片的方案是按照時間分庫分表,時間粒度可以年,月,季度。本demo中的時間粒度是年。
引入依賴
<properties>
<sharding-sphere.version>4.0.0-RC1</sharding-sphere.version>
</properties>
<!--shardingsphere -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-core-api</artifactId>
<version>${sharding-sphere.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-core</artifactId>
<version>${sharding-sphere.version}</version>
</dependency>
配置好依賴之后,接著就是配置數(shù)據(jù)源,以及數(shù)據(jù)的分片鍵,分片策略。在application.yml中配置如下
配置
#數(shù)據(jù)庫連接
spring:
shardingsphere:
datasource:
names: shard_order_0,shard_order_1
shard_order_0:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/shard_order_0
username: root
password: admin
shard_order_1:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/shard_order_1
username: root
password: admin
# 如下對orders表和orders_detail都做了分片配置,分片鍵分別是id,orders_id,
sharding:
tables:
orders:
actualDataNodes: shard_order_$->{0..1}.orders_$->{0..1}
databaseStrategy:
inline:
shardingColumn: adddate
tableStrategy:
inline:
shardingColumn: id
orders_detail:
actualDataNodes: shard_order_$->{0..1}.orders_detail_$->{0..1}
tableStrategy:
inline:
shardingColumn: orders_id
如上可以看出,我這里配置了兩個分庫shard_order_0和shard_order_1;然后,每個分庫下面又配置了兩個邏輯表orders和orders_detail,每個邏輯表下有兩個物理表。數(shù)據(jù)庫的分片鍵是adddate,邏輯表orders的分片鍵是id,邏輯表orders_detail的分片鍵是orders_id。
完成配置之后接著就是要定義數(shù)據(jù)庫的分片策略和分片的策略以及初始化DataSource。因為本次在主庫中加了一個路由表,在路由時動態(tài)查取該分片值所需要查找的分庫分表,所以,需要再多配置一個數(shù)據(jù)源。用于執(zhí)行分庫的算法時可以查詢路由表,項目結(jié)構(gòu)如圖所示:
分庫的數(shù)據(jù)源配置
//*** DataSourceConfig
/**
* 設(shè)置數(shù)據(jù)源
* @return
* @throws SQLException
*/
@Bean(name = "shardingDataSource")
DataSource getShardingDataSource() throws SQLException {
ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
shardingRuleConfig.getBindingTableGroups().add(ordersLogicTable);
shardingRuleConfig.getBindingTableGroups().add(ordersDetailLogicTable);
// 配置Orders表規(guī)則
shardingRuleConfig.getTableRuleConfigs().add(getOrderTableRuleConfiguration());
//配置ordersItem表規(guī)則
shardingRuleConfig.getTableRuleConfigs().add(getOrderDetailTableRuleConfiguration());
shardingRuleConfig.setDefaultDatabaseShardingStrategyConfig(new StandardShardingStrategyConfiguration(databaseShardingColumn, preciseModuloDatabaseShardingAlgorithm));
shardingRuleConfig.setDefaultDatabaseShardingStrategyConfig(new StandardShardingStrategyConfiguration(ordersShardingColumn,preciseModuloTableShardingAlgorithm));
//設(shè)置默認(rèn)數(shù)據(jù)庫
shardingRuleConfig.setDefaultDataSourceName(defaultDataSource);
return ShardingDataSourceFactory.createDataSource(createDataSourceMap(), shardingRuleConfig, new Properties());
}
/**
* 獲取sqlSessionFactory實例
* @param shardingDataSource
* @return
* @throws Exception
*/
@Bean
@Primary
public SqlSessionFactory sqlSessionFactory(DataSource shardingDataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(shardingDataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
return bean.getObject();
}
TableRuleConfiguration getOrderTableRuleConfiguration() {
TableRuleConfiguration orderTableRuleConfig=new TableRuleConfiguration(ordersLogicTable, ordersActualDataNodes);
orderTableRuleConfig.setDatabaseShardingStrategyConfig(new StandardShardingStrategyConfiguration(databaseShardingColumn, preciseModuloDatabaseShardingAlgorithm));
orderTableRuleConfig.setTableShardingStrategyConfig(new StandardShardingStrategyConfiguration(ordersShardingColumn,preciseModuloTableShardingAlgorithm));
return orderTableRuleConfig;
}
TableRuleConfiguration getOrderDetailTableRuleConfiguration() {
TableRuleConfiguration orderDetailTableRuleConfig=new TableRuleConfiguration(ordersDetailLogicTable, ordersDetailActualDataNodes);
orderDetailTableRuleConfig.setDatabaseShardingStrategyConfig(new StandardShardingStrategyConfiguration(databaseShardingColumn, preciseModuloDatabaseShardingAlgorithm));
orderDetailTableRuleConfig.setTableShardingStrategyConfig(new StandardShardingStrategyConfiguration(ordersDetailShardingColumn, orderDetailTableShardingAlgorithm));
return orderDetailTableRuleConfig;
}
如上,可以看出orders和orders_detail 兩個邏輯表都采用的標(biāo)準(zhǔn)分片策略,使用的是精確分片算法。分庫的策略也是標(biāo)準(zhǔn)分片策略,使用的是精確分片算法。
數(shù)據(jù)庫的分庫策略
@Data
@Slf4j
@Service("preciseModuloDatabaseShardingAlgorithm")
public class DatabaseShardingAlgorithm implements PreciseShardingAlgorithm<Timestamp>{
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//定義格式,不顯示毫秒
@Autowired
private ShardConfigMapper shardConfigMapper;
/**
*
* @param collection
* @param preciseShardingValue
* @return
*/
@Override
public String doSharding(Collection<String> collection, PreciseShardingValue<Timestamp> preciseShardingValue) {
String physicDatabase = null;
Timestamp valueTime = preciseShardingValue.getValue();
String orgValue = df.format(valueTime);
String subValue = orgValue.substring(0, 4).replace("-", "");
physicDatabase = getShardConfig(physicDatabase, subValue);
if (StringUtils.isBlank(physicDatabase)) {
log.info("----->該分片鍵值找不到對應(yīng)的分庫,默認(rèn)取第一個庫,分片鍵是={},邏輯表是={},分片值是={}",preciseShardingValue.getColumnName(),preciseShardingValue.getLogicTableName(),preciseShardingValue.getValue());
for (String value : collection) {
physicDatabase = value;
break;
}
}
return physicDatabase;
}
public String getShardConfig(String physicDatabase ,String subValue ) {
ShardConfig shardConfig = shardConfigMapper.selectByPrimaryKey(subValue);
if (shardConfig != null) {
physicDatabase = shardConfig.getConfigValue().split(",")[0];
}
return physicDatabase;
}
}
如上分片鍵是adddate,當(dāng)SQL中含有adddate字段時會執(zhí)行分片策略。如果SQL中adddate 字段是BETWEEN AND 需要執(zhí)行復(fù)合分片算法。否則會全庫查詢。因為是按照年份來分庫的,所以先截取當(dāng)前的年份,然后去路由表中查。對應(yīng)的分庫id。分表策略也是類似的。
路由表
接著我們來看看路由表,路由表是該分庫分表方案中的核心表。表結(jié)構(gòu)如下:
表的數(shù)據(jù)存儲如下:
如上圖所示,路由表中按照年份存放了,每個年份所對應(yīng)的分庫id,和分表id。所以當(dāng)有分片鍵,進(jìn)入分表策略時就可以根據(jù)年份找到對應(yīng)的分庫,分表。
多數(shù)據(jù)源配置
由于路由表是公共表,不參與分片,所以只在主庫中存儲了一份。當(dāng)進(jìn)入數(shù)據(jù)庫的分庫策略時就要查詢路由表。如果不配置多數(shù)據(jù)源的話,此時路由表對應(yīng)的shardConfigMapper為null。不能進(jìn)行查詢。過需要配置多數(shù)據(jù)源。
@MapperScan(basePackages = "com.jay.mapper.nosharding", sqlSessionTemplateRef = "noshardingSqlSessionTemplate")
public class NoShardingDataSourceConfig {
@Value("${spring.shardingsphere.datasource.shard_order_0.username}")
private String username0;
@Value("${spring.shardingsphere.datasource.shard_order_0.url}")
private String url0;
@Value("${spring.shardingsphere.datasource.shard_order_0.password}")
private String password0;
@Bean(name = "noshardingDataSource")
public DataSource testDataSource() {
BasicDataSource result = new BasicDataSource();
result.setDriverClassName(com.mysql.jdbc.Driver.class.getName());
result.setUrl(url0);
result.setUsername(username0);
result.setPassword(password0);
return result;
}
/**
* 獲取sqlSessionFactory實例
*
* @param shardingDataSource
* @return
* @throws Exception
*/
@Bean(name = "noshardingSqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(@Qualifier("noshardingDataSource") DataSource shardingDataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(shardingDataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/nosharding/*.xml"));
return bean.getObject();
}
總結(jié)
本文首先介紹了分庫分表的相關(guān)概念,然后,對比了幾種主流的分庫分表中間件。接著重點介紹了分片策略和相關(guān)的算法。最后通過一個demo,實現(xiàn)了對Sharding-JDBC 數(shù)據(jù)分片的落地。
參考資料
源碼地址
https://github.com/XWxiaowei/springboot-shardingsphere-example.git
作者:碼農(nóng)飛哥
微信公眾號:碼農(nóng)飛哥