注意:该项目基于上一节mysql主从配置
一. 主要pom依赖
注意:有些依赖是我自己添加用的,不是必须的
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
</parent>
<groupId>com.lzr</groupId>
<artifactId>study-readwrite-separation</artifactId>
<version>1.0-SNAPSHOT</version>
<name>读写分离项目</name>
<description>mysql 读写分离项目 测试</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-beanutils/commons-beanutils -->
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.7.0</version>
</dependency>
<!--分页工具类-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.5</version>
</dependency>
<!--mysql驱动包-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.35</version>
</dependency>
<!-- Hikaricp数据库连接池 -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
<!-- 使用Hikaricp数据库连接池所需的jdbc包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- 提供了大量mybatis操作的方法 -->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--用于编译jsp-->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<!--<scope>provided</scope>-->
</dependency>
<!-- 二维码工具包 -->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-base</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-web</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-annotation</artifactId>
<version>4.0.0</version>
</dependency>
<!-- 热更新依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<!-- Jsoup是用于解析HTML,就类似XML解析器用于解析XML -->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.10.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>1.4.2.RELEASE</version>
<configuration>
<mainClass>com.lzr.ReadWriteSeparationApplication</mainClass>
</configuration>
</plugin>
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.5</version>
<configuration>
<!-- 是否覆盖 -->
<overwrite>true</overwrite>
<!--允许移动生成的文件 -->
<verbose>true</verbose>
<!-- 自动生成的配置,${basedir}表示项目根目录 ,configurationFile默认在resource目录下-->
<configurationFile>${basedir}/src/main/resources/mybatis/mybatis-generator.xml</configurationFile>
</configuration>
<dependencies>
<!--mysql驱动包-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.35</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>3.5.3</version>
</dependency>
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.5</version>
</dependency>
</dependencies>
</plugin>
<!-- 忽略无web.xml警告 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
</plugins>
<!-- 注意:如果有这个,则不能使用mybatis-generator生成,必须先注释下面的resources配置才行 -->
<resources>
<resource>
<directory>src/main/webapp</directory>
<targetPath>META-INF/resources</targetPath>
<includes>
<include>**/**</include>
</includes>
</resource>
<!--<resource>-->
<!--<directory>src/main/java</directory>-->
<!--<includes>-->
<!--<include>**/*.yml</include>-->
<!--<include>**/*.properties</include>-->
<!--<include>**/*.xml</include>-->
<!--</includes>-->
<!--<filtering>false</filtering>-->
<!--</resource>-->
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/**</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
</project>
二. application.yml配置
spring:
# Hikaricp数据库连接池
datasource:
# 主配置(端口3306)
master:
type: com.zaxxer.hikari.HikariDataSource
jdbc-url: jdbc:mysql://localhost:3306/readwriteseparation?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
read-only: false
connection-test-query: SELECT 1
idle-timeout: 600000
max-lifetime: 3000000
connection-timeout: 3000
maximum-pool-size: 5
minimum-idle: 5
# 从配置(端口3307)(最好设置只读账户)
slave1:
type: com.zaxxer.hikari.HikariDataSource
jdbc-url: jdbc:mysql://localhost:3307/readwriteseparation?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true
username: onlyRead
password: root
driver-class-name: com.mysql.jdbc.Driver
read-only: true
idle-timeout: 600000
max-lifetime: 3000000
connection-timeout: 2000
maximum-pool-size: 5
minimum-idle: 5
# 从配置(端口3307)(最好设置只读账户)
slave2:
type: com.zaxxer.hikari.HikariDataSource
jdbc-url: jdbc:mysql://localhost:3307/readwriteseparation?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true
username: onlyRead
password: root
driver-class-name: com.mysql.jdbc.Driver
read-only: true
idle-timeout: 600000
max-lifetime: 3000000
connection-timeout: 2000
maximum-pool-size: 5
minimum-idle: 5
# Spring boot视图配置
mvc:
view:
prefix:
suffix: .jsp
# 数据时间格式处理
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
# 全局配置
server:
port: 8080
tomcat:
uri-encoding: UTF-8
# 配置pageHelper分页插件的内容
pagehelper:
auto-dialect: mysql
reasonable: true
support-methods-arguments: true
params: count=countSql
# 日志配置文件
logging:
config: classpath:logback.xml
三. 日志logback.xml配置
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<contextName>logback</contextName>
<property name="log.path" value="logs/study-readwriteseparation/logback/" />
<property name="log.file" value="logs/study-readwriteseparation/logback.log" />
<!-- 彩色日志 -->
<!-- 彩色日志依赖的渲染类 -->
<conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" />
<!-- 彩色日志格式 -->
<property name="CONSOLE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %clr(%contextName) [%thread] %clr(%-5level) %logger{36} - %msg%n" />
<!--输出到控制台 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<!-- <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>ERROR</level>
</filter> -->
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!--输出到文件 -->
<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.file}</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}%d{yyyy-MM-dd_HH}.log</fileNamePattern>
<maxHistory>30</maxHistory>
<!-- 指定日志文件的上限大小,例如设置为1GB的话,那么到了这个值,就会删除旧的日志 -->
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="console" />
<appender-ref ref="file" />
</root>
<!-- dao层日志为debug,可以打印执行的sql语句 -->
<logger name="com.lzr.dao" level="DEBUG"/>
</configuration>
四. 代码配置
注意:我这里只贴出主要代码
1. 切面处理
package com.lzr.config.datasourceconfig;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
/**
* 使用切面处理
* 如果在service层设置数据源
* 必须在事务AOP之前执行,所以实现PriorityOrdered,其中order的值越小,越先执行
* 如果一旦开始切换到写库,则之后的读都会走写库
* 注:AOP ,内部方法之间互相调用时,如果是this.xxx()这形式,不会触发AOP拦截
*/
@Aspect
@Component
public class DataSourceAop implements Ordered {
@Before("@annotation(com.lzr.annotation.Slave)")
public void read() {
DBContextHolder.slave();
}
@Before("@annotation(com.lzr.annotation.Master)")
public void write() {
DBContextHolder.master();
}
/**
* 值越小,越优先执行
* 需要优于事务的执行
* 在启动类中加上了@EnableTransactionManagement(order = 10)
* @return
*/
@Override
public int getOrder() {
return 1;
}
}
2. 数据源配置
package com.lzr.config.datasourceconfig;
import com.lzr.response.enums.DBTypeEnum;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* 数据源配置
*/
@Log4j2
@Configuration
public class DataSourceConfig {
// 也可以new HikariDataSource(),但是DataSourceBuilder.create().build()会去寻找依赖
// 当我们修改连接池的时候,就不需要修改本地的java代码了
@Bean
@ConfigurationProperties("spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.slave1")
public DataSource slave1DataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.slave2")
public DataSource slave2DataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public DataSource myRoutingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("slave1DataSource") DataSource slave1DataSource,
@Qualifier("slave2DataSource") DataSource slave2DataSource) {
log.info("开始配置多数据源");
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DBTypeEnum.MASTER, masterDataSource);
targetDataSources.put(DBTypeEnum.SLAVE1, slave1DataSource);
targetDataSources.put(DBTypeEnum.SLAVE2, slave2DataSource);
MyRoutingDataSource myRoutingDataSource = new MyRoutingDataSource();
// 默认执行数据源
myRoutingDataSource.setDefaultTargetDataSource(masterDataSource);
// 目标执行数据源集合
myRoutingDataSource.setTargetDataSources(targetDataSources);
log.info("多数据源配置完成");
return myRoutingDataSource;
}
}
3. 上下文key值处理设置类
package com.lzr.config.datasourceconfig;
import com.lzr.response.enums.DBTypeEnum;
import lombok.extern.log4j.Log4j2;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 上下文key值处理设置类
*/
@Log4j2
public class DBContextHolder {
// 初始化数字
private static final Integer initNum = new Integer(0);
private static final ThreadLocal<DBTypeEnum> threadLocal = new ThreadLocal<>();
private static final AtomicInteger atomicInteger = new AtomicInteger(initNum);
public static void set(DBTypeEnum dbType) {
threadLocal.set(dbType);
}
public static DBTypeEnum get() {
return threadLocal.get();
}
public static void master() {
set(DBTypeEnum.MASTER);
log.info("切换到master");
}
public static void slave() {
// 处理两个从库切换方式
int index = atomicInteger.getAndIncrement();
if (index>>10 > 1) {// 最大请求2048次
atomicInteger.set(initNum);
}
if (index>>1 == 0) {
set(DBTypeEnum.SLAVE1);
log.info("切换到"+DBTypeEnum.SLAVE1.name());
}else {
set(DBTypeEnum.SLAVE2);
log.info("切换到"+DBTypeEnum.SLAVE2.name());
}
}
}
4. mybatis 配置类
package com.lzr.config.datasourceconfig;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.annotation.Resource;
import javax.sql.DataSource;
/**
* mybatis 配置类
*/
@EnableTransactionManagement
@Configuration
public class MyBatisConfig {
@Resource(name = "myRoutingDataSource")
private DataSource myRoutingDataSource;
/**
* 工厂设置
* @return
* @throws Exception
*/
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(myRoutingDataSource);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));// 映射文件
sqlSessionFactoryBean.setTypeAliasesPackage("com.lzr.model,com.lzr.vo,com.lzr.dto");// 别名扫描包
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
configuration.setCallSettersOnNulls(true);// 为空时也设置数据
configuration.setMapUnderscoreToCamelCase(true);// 开启驼峰转换
sqlSessionFactoryBean.setConfiguration(configuration);
return sqlSessionFactoryBean.getObject();
}
/**
* 事务设置
* @return
*/
@Bean
public PlatformTransactionManager platformTransactionManager() {
return new DataSourceTransactionManager(myRoutingDataSource);
}
}
5. 对应的key值获取配置类
获取当前路由数据源key
package com.lzr.config.datasourceconfig;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.lang.Nullable;
/**
* 获取database对应的key值
* DataSourceConfig中配置的key
*/
public class MyRoutingDataSource extends AbstractRoutingDataSource {
/**
* 在DBContextHolder中获取当前请求对应的database的key值
* @return
*/
@Nullable
@Override
protected Object determineCurrentLookupKey() {
return DBContextHolder.get();
}
}
6. 两个注解Master与Slave
6.1 自定义Master注解
package com.lzr.annotation;
/**
* 走主库
* @author lzr
* @date 2019/12/13 0013 15:57
*/
public @interface Master {
}
6.2 自定义Slave注解
package com.lzr.annotation;
/**
* 走从库
* @author lzr
* @date 2019/12/13 0013 15:57
*/
public @interface Slave {
}
7. 自定义枚举类型
package com.lzr.response.enums;
/**
* 主从库 枚举类型 可以当做别名(也可以使用application.yml设置别名)
*/
public enum DBTypeEnum {
MASTER, SLAVE1, SLAVE2;
}
8. 启动类
package com.lzr;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import tk.mybatis.spring.annotation.MapperScan;
/**
* @author lzr
* @date 2019/12/11 0011 17:43
*/
@SpringBootApplication
@MapperScan(basePackages = "com.lzr.dao")
@EnableTransactionManagement(order = 10)// 配置事务优先级为10 (不设置order,则默认最大integer数值)
public class ReadWriteSeparationApplication {
public static void main(String[] args) {
SpringApplication.run(ReadWriteSeparationApplication.class,args);
}
}
9. service中测试图
当然,如果你不加注解,则会走默认库。
由于在类上加上了事务注解,所以在自定义aop中需要设置拦截order,需要在事务之前处理好当前线程所要走的库,不然,会先连接默认库。
五. 大致项目结构图
注意:此图非完整项目结构,如果需要,则拉取下面的git路径
六. 项目git地址
git地址:读写分离项目git地址
若有疑问,请在评论区留言,谢谢