Sharding-JDBC的實(shí)踐
基本概念
這幾天在研究分表分庫(kù)的方案。綜合了幾種數(shù)據(jù)庫(kù)方案。
最后選型Sharding-jdbc。它主要有如下幾個(gè)優(yōu)點(diǎn)。
- 支持分布式事務(wù)
- 適用于任何基于Java的ORM框架。
- 對(duì)業(yè)務(wù)零侵入。
數(shù)據(jù)分片
數(shù)據(jù)分片是指按照某個(gè)維度將存放在單一數(shù)據(jù)庫(kù)中的數(shù)據(jù)分散地存放至多個(gè)數(shù)據(jù)庫(kù)或者表中以達(dá)到提升性能瓶頸以及可用性的效果。數(shù)據(jù)分片有效手段是對(duì)關(guān)系型數(shù)據(jù)庫(kù)進(jìn)行分庫(kù)和分表。分表可以降低每個(gè)單表的數(shù)據(jù)閾值,同時(shí)還能夠?qū)⒎植际绞聞?wù)轉(zhuǎn)化為本地事務(wù)的。分庫(kù)可以有效的分散數(shù)據(jù)庫(kù)單點(diǎn)的訪問(wèn)量。
分片鍵
用于分片的數(shù)據(jù)庫(kù)字段,是將數(shù)據(jù)庫(kù)(表)進(jìn)行水平拆分的關(guān)鍵字段。例如:將訂單表中的訂單主鍵的尾數(shù)取模分批拿,則訂單主鍵就是分片字段,SQL中如果沒(méi)有分片字段,則執(zhí)行全庫(kù)路由,性能較差。Sharding-JDBC也支持多個(gè)字段進(jìn)行分片。
分片策略和分片算法
Sharding-JDBC 中共有五種分片策略。1、標(biāo)準(zhǔn)分片策略;2、復(fù)合分片策略;3、行表達(dá)式分片策略;4、Hint分片策略;5、不分片策略;對(duì)應(yīng)的有4種分片算法,1、精確分片算法;2、范圍分片算法;3、復(fù)合分片算法 ;4、Hint分片算法;
分片算法:
Sharding-JDBC并沒(méi)有提供內(nèi)置分片算法,而是通過(guò)分片策略將各種場(chǎng)景提煉出來(lái),提供更高層次的抽象,并提供接口讓?xiě)?yīng)用開(kāi)發(fā)者自行實(shí)現(xiàn)分片算法。
分片策略:
包含分片鍵和分片算法,由于分片算法的獨(dú)立性,將其獨(dú)立抽離。真正可用于分片操作的是分片鍵+分片算法,也就是分片策略。
Sharding-JDBC與SpringBoot整合策略
總體說(shuō)明
本實(shí)例是結(jié)合相關(guān)項(xiàng)目來(lái)的,在該項(xiàng)目中訂單id(orders_id)是一個(gè)核心的熱點(diǎn)字段。然后,訂單號(hào)是帶有日期的,所以,本次分片的方案是按照時(shí)間分庫(kù)分表,時(shí)間粒度可以年,月,季度。本demo中的時(shí)間粒度是年。
引入依賴
<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ù)庫(kù)連接
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
# 如下對(duì)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
如上可以看出,我這里配置了兩個(gè)分庫(kù)shard_order_0和shard_order_1;然后,每個(gè)分庫(kù)下面又配置了兩個(gè)邏輯表orders和orders_detail,每個(gè)邏輯表下有兩個(gè)物理表。數(shù)據(jù)庫(kù)的分片鍵是adddate,邏輯表orders的分片鍵是id,邏輯表orders_detail的分片鍵是orders_id。
完成配置之后接著就是要定義數(shù)據(jù)庫(kù)的分片策略和分片的策略以及初始化DataSource。因?yàn)楸敬卧谥鲙?kù)中加了一個(gè)路由表,在路由時(shí)動(dòng)態(tài)查取該分片值所需要查找的分庫(kù)分表,所以,需要再多配置一個(gè)數(shù)據(jù)源。用于執(zhí)行分庫(kù)的算法時(shí)可以查詢路由表,項(xiàng)目結(jié)構(gòu)如圖所示:
分庫(kù)的數(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ù)庫(kù)
shardingRuleConfig.setDefaultDataSourceName(defaultDataSource);
return ShardingDataSourceFactory.createDataSource(createDataSourceMap(), shardingRuleConfig, new Properties());
}
/**
* 獲取sqlSessionFactory實(shí)例
* @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 兩個(gè)邏輯表都采用的標(biāo)準(zhǔn)分片策略,使用的是精確分片算法。分庫(kù)的策略也是標(biāo)準(zhǔn)分片策略,使用的是精確分片算法。
數(shù)據(jù)庫(kù)的分庫(kù)策略
@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("----->該分片鍵值找不到對(duì)應(yīng)的分庫(kù),默認(rèn)取第一個(gè)庫(kù),分片鍵是={},邏輯表是={},分片值是={}",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字段時(shí)會(huì)執(zhí)行分片策略。如果SQL中adddate 字段是BETWEEN AND 需要執(zhí)行復(fù)合分片算法。否則會(huì)全庫(kù)查詢。因?yàn)槭前凑漳攴輥?lái)分庫(kù)的,所以先截取當(dāng)前的年份,然后去路由表中查。對(duì)應(yīng)的分庫(kù)id。分表策略也是類似的。
路由表
接著我們來(lái)看看路由表,路由表是該分庫(kù)分表方案中的核心表。表結(jié)構(gòu)如下:
表的數(shù)據(jù)存儲(chǔ)如下:
如上圖所示,路由表中按照年份存放了,每個(gè)年份所對(duì)應(yīng)的分庫(kù)id,和分表id。所以當(dāng)有分片鍵,進(jìn)入分表策略時(shí)就可以根據(jù)年份找到對(duì)應(yīng)的分庫(kù),分表。
多數(shù)據(jù)源配置
由于路由表是公共表,不參與分片,所以只在主庫(kù)中存儲(chǔ)了一份。當(dāng)進(jìn)入數(shù)據(jù)庫(kù)的分庫(kù)策略時(shí)就要查詢路由表。如果不配置多數(shù)據(jù)源的話,此時(shí)路由表對(duì)應(yīng)的shardConfigMapper為null。不能進(jìn)行查詢。過(guò)需要配置多數(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實(shí)例
*
* @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é)
本文首先介紹了分庫(kù)分表的相關(guān)概念,然后,對(duì)比了幾種主流的分庫(kù)分表中間件。接著重點(diǎn)介紹了分片策略和相關(guān)的算法。最后通過(guò)一個(gè)demo,實(shí)現(xiàn)了對(duì)Sharding-JDBC 數(shù)據(jù)分片的落地。
參考資料
源碼地址
https://github.com/XWxiaowei/springboot-shardingsphere-example.git
作者:碼農(nóng)飛哥
微信公眾號(hào):碼農(nóng)飛哥