shadring-jdbc解决查询数据库分库分表的问题
问题背景
业务组在最开始的数据库规划中,通过对未来数据规模的预估后,一开始就进行了分库分表的设计。简单的说,一个业务分为16个库,其中每个库有64张业务表…过去经常使用的和数据库交互的经验就完全派不上用场了,难道真的丑陋的在代码中组装16个连接然后每个语句都去拼接吗?对不起做不到。
将自己的需求梳理清楚后可以看清楚,我需要的是一个不涉及原数据库的改动(不通过增加中间件),可以简单封装我的查询请求发送到所有分库分表中,让代码简洁好看(虽然实际上可能真的要做n * 16个数据库连接)直接一次能查询 16个库 * 64张表 的一个依赖,通过谷歌后,发现了一个叫做sharding-jdbc的开源完全贴合我的需求。
工具介绍
首先放上github的链接sharding-jdbc
这边抄袭一段介绍来凑字数:
Sharding-JDBC直接封装JDBC API,可以理解为增强版的JDBC驱动,旧代码迁移成本几乎为零:
可适用于任何基于java的ORM框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template或直接使用JDBC。
可基于任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, Druid等。
理论上可支持任意实现JDBC规范的数据库。虽然目前仅支持MySQL,但已有支持Oracle,SQLServer,DB2等数据库的计划
事实上给我的使用体验还不错,当然也是因为我的查询语句是最简单的普通查询语句,并没有涉及到太复杂的操作。翻阅issues的时候还是能看到这种第三方的依赖对于一些复杂查询的支持不足。但现在已经完全满足我的需求了。
操作步骤
正常的一个ORM框架查询数据库的应用是比较固定和规范的,由DAO层来负责数据库的沟通,本次我的应用简单的使用了Mybatis来做和Mysql的沟通。DAO层代码简单如下
public interface MtxxTagMapper {
@Select("select tag_id,tag_name,uid,`desc`,status from tb_tag where tag_id = #{tag_id}")
public Map<String, Object> findTagByTagId(@Param("tag_id") String tag_id);
}
是的就是这么简单操作。但是最大的麻烦就在于,并没有tb_tab表,或者人人都是tb_tag表。根据tag_id进行hash过后,tb_tag表被横向分布在16个库和64张表中,这条语句其实是无法执行的。所以重点不在DAO层而在于配置层,这也应了上面的介绍的话
Sharding-JDBC直接封装JDBC API,可以理解为增强版的JDBC驱动,旧代码迁移成本几乎为零
确实在查询层几乎不需要怎么动,不过复杂应用另说吧。
重点其实在于,数据源的配置,这里我使用了javaconfig的方式来配置,代码如下
@Data
@Configuration
@ConfigurationProperties(prefix = "spring.datasource.feed")
@MapperScan(basePackages = "com.meitu.dump.mapper.mtxx.tag", sqlSessionFactoryRef = "mtxxTagSqlSessionFactory")
public class MtxxTagDataSourceConfig {
private String url;
private String username;
private String password;
@Bean(name = "mtxxTagDataSource")
public DataSource getDataSource() throws SQLException {
//设置分库映射
Map<String, DataSource> dataSourceMap = new HashMap<>(16);
dataSourceMap.put("xiuxiu_feed_0", mybatisDataSource("xiuxiu_feed_0"));
dataSourceMap.put("xiuxiu_feed_1", mybatisDataSource("xiuxiu_feed_1"));
dataSourceMap.put("xiuxiu_feed_2", mybatisDataSource("xiuxiu_feed_2"));
dataSourceMap.put("xiuxiu_feed_3", mybatisDataSource("xiuxiu_feed_3"));
dataSourceMap.put("xiuxiu_feed_4", mybatisDataSource("xiuxiu_feed_4"));
dataSourceMap.put("xiuxiu_feed_5", mybatisDataSource("xiuxiu_feed_5"));
dataSourceMap.put("xiuxiu_feed_6", mybatisDataSource("xiuxiu_feed_6"));
dataSourceMap.put("xiuxiu_feed_7", mybatisDataSource("xiuxiu_feed_7"));
dataSourceMap.put("xiuxiu_feed_8", mybatisDataSource("xiuxiu_feed_8"));
dataSourceMap.put("xiuxiu_feed_9", mybatisDataSource("xiuxiu_feed_9"));
dataSourceMap.put("xiuxiu_feed_10", mybatisDataSource("xiuxiu_feed_10"));
dataSourceMap.put("xiuxiu_feed_11", mybatisDataSource("xiuxiu_feed_11"));
dataSourceMap.put("xiuxiu_feed_12", mybatisDataSource("xiuxiu_feed_12"));
dataSourceMap.put("xiuxiu_feed_13", mybatisDataSource("xiuxiu_feed_13"));
dataSourceMap.put("xiuxiu_feed_14", mybatisDataSource("xiuxiu_feed_14"));
dataSourceMap.put("xiuxiu_feed_15", mybatisDataSource("xiuxiu_feed_15"));
List<String> actualTables = new ArrayList<>();
for(int i = 0; i < XiuXiuTableItemHelper.tableNums; i++){
actualTables.add(String.format("tb_tag_%s", Integer.toString(i)));
}
//设置默认库,两个库以上时必须设置默认库。默认库的数据源名称必须是dataSourceMap的key之一
DataSourceRule dataSourceRule = new DataSourceRule(dataSourceMap, "xiuxiu_feed_0");
//设置分表映射
TableRule userTableRule = TableRule.builder("tb_tag")
.generateKeyColumn("tag_id") //将user_id作为分布式主键
.actualTables(actualTables)
.dataSourceRule(dataSourceRule)
.build();
//具体分库分表策略
ShardingRule shardingRule = ShardingRule.builder()
.dataSourceRule(dataSourceRule)
.tableRules(Collections.singletonList(userTableRule))
.databaseShardingStrategy(new DatabaseShardingStrategy("tag_id", new ModuloDatabaseShardingAlgorithm()))
.tableShardingStrategy(new TableShardingStrategy("tag_id", new ModuloTableShardingAlgorithm()))
.build();
DataSource dataSource = ShardingDataSourceFactory.createDataSource(shardingRule);
//return new ShardingDataSource(shardingRule);
return dataSource;
}
private DataSource mybatisDataSource(final String dataSourceName) throws SQLException {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl(String.format(url, dataSourceName));
dataSource.setUsername(username);
dataSource.setPassword(password);
/* 配置初始化大小、最小、最大 */
dataSource.setInitialSize(1);
dataSource.setMinIdle(1);
dataSource.setMaxActive(20);
/* 配置获取连接等待超时的时间 */
dataSource.setMaxWait(60000);
/* 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */
dataSource.setTimeBetweenEvictionRunsMillis(60000);
/* 配置一个连接在池中最小生存的时间,单位是毫秒 */
dataSource.setMinEvictableIdleTimeMillis(300000);
dataSource.setValidationQuery("SELECT 'x'");
dataSource.setTestWhileIdle(true);
dataSource.setTestOnBorrow(false);
dataSource.setTestOnReturn(false);
/* 打开PSCache,并且指定每个连接上PSCache的大小。
如果用Oracle,则把poolPreparedStatements配置为true,
mysql可以配置为false。分库分表较多的数据库,建议配置为false */
dataSource.setPoolPreparedStatements(false);
dataSource.setMaxPoolPreparedStatementPerConnectionSize(20);
/* 配置监控统计拦截的filters */
// dataSource.setFilters("stat,wall,log4j");
return dataSource;
}
/**
* Sharding-jdbc的事务支持
*
* @return
*/
@Bean(name = "mtxxTagTransactionManager")
public DataSourceTransactionManager mybatisTransactionManager(@Qualifier("mtxxTagDataSource") DataSource dataSource) throws SQLException {
return new DataSourceTransactionManager(dataSource);
}
@Bean(name = "mtxxTagSqlSessionFactory")
public SqlSessionFactory mybatisSqlSessionFactory(@Qualifier("mtxxTagDataSource") DataSource mybatisDataSource)
throws Exception {
final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(mybatisDataSource);
return sessionFactory.getObject();
}
}
在这里我确实配置了16个数据库的链接,不过是通过工厂方式生成的。对于shadring-jdbc来说,他引申出的概念叫做逻辑库和逻辑表。在配置中描述逻辑库和逻辑表的生成规则,然后以后用查询逻辑库与逻辑表来代替。我们看看是在哪里描述这个逻辑
//设置分表映射
TableRule userTableRule = TableRule.builder("tb_tag")
.generateKeyColumn("tag_id") //将user_id作为分布式主键
.actualTables(actualTables)
.dataSourceRule(dataSourceRule)
.build();
//具体分库分表策略
ShardingRule shardingRule = ShardingRule.builder()
.dataSourceRule(dataSourceRule)
.tableRules(Collections.singletonList(userTableRule))
.databaseShardingStrategy(new DatabaseShardingStrategy("tag_id", new ModuloDatabaseShardingAlgorithm()))
.tableShardingStrategy(new TableShardingStrategy("tag_id", new ModuloTableShardingAlgorithm()))
.build();
分表映射规则采用了链式生产对象的方式来描述,generateKeyColumn指定了虚拟表的主键,actualTables用来存储所有真实表的名字。
分库映射规则除了包含分表规则以外,更重要的是指定了如何计算分库和分表尾缀的逻辑。方法databaseShardingStrategy(new DatabaseShardingStrategy("tag_id", new ModuloDatabaseShardingAlgorithm()))
指定了分库的逻辑类,方法.tableShardingStrategy(new TableShardingStrategy("tag_id", new ModuloTableShardingAlgorithm()))
指定了分表的逻辑类。而这两个类的逻辑由我们自己继承接口实现而得。
再看分库实现类
@Slf4j
public class ModuloDatabaseShardingAlgorithm implements SingleKeyDatabaseShardingAlgorithm<String> {
@Override
public String doEqualSharding(Collection collection, ShardingValue shardingValue) {
int databaseNum = XiuXiuTableItemHelper.getDatabase(shardingValue.getValue().toString());
for (Object database : collection) {
if (database.toString().endsWith("_" + Integer.toString(databaseNum))) {
return database.toString();
}
}
return null;
}
@Override
public Collection<String> doInSharding(Collection collection, ShardingValue shardingValue) {
return null;
}
@Override
public Collection<String> doBetweenSharding(Collection collection, ShardingValue shardingValue) {
return null;
}
}
类只要实现了SingleKeyDatabaseShardingAlgorithm
即可作为逻辑类使用,3个实现方法分别是对应sql的一些语句使用,目前我只实现了doEqualSharding
方法,我的语句where xx =
就会执行这个方法来获得具体库名,在逻辑更复杂的情况下旧需要考虑另外2个方法的实现。
public String doEqualSharding(Collection collection, ShardingValue shardingValue)
方法中,collection是所有真实存在的库名,也就是刚刚配置代码中我们注册进去的16个库名的集合,而shardingValue指的是我们设置用来分片的字段,generateKeyColumn("tag_id")
。根据业务,我需要通过对tag_id进行hash后得到在[0-15]中对应的数据做为库名尾缀,所以我的逻辑就变成这样:
- 循环所有真实库名
- 真实库名是否包含 "_n"(n为计算分库算法得到的尾缀)
- 返回真实库名
到此就完成了一个简单的分库查询