springboot分库分表
每天多学一点点~
博主最近遇到订单需求,在公司大牛指导下,自己写了个分库分表,在这里记录一下,如有不足之处,欢迎各位指出。
话不多说,这就开始吧…
1.springboot配置多个数据源
- springboot配置多个数据源,话不多说,直接上代码
/**
* @Title: 多个数据源映射
* @Date 2019/5/2 01:25
* @Created by 爆裂无球
*/
@ConfigurationProperties(prefix = "spring.datasource") //配置文件的信息,读取并自动封装成实体类
@Data
public class TulingDruidProperties {
private String druid00username;
private String druid00passwrod;
private String druid00jdbcUrl;
private String druid00driverClass;
private String druid01username;
private String druid01passwrod;
private String druid01jdbcUrl;
private String druid01driverClass;
private String druid02username;
private String druid02passwrod;
private String druid02jdbcUrl;
private String druid02driverClass;
}
AbstractRoutingDataSource 这个类,不知道的可以百度下,配置多数据源这个类很重要,可以灵活的切换,可以通过AOP或者手动编程设置当前的DataSource
/**
* 多数据源类
* Created by 爆裂无球 on 2019/4/16. AbstractRoutingDataSource, 该类充当了DataSource的路由中介,
* 能有在运行时, 根据某种key值来动态切换到真正的DataSource上。
*/
@Slf4j
public class TulingMultiDataSource extends AbstractRoutingDataSource {
@Nullable
@Override
protected Object determineCurrentLookupKey() {
return MultiDataSourceHolder.getDataSourceKey();
}
}
根据当前线程来选择具体的数据源
/**
* 多数据源key 缓存类 根据当前线程来选择具体的数据源
* Created by 爆裂无球 on 2019/4/16.
*/
@Slf4j
public class MultiDataSourceHolder {
/**
* 设置动态选择的Datasource,这里的Set方法可以留给AOP调用,或者留给我们的具体的Dao层或者Service层中手动调用,在执行SQL语句之前。
*/
private static final ThreadLocal<String> dataSourceHolder = new ThreadLocal<>();
private static final ThreadLocal<String> tableIndexHolder = new ThreadLocal<>();
/**
* 保存数据源的key
* @param dsKey
*/
public static void setdataSourceKey(String dsKey) {
dataSourceHolder.set(dsKey);
}
/**
* 从threadLocal中取出key
* @return
*/
public static String getDataSourceKey() {
return dataSourceHolder.get();
}
/**
* 清除key
*/
public static void clearDataSourceKey() {
dataSourceHolder.remove();
}
public static String getTableIndex(){
return tableIndexHolder.get();
}
public static void setTableIndex(String tableIndex){
tableIndexHolder.set(tableIndex);
}
public static void clearTableIndex(){
tableIndexHolder.remove();
}
}
配置多个数据源,注入spring容器
/**
* @Title: 配置多个数据源
* @Date 2019/5/2 01:31
* @Created by 爆裂无球
*/
@Slf4j
@Configuration
@EnableConfigurationProperties({TulingDsRoutingSetProperties.class, TulingDruidProperties.class}) // @EnableConfigurationProperties注解的作用是:使使用 @ConfigurationProperties 注解的类生效。
@MapperScan(basePackages = "com.tuling.busi.dao")
public class DataSourceConfiguration {
@Autowired
private TulingDsRoutingSetProperties tulingDsRoutingSetProperties;
@Autowired
private TulingDruidProperties tulingDruidProperties;
@Bean
@ConfigurationProperties(prefix = "spring.datasource.druid00")
public DataSource dataSource00() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUsername(tulingDruidProperties.getDruid00username());
dataSource.setPassword(tulingDruidProperties.getDruid00passwrod());
dataSource.setUrl(tulingDruidProperties.getDruid00jdbcUrl());
dataSource.setDriverClassName(tulingDruidProperties.getDruid00driverClass());
return dataSource;
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.druid01")
public DataSource dataSource01() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUsername(tulingDruidProperties.getDruid01username());
dataSource.setPassword(tulingDruidProperties.getDruid01passwrod());
dataSource.setUrl(tulingDruidProperties.getDruid01jdbcUrl());
dataSource.setDriverClassName(tulingDruidProperties.getDruid01driverClass());
return dataSource;
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.druid02")
public DataSource dataSource02() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUsername(tulingDruidProperties.getDruid02username());
dataSource.setPassword(tulingDruidProperties.getDruid02passwrod());
dataSource.setUrl(tulingDruidProperties.getDruid02jdbcUrl());
dataSource.setDriverClassName(tulingDruidProperties.getDruid02driverClass());
return dataSource;
}
@Bean("tulingMultiDataSource")
public TulingMultiDataSource dataSource() {
// 自己的多数据源类 需要 继承 AbstractRoutingDataSource
TulingMultiDataSource tulingMultiDataSource = new TulingMultiDataSource();
Map<Object,Object> targetDataSources = new HashMap<>();
targetDataSources.put("dataSource00",dataSource00());
targetDataSources.put("dataSource01",dataSource01());
targetDataSources.put("dataSource02",dataSource02());
//把多个数据 和 多数据源 进行关联
tulingMultiDataSource.setTargetDataSources(targetDataSources);
//设置默认数据源
tulingMultiDataSource.setDefaultTargetDataSource(dataSource00());
//将索引字段和 数据源进行映射 ,方便 分库时候 根据取模的值 计算出是哪个库
Map<Integer,String> setMappings = new HashMap<>();
setMappings.put(0,"dataSource00");
setMappings.put(1,"dataSource01");
setMappings.put(2,"dataSource02");
tulingDsRoutingSetProperties.setDataSourceKeysMapping(setMappings);
return tulingMultiDataSource;
}
@Bean
public SqlSessionFactory sqlSessionFactory(@Qualifier("tulingMultiDataSource") TulingMultiDataSource tulingMultiDataSource) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
//设置数据源 为 上面的 自定义数据源
sqlSessionFactoryBean.setDataSource(tulingMultiDataSource);
//设置mybatis映射路径
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:/mybatis/mapper/*.xml"));
return sqlSessionFactoryBean.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) throws Exception {
return new SqlSessionTemplate(sqlSessionFactory);
}
@Bean
public DataSourceTransactionManager transactionManager(@Qualifier("tulingMultiDataSource") TulingMultiDataSource tulingMultiDataSource){
return new DataSourceTransactionManager(tulingMultiDataSource);
}
博主 在这里配置了 三个数据源 ,代码中都加了注释;
- properties配置
#配置多个数据源属性(第一个数据库)
spring.datasource.druid00username=root
spring.datasource.druid00passwrod=root
spring.datasource.druid00jdbcUrl=jdbc:mysql://localhost:3306/tuling-multiDs00
spring.datasource.druid00driverClass=com.mysql.jdbc.Driver
#配置第二个数据源
spring.datasource.druid01username=root
spring.datasource.druid01passwrod=root
spring.datasource.druid01jdbcUrl=jdbc:mysql://localhost:3306/tuling-multiDs01
spring.datasource.druid01driverClass=com.mysql.jdbc.Driver
#配置第三个数据源
spring.datasource.druid02username=root
spring.datasource.druid02passwrod=root
spring.datasource.druid02jdbcUrl=jdbc:mysql://localhost:3306/tuling-multiDs02
spring.datasource.druid02driverClass=com.mysql.jdbc.Driver
mybatis.configuration.map-underscore-to-camel-case=true
logging.level.org.springframework = WARN
logging.level.com.tuling.busi.dao= DEBUG
#配置分表分库设置属性
#分三个数据库
tuling.dsroutingset.dataSourceNum=3
#每一个库分为5个相同的表结构
tuling.dsroutingset.tableNum=4
#指定路由的字段(必须指定)
tuling.dsroutingset.routingFiled=
tuling.dsroutingset.tableSuffixStyle=%04d
tuling.dsroutingset.tableSuffixConnect=_
# 配置路由策略 ROUTING_DS_TABLE_STATEGY 多库多表 ROUTGING_DS_STATEGY 多库一表 ROUTGIN_TABLE_STATEGY 一库夺标
tuling.dsroutingset.routingStategy=ROUTING_DS_TABLE_STATEGY
2.分库分表 策略模式
提交分库分表,无非三种
- ROUTING_DS_TABLE_STATEGY 多库多表
- ROUTGING_DS_STATEGY 多库一表
- ROUTGIN_TABLE_STATEGY 一库多标
既然如此,为了遵循开闭原则,很容易想到可以用设计模式中的 策略模式 进行分装,如果不知道啥事策略模式,请自行百度,这里不再赘述;
- 分库分表三种策略的实现
代码结构图
/**
* @Title: 路由接口 策略模式接口
* @Date 2019/5/2 01:28
* @Created by 爆裂无球
*/
public interface ITulingRouting {
/**
* 根据规则计算出
* @param routingFieldValue 参数值 routingField 参数即路由字段
* @return
*/
String calDataSourceKey(String routingFieldValue,String routingField) throws LoadRoutingStategyUnMatch,RoutingFiledArgsIsNull;
/**
* 计算routingFiled字段的 hashcode值
* @param routingFiled
* @return
*/
Integer getRoutingFileHashCode(String routingFiled);
/**
* 计算一个库所在表的索引值
* @param routingFiled
* @return
*/
String calTableKey(String routingFiled) throws LoadRoutingStategyUnMatch,RoutingFiledArgsIsNull;
String getFormatTableSuffix(Integer tableIndex) throws FormatTableSuffixException;
}
```
```
/**
* 路由规则抽象类 策略模式 并加入 检查配置路由参数和 策略是否相匹配
* Created by 爆裂无球 on 2019/4/16.
*/
@Slf4j
@EnableConfigurationProperties(value = {TulingDsRoutingSetProperties.class})
@Data
public abstract class AbstractTulingRouting implements ITulingRouting ,InitializingBean{
@Autowired
private TulingDsRoutingSetProperties tulingDsRoutingSetProperties;
/**
* 获取路由key的hash值
* @param routingFiled 路由key
* @return
*/
public Integer getRoutingFileHashCode(String routingFiled){
return Math.abs(routingFiled.hashCode());
}
/**
* 获取表的后缀
* @param tableIndex 表的索引值
* @return
*/
public String getFormatTableSuffix(Integer tableIndex) {
StringBuffer stringBuffer = new StringBuffer(tulingDsRoutingSetProperties.getTableSuffixConnect());
try {
stringBuffer.append(String.format(getTulingDsRoutingSetProperties().getTableSuffixStyle(), tableIndex));
} catch (Exception e) {
log.error("格式化表后缀异常:{}",getTulingDsRoutingSetProperties().getTableSuffixStyle());
throw new FormatTableSuffixException(MultiDsErrorEnum.FORMAT_TABLE_SUFFIX_ERROR);
}
return stringBuffer.toString();
}
/**
* 工程在启动的时候 检查配置路由参数和 策略是否相匹配 因为继承了 InitializingBean
* @throws Exception
*/
public void afterPropertiesSet() throws LoadRoutingStategyUnMatch{
switch (getTulingDsRoutingSetProperties().getRoutingStategy()) {
case TulingConstant.ROUTING_DS_TABLE_STATEGY:
checkRoutingDsTableStategyConfig();
break;
case TulingConstant.ROUTGING_DS_STATEGY:
checkRoutingDsStategyConfig();
break;
case TulingConstant.ROUTGIN_TABLE_STATEGY:
checkRoutingTableStategyConfig();
break;
}
}
/**
* 检查多库 多表配置
*/
private void checkRoutingDsTableStategyConfig() {
if(tulingDsRoutingSetProperties.getTableNum()<=1 ||tulingDsRoutingSetProperties.getDataSourceNum()<=1){
log.error("你的配置项routingStategy:{}是多库多表配置,数据库个数>1," +
"每一个库中表的个数必须>1,您的配置:数据库个数:{},表的个数:{}",tulingDsRoutingSetProperties.getRoutingStategy(),
tulingDsRoutingSetProperties.getDataSourceNum(),tulingDsRoutingSetProperties.getTableNum());
throw new LoadRoutingStategyUnMatch(MultiDsErrorEnum.LOADING_STATEGY_UN_MATCH);
}
}
/**
* 检查多库一表的路由配置项
*/
private void checkRoutingDsStategyConfig() {
if(tulingDsRoutingSetProperties.getTableNum()!=1 ||tulingDsRoutingSetProperties.getDataSourceNum()<=1){
log.error("你的配置项routingStategy:{}是多库一表配置,数据库个数>1," +
"每一个库中表的个数必须=1,您的配置:数据库个数:{},表的个数:{}",tulingDsRoutingSetProperties.getRoutingStategy(),
tulingDsRoutingSetProperties.getDataSourceNum(),tulingDsRoutingSetProperties.getTableNum());
throw new LoadRoutingStategyUnMatch(MultiDsErrorEnum.LOADING_STATEGY_UN_MATCH);
}
}
/**
* 检查一库多表的路由配置项
*/
private void checkRoutingTableStategyConfig() {
if(tulingDsRoutingSetProperties.getTableNum()<=1 ||tulingDsRoutingSetProperties.getDataSourceNum()!=1){
log.error("你的配置项routingStategy:{}是一库多表配置,数据库个数=1," +
"每一个库中表的个数必须>1,您的配置:数据库个数:{},表的个数:{}",tulingDsRoutingSetProperties.getRoutingStategy(),
tulingDsRoutingSetProperties.getDataSourceNum(),tulingDsRoutingSetProperties.getTableNum());
throw new LoadRoutingStategyUnMatch(MultiDsErrorEnum.LOADING_STATEGY_UN_MATCH);
}
}
/**
* @Title: 策略配置类与配置属性关联类
* @Date 2019/5/1 11:22
* @Created by 爆裂无球
*/
@Configuration
public class RoutingStategyConfig {
/**
* 多库多表
* @return
* @ConditionalOnProperty 读取propeties文件中内容
*
* 属性name以及havingValue来实现的,其中name用来从application.properties中读取某个属性值。
如果该值为空,则返回false;
如果值不为空,则将该值与havingValue指定的值进行比较,如果一样则返回true;否则返回false。
如果返回值为false,则该configuration不生效;为true则生效。
*
*
*
*/
@Bean
@ConditionalOnProperty(prefix = "tuling.dsroutingset",name = "routingStategy",havingValue ="ROUTING_DS_TABLE_STATEGY")
public ITulingRouting routingDsAndTbStrategy() {
return new RoutingDsAndTbStrategy();
}
/**
* 多库一表
* @return
*/
@Bean
@ConditionalOnProperty(prefix = "tuling.dsroutingset",name = "routingStategy",havingValue ="ROUTGING_DS_STATEGY")
public ITulingRouting routingDsStrategy() {
return new RoutingDsStrategy();
}
/**
* 一库夺多表
* @return
*/
@Bean
@ConditionalOnProperty(prefix = "tuling.dsroutingset",name = "routingStategy",havingValue ="ROUTGIN_TABLE_STATEGY")
public ITulingRouting routingTbStategy() {
return new RoutingTbStategy();
}
}
RoutingStategyConfig 这个类,博主通过@ConditionalOnProperty(prefix = “tuling.dsroutingset”,name = “routingStategy”,havingValue =“ROUTING_DS_TABLE_STATEGY”) 注解,直接读取properties里面的内容。你在properties里面配置什么策略,则在aop切面注入策略接口ITulingRouting时,就是这个类;
取模算法:
a–hashcode值 正整数
b–数据库个数 正整数
a%b 的值一定小于b,所以一定在配置的数据源中;
博主这里定义了三个库,每个库四张表,所以一定会存入某个库中;
这只是简单的用了取模,各位可以根据实际情况选择不同的算法;
库的索引值计算:
假设路由字段是orderId,这里博主是取的是路由字段的hashcode,然后根据分库分表策略进行取模(如orderId是10086,其hashcode则是46730416,策略是多库多表,博主这里定义了三个库,每库四张表,则取模出的库的索引值则是 46730416%3=1,druid01这个库)
//将索引字段和 数据源进行映射 ,方便 分库时候 根据取模的值 计算出是哪个库
Map<Integer,String> setMappings = new HashMap<>();
setMappings.put(0,"dataSource00");
setMappings.put(1,"dataSource01");
setMappings.put(2,"dataSource02");
tulingDsRoutingSetProperties.setDataSourceKeysMapping(setMappings);
表的索引值计算:
同上;
在实体类中会有tableSuffix字段,在mybatis中
insert into order ${tableSuffix} …代表了表的索引值
- 自定义路由关键字注解,拦截数据源
/**
* 路由注解
* Created by 爆裂无球 on 2019/4/17.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Router {
String routingFiled();
}
- Aop拦截自定义注解
/**
* 拦截切面组件
* Created by 爆裂无球 on 2019/4/17.
*/
@Component
@Aspect
@Slf4j
public class RoutingAspect {
@Autowired
private ITulingRouting routing;
@Pointcut("@annotation(com.tuling.multidatasource.annotation.Router)")
public void pointCut(){};
@Before("pointCut()")
public void before(JoinPoint joinPoint) throws LoadRoutingStategyUnMatch, RoutingFiledArgsIsNull, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
long beginTime = System.currentTimeMillis();
//获取方法调用名称
Method method = getInvokeMethod(joinPoint);
//获取方法指定的注解
Router router = method.getAnnotation(Router.class);
//获取指定的路由key
String routingFiled = router.routingFiled();
//获取方法入参
Object[] args = joinPoint.getArgs();
boolean havingRoutingField = false;
if(args!=null && args.length>0) {
for(int index=0;index<args.length;index++) {
//参数值
String routingFieldValue = BeanUtils.getProperty(args[index],routingFiled);
if(!StringUtils.isEmpty(routingFieldValue)) {
//根据路由关键字 计算出 哪个数据源
String dbKey = routing.calDataSourceKey(routingFieldValue,routingFiled);
//根据路由关键字 计算出 哪个表
String tableIndex = routing.calTableKey(routingFieldValue);
log.info("选择的Dbkey是:{},tableKey是:{}",dbKey,tableIndex);
havingRoutingField = true;
break;
}else {
}
}
//判断入参中没有路由字段
if(!havingRoutingField) {
log.warn("入参{}中没有包含路由字段:{}",args,routingFiled);
throw new ParamsNotContainsRoutingField(MultiDsErrorEnum.PARAMS_NOT_CONTAINS_ROUTINGFIELD);
}
}
}
private Method getInvokeMethod(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature)signature;
Method targetMethod = methodSignature.getMethod();
return targetMethod;
}
/**
* 清除线程缓存
* @param joinPoint
*/
@After("pointCut()")
public void methodAfter(JoinPoint joinPoint){
MultiDataSourceHolder.clearDataSourceKey();
MultiDataSourceHolder.clearTableIndex();
}
ITulingRouting,有兴趣的同学debug一下,博主在上文已经说明过了,通过RoutingStategyConfig进行配置的;
分库分表的计算通过切面进行。
3.项目启动流程
- 配置properties多数据源信息
- 在controller或者service方法上面加上自定义注解 @Router() 并写入路由关键字
4.运行结果
用posman进行测试
运行结果
数据库
5.总结+源码
-
前置知识点
1.1 spring aop 知识点
1.2 自定义注解以及如何解析自定义注解
1.3 springboot 整合druid mybatis
1.4 熟悉多数据源 AbstractRoutingDataSource的工作原理
1.5 ThreadLocal的应用
1.6 设计模式 策略模式 -
插件名称解释 多库:在数据中有多个相同的数据库比如Order00 Order01 Order02 数据库 多表:在一个数据库中比如Order00中有四个order订单表 比如 order_0000 order_0001 order_0002 order_0003
-
分表分库策略 ①:多库多表策略(ROUTING_DS_TABLE_STATEGY) ②:一库多表策略(ROUTGING_DS_STATEGY) ③:多库一表策略(ROUTGIN_TABLE_STATEGY) 四:由于分库分表策略不同,导致数据库个数 和表的个数不同,可以出现错误配置,项目中作了启动配置策略检查 com.tuling.multidatasource.core.AbstractTulingRouting.afterPropertiesSet
-
由于分库分表策略不同,导致数据库个数 和表的个数不同,可以出现错误配置,项目中作了启动配置策略检查 com.tuling.multidatasource.core.AbstractTulingRouting.afterPropertiesSet
-
在自定义注解中 可以配置指定的路由key,然后在切面中去解析自定义注解获取到自定义的路由key
-
根据application.properties tuling.dsroutingset.routingStategy来指定条件装配策略
6.存在的问题
- 项目中crud都可以根据路由字段进行,但是路由字段是orderId,若想根据用户id查询到其所有的订单,那么就要根据userId再进行插入操作,会造成数据库信息冗余。
- 代码中若是不用注解则是默认数据,还不够灵活,适合作为插件使用。
- 不支持事务,也不能加入事务注解,否则数据可以插入,但是显示的插入信息不对
7.结语
世上无难事,只怕有心人,每天积累一点点,fighting!!!