Springboot实现Mysql的读写分离
Posted CuratorCrision
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Springboot实现Mysql的读写分离相关的知识,希望对你有一定的参考价值。
一、描述
读写分离就是对于一条SQL该选择哪一个数据库去执行,至于谁来做选择数据库这件事,有两个,要么使用中间件帮我们做,要么程序自己做。一般来说,读写分离有两种实现方式。第一种是依靠中间件MyCat,也就是说应用程序连接到中间件,中间件帮我们做SQL分离,去选择指定的数据源;第二种是应用程序自己去做分离。这里我用程序自己来做,主要是利用Spring提供的路由数据源,以及AOP。
二、AbstractRoutingDataSource
SpringBoot提供了
AbstractRoutingDataSource
类根据用户定义的规则选择当前的数据源,这样我们可以在执行查询之前,设置使用的数据源。实现可动态路由的数据源,在每次数据库查询操作前执行。它的抽象方法determineCurrentLookupKey()
决定使用哪个数据源。
三、代码实现
工程目录结构
(1)Maven依赖
<dependencies> <!--SpringBoot集成Aop起步依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <!--SpringBoot集成Jdbc起步依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!--SpringBoot集成WEB起步依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--mybatis集成SpringBoot起步依赖--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.3</version> </dependency> <!--mysql驱动--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--SpringBoot单元测试依赖--> <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> </dependencies>
(2)yml文件配置
spring:
datasource:
master:
jdbc-url: jdbc:mysql://192.168.200.199:3306/read_write_splitting?useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 362623
driver-class-name: com.mysql.cj.jdbc.Driver
slave1:
jdbc-url: jdbc:mysql://192.168.200.200:3306/read_write_splitting?useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&useSSL=false&serverTimezone=Asia/Shanghai
username: huaxin # 只读账户
password: 362623
driver-class-name: com.mysql.cj.jdbc.Driver
slave2:
jdbc-url: jdbc:mysql://192.168.200.201:3306/read_write_splitting?useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&useSSL=false&serverTimezone=Asia/Shanghai
username: huaxin # 只读账户
password: 362623
driver-class-name: com.mysql.cj.jdbc.Driver
#MyBatis
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: huaxin.entity
(3)配置多数据源
这里配置了4个数据源,1个master,2两个slave,1个路由数据源。前3个数据源都是为了生成第4个数据源,而且后续我们只用这最后一个路由数据源。
package zqb.config; 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; /** * 关于数据源配置,参考SpringBoot官方文档第79章《Data Access》 * 79. Data Access * 79.1 Configure a Custom DataSource * 79.2 Configure Two DataSources * 这里配置了4个数据源,1个master,2两个slave,1个路由数据源。前3个数据源都是为了生成第4个数据源,而且后续我们只用这最后一个路由数据源。 */ @Configuration public class DataSourceConfig @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) 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); return myRoutingDataSource;
(4)配置Mybatis指定数据源
package zqb.config;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
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;
/**
* 由于Spring容器中现在有4个数据源,所以我们需要为事务管理器和MyBatis手动指定一个明确的数据源。
* @author zqb
*/
@EnableTransactionManagement
@Configuration
public class MyBatisConfig
@Resource(name = "myRoutingDataSource")
private DataSource myRoutingDataSource;
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(myRoutingDataSource);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
return sqlSessionFactoryBean.getObject();
@Bean
public PlatformTransactionManager platformTransactionManager()
return new DataSourceTransactionManager(myRoutingDataSource);
(5)定义一个枚举类来代表这三个数据源
package zqb.config;
/**
* 定义一个枚举来代表这三个数据源
* @author zqb
*/
public enum DBTypeEnum
MASTER, SLAVE1, SLAVE2;
(6)通过ThreadLocal
将数据源绑定到每个线程上下文中
package zqb.config;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 通过ThreadLocal将数据源设置到每个线程上下文中
* @author zqb
*/
public class DBContextHolder
private static final ThreadLocal<DBTypeEnum> contextHolder = new ThreadLocal<>();
private static final AtomicInteger counter = new AtomicInteger(-1);
public static void set(DBTypeEnum dbType)
contextHolder.set(dbType);
public static DBTypeEnum get()
return contextHolder.get();
public static void master()
set(DBTypeEnum.MASTER);
System.out.println("切换到master");
public static void slave()
// 轮询
int index = counter.getAndIncrement() % 2;
if (counter.get() > 9999)
counter.set(-1);
if (index == 0)
set(DBTypeEnum.SLAVE1);
System.out.println("切换到slave1");
else
set(DBTypeEnum.SLAVE2);
System.out.println("切换到slave2");
(7)通过Aop的前置通知来设置要使用的路由key(数据源)
package zqb.config;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* 默认情况下,所有的查询都走从库,插入/修改/删除走主库。我们通过方法名来区分操作类型(CRUD)
* @author zqb
*
*/
@Aspect
@Component
public class DataSourceAop
@Pointcut("!@annotation(zqb.annotation.Master) " +
"&& (execution(* zqb.service.*.select*(..)) " +
"|| execution(* zqb.service..*.find*(..)))")
public void readPointcut()
@Pointcut("@annotation(zqb.annotation.Master) " +
"|| execution(* zqb.service..*.save*(..)) " +
"|| execution(* zqb.service..*.add*(..)) " +
"|| execution(* zqb.service..*.update*(..)) " +
"|| execution(* zqb.service..*.edit*(..)) " +
"|| execution(* zqb..*.delete*(..)) " +
"|| execution(* zqb..*.remove*(..))")
public void writePointcut()
@Before("readPointcut()")
public void read()
DBContextHolder.slave();
@Before("writePointcut()")
public void write()
DBContextHolder.master();
/**
* 另一种写法:if...else... 判断哪些需要读从数据库,其余的走主数据库
*/
// @Before("execution(* com.cjs.zqb.service.impl.*.*(..))")
// public void before(JoinPoint jp)
// String methodName = jp.getSignature().getName();
//
// if (StringUtils.startsWithAny(methodName, "get", "select", "find"))
// DBContextHolder.slave();
// else
// DBContextHolder.master();
//
//
(8)获取当前线程上绑定的路由key
package zqb.config;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.lang.Nullable;
/**
* 获取路由key
* @author x3626
*/
public class MyRoutingDataSource extends AbstractRoutingDataSource
@Nullable
@Override
protected Object determineCurrentLookupKey()
return DBContextHolder.get();
(9)特殊情况下我们需要强制读主库,针对这种情况,我们定义一个注解,用该注解标注的就读主库
package zqb.annotation;
public @interface Master
(10)测试
(11)给查询所有添加@Master注解
以上是关于Springboot实现Mysql的读写分离的主要内容,如果未能解决你的问题,请参考以下文章
SpringBoot+ShardingSphereJDBC实现读写分离!
SpringBoot+ShardingSphereJDBC实现读写分离!