在大型应用程序中,配置主从数据库并使用读写分离是常见的设计模式。
在Spring应用程序中,要实现读写分离,最好不要对现有代码进行改动,而是在底层透明地支持。
这样,就需要我们再一个项目中,配置两个,乃至多个数据源。
今天,小编先来介绍一下自己配置动态多数据源的步骤
项目简介:
编译器:IDEA
JDK:1.8
框架:Spring Boot 2.1.0.RELEASES + Mybatis + Druid
一、配置数据库连接数据
因为项目使用的是Spring Boot 框架,该框架会自动配置数据源,自动从application.properties中读取数据源信息,如果没有配置,启动时会报错,因此我们再配置自定义的数据源的时候,需要禁掉数据源的自动配置。
但是小编在启动项目的时候,还是报错了,可是由于jdbcTemplate重复了,框架自动帮我们定义了一个jdbcTemplate,而小编自己又自定义了一个,因此,也要将这个自动配置禁止掉
启动类方法如下:
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class,JdbcTemplateAutoConfiguration.class}) @MapperScan(sqlSessionTemplateRef = "jdbcTemplate") public class DynamicDatasourseApplication { public static void main(String[] args) { SpringApplication.run(DynamicDatasourseApplication.class, args); } }
下面开始配置自定义的数据源。
新建jdbc.properties文件,配置数据库的连接,数据源1为写库,数据源2为读库
jdbc.driverClassName.db=com.mysql.jdbc.Driver #写数据源 jdbc.w.url=jdbc:mysql://localhost:3306/learning?characterEncoding=UTF-8&&serverTimezone=UTC jdbc.w.user=root jdbc.w.password=123456 #读数据源 jdbc.r.url=jdbc:mysql://localhost:3306/learning?characterEncoding=UTF-8&&serverTimezone=UTC jdbc.r.user=root jdbc.r.password=123456 #连接池属性 druid.initialSize=2 druid.minIdle=30 druid.maxActive=80 druid.maxWait=60000 druid.timeBetweenEvictionRunsMillis=60000 druid.minEvictableIdleTimeMillis=300000 druid.validationQuery=SELECT 'x' druid.testWhileIdle=true druid.testOnBorrow=false druid.testOnReturn=false druid.poolPreparedStatements=true druid.maxPoolPreparedStatementPerConnectionSize=20 druid.filters=wall,stat
二、配置mybatis的属性
在application.properties中配置mybatis的属性
mybatis.type-aliases-package:实体类的位置,如果将实体类放到Application.java文件的同级包或者下级包时,这个属性可以不配置
mybatis.mapper-locations:mapper.xml的位置
mybatis.config-location:mybatis配置文件的位置,无则不填
mybatis.type-aliases-package=cn.com.exercise.dynamicDatasourse.module.condition
mybatis.mapper-locations=/mappers/**.xml
mybatis.config-location=/config/sqlmap-config.xml
三、使用Java文件读取资源数据
1)配置主数据源(写库)
@Bean(name = '写库名字') @Primary public DataSource master(){ DruidDataSource source = new DruidDataSource(); //使用source.setXxx(Yyy);进行配置 //数据库基本属性driverClassName url、user、password配置 //连接池基本属性配置 return source; }
@Primary表示优先为注入的Bean,此处用来标识住数据源
2)配置从数据源(读库),配置内容和主数据源相同
@Bean(name = '读库名字') public DataSource master(){ DruidDataSource source = new DruidDataSource(); //使用source.setXxx(Yyy);进行配置 //数据库基本属性driverClassName url、user、password配置 //连接池基本属性配置 return source; }
3)数据源支持,配置默认数据源
@Bean(name = "dynamicDataSource") public DataSource dynamicDataSource(){ DynamicDataSource dynamicRoutingDataSource = new DynamicDataSource(); //配置多数据源 Map<Object, Object> dataSourceMap = new HashMap<>(2); dataSourceMap.put("写库名字", master()); dataSourceMap.put("读库名字", slave()); // 将 master 数据源作为默认指定的数据源 dynamicRoutingDataSource.setDefaultTargetDataSource(master()); // 将 master 和 slave 数据源作为指定的数据源 dynamicRoutingDataSource.setTargetDataSources(dataSourceMap); return dynamicRoutingDataSource; } @Bean(name="dataSourceProxy") public TransactionAwareDataSourceProxy dataSourceProxy(){ TransactionAwareDataSourceProxy proxy = new TransactionAwareDataSourceProxy(); proxy.setTargetDataSource(dynamicDataSource()); return proxy; }
4)配置sqlSessionFactory和jdbcTemplate
在sqlSessionFactory中,配置mybatis相关的三个内容:typeAliasesPackage,configLocation和mapperLocation,分别对应了application.properties中的三个内容,有则配置,无则省略。
@Bean(name = "sqlSessionFactory") public SqlSessionFactoryBean sqlSessionFactoryBean() throws Exception{ SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setVfs(SpringBootVFS.class); sqlSessionFactoryBean.setTypeAliasesPackage(typeAlias); sqlSessionFactoryBean.setConfigLocation( new ClassPathResource(sqlmapConfigPath)); PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); String packageSearchPath = PathMatchingResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX+mapperLocation; sqlSessionFactoryBean.setMapperLocations(resolver.getResources(packageSearchPath)); sqlSessionFactoryBean.setDataSource(dataSourceProxy()); return sqlSessionFactoryBean; } @Bean(name = "jdbcTemplate") public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory){ return new SqlSessionTemplate(sqlSessionFactory); }
5)配置事务传播相关内容
@Bean(name = "transactionManager") public PlatformTransactionManager transactionManager() { DataSourceTransactionManager manager = new DataSourceTransactionManager(dataSourceProxy()); return manager; } /** * 配置事务的传播特性 */ @Bean(name = "txAdvice") public TransactionInterceptor txAdvice(){ TransactionInterceptor interceptor = new TransactionInterceptor(); interceptor.setTransactionManager(transactionManager()); Properties transactionAttributes = new Properties(); //使用transactionAttributes.setProperty()配置传播特性 interceptor.setTransactionAttributes(transactionAttributes); return interceptor; } @Bean(name = "txAdviceAdvisor") public Advisor txAdviceAdvisor() { AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); String transactionExecution = "execution(* cn.com.hiveview.springboot.demoapi..service.*.*(..))"; pointcut.setExpression(transactionExecution); return new DefaultPointcutAdvisor(pointcut, txAdvice()); }
四、动态数据源支持
在上面配置动态数据源支持的时候,我们使用了一个类“DynamicDataSource.java”。
这个类是自定义的类,继承了抽象类AbstractRoutingDataSource,正是通过这个抽象类来实现动态数据源的选择的。
来看下这个抽象类的成员变量:
private Map<Object, Object> targetDataSources; private Object defaultTargetDataSource;private Map<Object, DataSource> resolvedDataSources;
以下介绍可以参考上一节(3)的内容。
【1】targetDataSources:保存了key和数据库连接的映射关系
【2】defaultTargetDataSource:表示默认的数据库连接
【3】resolvedDataSources:是通过targetDataSources构建而来,存储的结构也是数据库标识和数据源的映射关系
接下来就是根据这个类,实现我们自己的类DynamicDataSource.java
public class DynamicDataSource extends AbstractRoutingDataSource { @Autowired private DBHelper helper; @Override protected Object determineCurrentLookupKey() { return helper.getDBType(); } }
determineCurrentLookUpKey():决定需要使用哪个数据库,这个方法需要我们自己实现
先看一下在抽象类中,是如何使用这个方法的
protected DataSource determineTargetDataSource() { Assert.notNull(this.resolvedDataSources, "DataSource router not initialized"); Object lookupKey = this.determineCurrentLookupKey(); DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey); if (dataSource == null && (this.lenientFallback || lookupKey == null)) { dataSource = this.resolvedDefaultDataSource; } if (dataSource == null) { throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]"); } else { return dataSource; } }
因此我们只需在determineCurrentLookUpKey方法中,返回数据库的标志即可。
DBHelper类也是自定义的类,数据源持有类,存放了读、写库名字,以及设置数据源类型、获取数据源类型、清除数据源类型的方法
@Component public class DBHelper { /** * 线程独立 */ private ThreadLocal<String> contextHolder = new ThreadLocal<String>(); public static final String DB_TYPE_RW = "dataSource_db01"; public static final String DB_TYPE_R = "dataSource_db02"; public String getDBType() { String db = contextHolder.get(); if (db == null) { db = DB_TYPE_RW; // 默认是读写库 } return db; } public void setDBType(String str) { contextHolder.set(str); } public void clearDBType() { contextHolder.remove(); } }
五、动态切换
1)配置注解 DS.java
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE,ElementType.METHOD}) public @interface DS { String value(); }
2)使用AOP切换
@Aspect @Component public class DynamicDataSourceAspect { private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class.getName()); @Around("execution(* cn.com.exercise.dynamicDatasourse.module..service.*.*(..))") public Object switchDS(ProceedingJoinPoint point) throws Throwable { Class<?> className = point.getTarget().getClass(); String dataSource = DBHelper.DB_TYPE_RW; if (className.isAnnotationPresent(DS.class)) { DS ds = className.getAnnotation(DS.class); dataSource = ds.value(); }else{ // 得到访问的方法对象 String methodName = point.getSignature().getName(); Class[] argClass = ((MethodSignature)point.getSignature()).getParameterTypes(); Method method = className.getMethod(methodName, argClass); // 判断是否存在@DS注解 if (method.isAnnotationPresent(DS.class)) { DS annotation = method.getAnnotation(DS.class); // 取出注解中的数据源名 dataSource = annotation.value(); } } // 切换数据源 DBHelper dbHelper = new DBHelper(); dbHelper.setDBType(dataSource); logger.info("当前数据源"+dataSource); try { return point.proceed(); } finally { dbHelper.clearDBType(); } } }
完成以上内容后,就可以在service层的方法上,添加@DS注解,来实现数据源的切换了。
六、使用
service层代码
@Service public class DynamicService { @Autowired DynamicDao dynamicDao; public List<DynamicCondition> getListFromSource1(){ return dynamicDao.getListFromSource1(); } @DS('读库名字') public List<DynamicCondition> getListFromSource2(){ return dynamicDao.getListFromSource2(); } }
由于写库是默认数据源,因此当不使用@DS配置数据源,以及使用@DS(“写库名字”)时,使用的都是写库。
依次访问地址:
http://localhost:8082/dynamic/source1,
http://localhost:8082/dynamic/source2
运行结果如下:
按照以上步骤,就可以完成动态切换数据源了,下面附上 完整代码连接