随着业务量的不断增长,数据库的读写压力也越来越大。为了解决这个问题,我们可以采用读写分离的方案来分担数据库的读写负载。本文将介绍如何使用 Spring Boot + MyBatis Plus + MySQL 实现读写分离。
读写分离是指将数据库的读操作和写操作分别放到不同的数据库实例上,从而达到分担数据库负载的目的。一般情况下,写操作的频率比读操作高,因此可以将写操作放到主库上,将读操作放到从库上。这样可以保证主库的写入性能,同时也可以提高从库的读取性能。
首先,我们需要创建两个数据库实例,一个用于主库,一个用于从库。这里我们使用 MySQL 数据库作为示例。
参见搭建mysql主从复制一文。
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!-- 连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.9</version>
</dependency>
<!--aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>1.8.13</version>
</dependency>
在 Spring Boot 中,我们可以使用 application.yml
文件来配置数据源。在这里,我们需要配置两个数据源,一个用于主库,一个用于从库。具体配置如下:
mysql:
datasource:
readNum: 1
type: com.alibaba.druid.pool.DruidDataSource
write:
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://10.10.26.212:3306/test?useSSL=false&useUnicode=true
minIdle: 5
maxActive: 100
initialSize: 10
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: select 'x'
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 50
removeAbandoned: true
filters: stat
read1:
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://10.10.26.213:3306/test?useSSL=false&useUnicode=true
minIdle: 5
maxActive: 100
initialSize: 10
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: select 'x'
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 50
removeAbandoned: true
filters: stat
MyBatis Plus 是一个 MyBatis 的增强工具,它可以简化 MyBatis 的开发流程。在这里,我们需要在 application.yml
文件中配置 MyBatis Plus 的相关参数。具体配置如下:
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
cache-enabled: false
typeAliasesPackage: com.sandy.dyds.model
首先,我们需要创建一个 DbContextHolder 类,用于保存当前线程使用的数据源。具体实现如下:
@Log4j2
public class DbContextHolder {
public static final String WRITE = "write";
public static final String READ = "read";
private static ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static void setDbType(String dbType) {
if(dbType == null) {
throw new NullPointerException();
}
contextHolder.set(dbType);
}
public static String getDbType() {
return contextHolder.get() == null ? WRITE : contextHolder.get();
}
public static void clearDbType() {
contextHolder.remove();
}
}
然后,我们需要创建一个 RoutingDataSource 类,用于动态切换数据源。具体实现如下:
@Log4j2
public class RoutingDataSource extends AbstractRoutingDataSource {
@Value("${mysql.datasource.readNum}")
private int num;
@Override
protected Object determineCurrentLookupKey() {
String typeKey = DbContextHolder.getDbType();
if(typeKey.equals(DbContextHolder.WRITE)) {
return typeKey;
}
//使用随机数决定使用哪个读库
//在1-N之间生成整型随机数
int random = (int) (Math.random() * 1) + num;
return DbContextHolder.READ + random;
}
}
接着,我们需要创建一个 DataSourceConfig 类,用于配置数据源。具体实现如下:
@Configuration
public class DataSourceConfig {
@Value("${mysql.datasource.type}")
private Class<? extends DataSource> dataSourceType;
/**
* 写数据源
* Primary 标志这个 Bean 如果在多个同类 Bean 候选时,该 Bean 优先被考虑。
* 多数据源配置的时候注意,必须要有一个主数据源,用 @Primary 标志该 Bean
*/
@Primary
@Bean
@ConfigurationProperties(prefix = "mysql.datasource.write")
public DataSource writeDataSource() {
DataSource ds = DataSourceBuilder.create().type(dataSourceType).build();
return ds;
}
@Bean
@ConfigurationProperties(prefix = "mysql.datasource.read1")
public DataSource readDataSource_1() {
DataSource ds = DataSourceBuilder.create().type(dataSourceType).build();
return ds;
}
/**
* 设置数据源路由,通过该类中的determineCurrentLookupKey决定使用哪个数据源
*/
@Bean
public AbstractRoutingDataSource routingDataSource() {
RoutingDataSource proxy = new RoutingDataSource();
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DbContextHolder.WRITE, writeDataSource());
targetDataSources.put(DbContextHolder.READ + "1", readDataSource_1());
proxy.setDefaultTargetDataSource(writeDataSource());
proxy.setTargetDataSources(targetDataSources);
return proxy;
}
/**
* 由于Spring容器中现在有多个数据源,所以我们需要为事务管理器和MyBatis手动指定一个明确的数据源。
*/
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean();
sqlSessionFactory.setDataSource(routingDataSource());
MybatisConfiguration configuration = new MybatisConfiguration();
configuration.setJdbcTypeForNull(JdbcType.NULL);
configuration.setMapUnderscoreToCamelCase(true);
configuration.setCacheEnabled(false);
sqlSessionFactory.setConfiguration(configuration);
return sqlSessionFactory.getObject();
}
@Bean
public DataSourceTransactionManager transactionManager() {
return new DataSourceTransactionManager(routingDataSource());
}
}
最后,我们需要使用AOP以注解方式切换只读数据库。具体实现如下:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadOnly {
}
@Aspect
@Component
@Log4j2
public class DataSourceAop implements Ordered {
@Around("@annotation(readOnly)")
public Object setRead(ProceedingJoinPoint joinPoint, ReadOnly readOnly) throws Throwable{
try {
DbContextHolder.setDbType(DbContextHolder.READ);
return joinPoint.proceed();
} finally {
//清除DbType一方面为了避免内存泄漏,更重要的是避免对后续在本线程上执行的操作产生影响
DbContextHolder.clearDbType();
log.info("清除threadLocal");
}
}
@Override
public int getOrder() {
return 0;
}
}
通过本文的介绍,我们了解了如何使用 Spring Boot + MyBatis Plus + MySQL 实现读写分离。读写分离可以有效地分担数据库的读写负载,提高数据库的性能和可用性。希望本文能对读写分离的实现有所帮助。