SpringBoot+Mybatis动态切换数据源

前言:目前在接手学校的一个项目,架构是SSM,用到了读写分离,学弟改成把项目改成了SpringBoot,因为流量不大,所以取消了读写分离,为了确保项目的没问题,同时多学点知识,决定先复盘一下之前SSM项目的读写分离,然后学习一下在SpringBoot中如何实现读写分离,这里出于循序渐进的考虑,在这篇文章中我们先实现在SpringBoot+Mybatis架构下动态的切换数据源,后面的文章再实现读写分离。

参考:

https://www.cnblogs.com/panxuejun/p/6770515.html

https://blog.csdn.net/qq_37502106/article/details/91044952

目录

第一章 SSM项目中的读写分离(仅讲解)

1.1 DataSource

1.2 配置文件

1.3 实现机制分析

第二章 SpringBoot+mybatis实现动态切换数据源

2.1 配置文件

2.2 多数据源的配置代码

2.3 基础公用代码

2.4 测试代码


第一章 SSM项目中的读写分离(仅讲解)

1.1 DataSource

DataSource是一个接口,可以获取数据库的Connection。

为什么要设置DataSource呢,个人是这么理解的,业务不应该被数据库的类型所干扰,所以设置一个DataSource接口,有需要连接直接调用接口,有需要更换数据库也不需要改业务,改一下DataSource的实现就好了。

DataSource有不同的实现方案,如c3p0连接池或者阿里的DruidDataSource连接池。

值得注意的是,如果要某些现成框架操作数据库,仅仅实现了DataSource接口还是不够的,我们还需要将DataSource注入到我们操作数据库的工具中,如JdbcTemplate或者Mybatis。个人理解的话就是,DataSource负责获取连接,数据库框架负责用这些连接进行查询。

1.2 配置文件

先来看一下老项目中读写分离的配置文件,这里对一些涉及到隐私的更换了点信息

数据库配置文件(jdbc.properties):

#write主库(读写)
 write.jdbc.driver=com.mysql.jdbc.Driver
 write.jdbc.url=jdbc:mysql://127.0.0.1:3306/yjpj?useUnicode=true&characterEncoding=utf8&useSSL=false
 write.jdbc.username=writeuser
 write.jdbc.password=123456

 read.jdbc.driver=com.mysql.jdbc.Driver
 read.jdbc.url=jdbc:mysql://127.0.0.1:3306/yjpj?useUnicode=true&characterEncoding=utf8&useSSL=false
 read.jdbc.username=writeuser
 read.jdbc.password=123456

################################连接池配置###################################################
#druid连接池配置相关
#初始化连接池连接数大小
druid.initialSize=20
#连接池最小空闲连接数
druid.minIdle=20
#连接池最大连接数
druid.maxActive=100
#配置获取连接等待超时的时间(单位:毫秒)
druid.maxWait=60000
#配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
druid.timeBetweenEvictionRunsMillis=60000
#配置一个连接在池中最小生存的时间,单位是毫秒
druid.minEvictableIdleTimeMillis=40000

Mybatis配置文件(spring-dao.xml):

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	   xmlns:context="http://www.springframework.org/schema/context"
	   xmlns:cache="http://www.springframework.org/schema/cache"
	   xsi:schemaLocation="http://www.springframework.org/schema/beans
	http://www.springframework.org/schema/beans/spring-beans.xsd
	http://www.springframework.org/schema/context
	http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd">

	<context:annotation-config/>
	<!-- 1.配置数据库相关参数properties的属性:${url} -->
	<context:property-placeholder location="classpath:*.properties" />

	<!-- 2.1.两个数据源公共部分  -->
	<!-- druid数据库连接池 -->
	<bean id="abstractDataSource"  class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
	
	    <!-- 配置初始化大小、最小、最大 -->
	    <property name="initialSize" value="${druid.initialSize}" />
	    <property name="minIdle" value="${druid.minIdle}" />
	    <property name="maxActive" value="${druid.maxActive}" />
	
	    <!-- 配置获取连接等待超时的时间(单位:毫秒) -->
	    <property name="maxWait" value="${druid.maxWait}" />
	
	    <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
	    <property name="timeBetweenEvictionRunsMillis" value="${druid.timeBetweenEvictionRunsMillis}" />
	
	    <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
	    <property name="minEvictableIdleTimeMillis" value="${druid.minEvictableIdleTimeMillis}" />
	
	    <property name="validationQuery" value="select 'x'" />
	    <property name="testWhileIdle" value="true" />
	
	    <!-- 配置监控统计拦截的filters,去掉后监控界面sql无法统计 -->
	    <property name="filters" value="stat" />
	</bean>

	<!-- 2.2.写库数据源 -->
	<bean id="writeDataSource" parent="abstractDataSource">
		<!-- 配置连接池属性 -->
		<property name="driverClassName" value="${write.jdbc.driver}" />
		<property name="url" value="${write.jdbc.url}" />
		<property name="username" value="${write.jdbc.username}" />
		<property name="password" value="${write.jdbc.password}" />
	</bean>

	<!-- 2.3.读库数据源 -->
	<bean id="readDataSource" parent="abstractDataSource">
		<!-- 配置连接池属性 -->
		<property name="driverClassName" value="${read.jdbc.driver}" />
		<property name="url" value="${read.jdbc.url}" />
		<property name="username" value="${read.jdbc.username}" />
		<property name="password" value="${read.jdbc.password}" />
	</bean>

	<!-- 3.配置动态数据源 -->
	<bean id="dataSource" class="com.qcpj.common.db.DynamicDataSource">
		<property name="targetDataSources">
			<map>
				<entry key="write" value-ref="writeDataSource" />
				<entry key="read" value-ref="readDataSource" />
			</map>
		</property>
		<!-- 默认使用主库 -->
		<property name="defaultTargetDataSource" ref="writeDataSource" />
	</bean>

	<!-- 4.配置SqlSessionFactory对象 -->
	<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
		<!-- 注入数据库连接池 -->
		<property name="dataSource" ref="dataSource" />
		<!-- 配置MyBaties全局配置文件:mybatis-config.xml -->
		<property name="configLocation" value="classpath:mybatis-config.xml" />
		<!-- 扫描sql配置文件:mapper需要的xml文件 -->
		<property name="mapperLocations" value="classpath:mapper/**/*.xml" />
	</bean>

	<!-- 5.配置扫描Dao接口包,动态实现Dao接口,注入到spring容器中 -->
	<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
		<!-- 注入sqlSessionFactory -->
		<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
		<!-- 给出需要扫描Dao接口包 -->
		<property name="basePackage" value="com.qcpj.**.dao" />
	</bean>
	
