前言:目前在接手学校的一个项目,架构是SSM,用到了读写分离,学弟改成把项目改成了SpringBoot,因为流量不大,所以取消了读写分离,为了确保项目的没问题,同时多学点知识,决定先复盘一下之前SSM项目的读写分离,然后学习一下在SpringBoot中如何实现读写分离,这里出于循序渐进的考虑,在这篇文章中我们先实现在SpringBoot+Mybatis架构下动态的切换数据源,后面的文章再实现读写分离。
参考:
https://www.cnblogs.com/panxuejun/p/6770515.html
https://blog.csdn.net/qq_37502106/article/details/91044952
目录
第二章 SpringBoot+mybatis实现动态切换数据源
第一章 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;
}
}