</beans>

运行代码:

public class DS {

	private static Logger logger = LoggerFactory.getLogger(DS.class);

	@Autowired
	private static DataSourceHolder dsh;
    //切换为写数据库
	public static void write() {
		logger.debug("DataSource : write");
		dsh.setWrite();
	}
    //切换为读数据库
	public static void read() {
		logger.debug("DataSource : read");
		dsh.setRead();
	}

}

 

public class DataSourceHolder {

	// 线程本地环境
	private static final ThreadLocal<String> dataSources = new ThreadLocal<String>();

	// 设置数据源
	public static void setDataSource(String customerType) {
		dataSources.set(customerType);
	}

	// 设置数据源为写库
	public static void setWrite() {
		dataSources.remove();
	}

	// 设置数据库为读库
	public static void setRead() {
		dataSources.set("read");
	}

	// 获取数据源
	public static String getDataSource() {
		return (String) dataSources.get();
	}

	// 清除数据源
	public static void clearDataSource() {
		dataSources.remove();
	}

}

 

public class DynamicDataSource extends AbstractRoutingDataSource {

	@Override
	protected Object determineCurrentLookupKey() {
		return DataSourceHolder.getDataSource();
	}

}

1.3 实现机制分析

这里因为SSM再配置有点麻烦就不配置了,主要是讲解一下思路,后面会用SpringBoot实现读写分离。

可以看到使用DruidDataSource创建了两个数据源,一个是读数据源readDataSource,一个是写数据源writeDataSource,

将两个数据源注入了DynamicDataSource中(读和写注入了targetDataSources中,其中也设置了默认数据源defaultTargetDataSource),可以看到而DynamicDataSource继承了AbstractRoutingDataSource,其实也就是注入了AbstractRoutingDataSource中,在网上查到,动态切换数据数据源只要实现AbstractRoutingDataSource中的determineCurrentLookupKey方法,个人推测的话,AbstractRoutingDataSource会根据determineCurrentLookupKey方法返回的值去targetDataSources中查找对应的数据源,查找到对应的数据源后就会进行对应的切换,如果找不到的话就会返回默认。

我们的tomcat在处理请求时会使用线程池,所以这里我们有必要建立一个单例的ThreadLocal,来确保数据源的切换不会因为多线程而干扰运行,如果不设立,则会发生如一个线程切换为读模式,另一个线程切换为写模式,然后就都按写模式来了,这属于多线程操作共享资源吧,好象加锁也行,不过那就太慢了,所以我们上面建立了一个单例的dataSources,为每个线程建立独特的数据。

下面是一个数据源切换的实际代码,这里本人思考了一下,我认为其实表面说是数据源切换,实际上是根据key返回对应数据源的连接,如果数据源是切换的,那一个线程切换为写,一个切换为读那就冲突了,所以实际就是当执行sql需要连接时,调用对应的函数获取数据源的key,然后去map中拿到对应的数据源连接进行使用(仅仅是个人推断,没看源码)

//读的应用
DS.read();
List<EvaluationTemplateDto> list = evaluationTemplateDao.query(schoolId, grade, type,
					subject, target);

//写的应用
DS.write();
		List<EvaluationHabitDto> evaluationHabits = new ArrayList<EvaluationHabitDto>();
		for (TeacherEvaluation evaluation : teacherEvaluations) {
			//自定义评价不进行记录
			if(evaluation.getContentId().equals("-1") == false) {
				EvaluationHabitDto evaluationHabit = new EvaluationHabitDto();
				evaluationHabit.setSubject(evaluation.getSubject());
				evaluationHabit.setTeacherId(evaluation.getTeacherId());
				evaluationHabit.setTemplateId(evaluation.getContentId());
				evaluationHabits.add(evaluationHabit);
			}
		}

 

 

第二章 SpringBoot+mybatis实现动态切换数据源

这里主要是列代码了,具体的解析在看了第一章应该是没问题的,大致的项目架构如下:

2.1 配置文件

maven:

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.3</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

application.properties:

server.port=8080
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 数据源1
spring.datasource.druid.first.url=jdbc:mysql://localhost:3306/mysql_learn?serverTimezone=GMT%2B8
spring.datasource.druid.first.username=root
spring.datasource.druid.first.password=123456
# 数据源2
spring.datasource.druid.second.url=jdbc:mysql://localhost:3306/mysql_learn_copy?serverTimezone=GMT%2B8
spring.datasource.druid.second.username=root
spring.datasource.druid.second.password=123456
mybatis.mapperLocations=classpath:mappers/*.xml

Person.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.aesjourey.mybatisspringboot.dao.PersonMapper">
    <select id="getPersonById" parameterType="String" resultType="com.aesjourey.mybatisspringboot.entity.Person">
        select * from person where person_id = #{0}
    </select>
</mapper>

2.2 多数据源的配置代码

public interface DataSourceNames {
    String FIRST ="first";
    String SECOND = "second";
}


public class DynamicDataSource extends AbstractRoutingDataSource {

    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }

    @Override
    protected Object determineCurrentLookupKey() {
        return getDataSource();
    }

    public static void setDataSource(String dataSource) {
        CONTEXT_HOLDER.set(dataSource);
    }

    public static String getDataSource() {
        return CONTEXT_HOLDER.get();
    }

    public static void clearDataSource() {
        CONTEXT_HOLDER.remove();
    }


}


@Configuration
public class DynamicDataSourceConfig {
    @Bean
    @ConfigurationProperties("spring.datasource.druid.first")
    public DataSource firstDataSource(){
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.druid.second")
    public DataSource secondDataSource(){
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @Primary
    public DynamicDataSource dataSource(DataSource firstDataSource, DataSource secondDataSource) {
        Map<Object, Object> targetDataSources = new HashMap<>(5);
        targetDataSources.put(DataSourceNames.FIRST, firstDataSource);
        targetDataSources.put(DataSourceNames.SECOND, secondDataSource);
        return new DynamicDataSource(firstDataSource, targetDataSources);
    }
}

这里有个特别重要的,要在启动类注解上排除掉@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class }) ,因为springboot会自动注入datasource,会引起循环依赖,至于为什么自动注入会引起循环依赖我也不清楚,有知道的可以在评论区说一下,后面我会再研究下这个问题,参考的是这个文章的评论区:https://blog.csdn.net/weixin_34344677/article/details/85965061

@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class})
@MapperScan("com.aesjourey.mybatisspringboot.dao")
public class MybatisspringbootApplication {

    public static void main(String[] args) {
        SpringApplication.run(MybatisspringbootApplication.class, args);
    }

}

循环依赖大概就是下面的样子:

The dependencies of some of the beans in the application context form a cycle:

   getPerson (field com.aesjourey.mybatisspringboot.service.PersonService com.aesjourey.mybatisspringboot.controller.getPerson.personService)
      ↓
   personService (field com.aesjourey.mybatisspringboot.dao.PersonMapper com.aesjourey.mybatisspringboot.service.PersonService.personMapper)
      ↓
   personMapper defined in file [F:\IdeaProjects\mybatisspringboot\target\classes\com\aesjourey\mybatisspringboot\dao\PersonMapper.class]
      ↓
   sqlSessionFactory defined in class path resource [org/mybatis/spring/boot/autoconfigure/MybatisAutoConfiguration.class]
┌─────┐
|  dataSource defined in class path resource [com/aesjourey/mybatisspringboot/common/config/DynamicDataSourceConfig.class]
↑     ↓
|  firstDataSource defined in class path resource [com/aesjourey/mybatisspringboot/common/config/DynamicDataSourceConfig.class]
↑     ↓
|  org.springframework.boot.autoconfigure.jdbc.DataSourceInitializerInvoker
└─────┘

 

2.3 基础公用代码

@Getter
@Setter
public class Person {
    String person_id;
    String name;
    int age;
    String school;
    String home;
}
public interface PersonMapper {
    Person getPersonById(String personId);
}
@Service
public class PersonService {
    @Autowired
    PersonMapper personMapper;
    public Person getPersonById(String peopleId){
        Person person = personMapper.getPersonById(peopleId);
        return person;
    }
}

2.4 测试代码

@RestController
public class getPerson {
    static int i=0;
    @Autowired
    PersonService personService;
    @RequestMapping("/getPersonByPersonId")
    public Person getPersonByPersonId(String personId){
        if(i++%2==0){
            DynamicDataSource.setDataSource(DataSourceNames.FIRST);
        }else{
            DynamicDataSource.setDataSource(DataSourceNames.SECOND);
        }
        Person person = personService.getPersonById(personId);
        System.out.println(Thread.currentThread());
        return person;
    }
}

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 13
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值