SpringBoot + MyBatis 实现 MySQL 主从复制动态数据源切换
概述
在项目中的某些场景中,需要对数据库进行一些优化。常用的有如下的实现方法:读写分离、引入缓存技术、主从复制、分库分表等。今天来简单介绍一些如何在程序中实现动态切换数据源,可能某台服务器性能比较好,让流量多的方法执行切换到此数据源去操作等等。
当然这种思想也可以扩展实现为读写分离,主库(主数据源)只负责写操作,从库(从数据源)只负责读操作,前提是数据库之间需要提前搭建好主从复制环境。
接下来,我们就将总结一下如何在代码层面动态切换不同的数据源。
动态切换具体实现步骤
主要有以下几个步骤:
- 定义存放当前数据源的上下文环境;
- 定义数据源路由的配置;
- 定义数据源的配置;
- 自定义动态切换数据源的注解;
- 定义注解的AOP切面处理类;
- 定义业务类以及mapper接口、实现;
- 测试动态切换数据源;
项目结构
创建数据库
CREATE DATABASE yefeng;
USE yefeng;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4;
INSERT INTO yefeng values(1, 'xiaoming', 29), (2, '小天', 18);
导入Maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.2</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
配置yml文件
在application.yml
文件中配置主从数据源信息。
server:
port: 8080
spring:
datasource:
master:
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://192.168.1.1:3306/yefeng?useSSL=false&serverTimezone=UTC&autoReconnect=true&&failOverReadOnly=false
username: root
password: 123456
slave1:
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://192.168.1.2:3306/yefeng?useSSL=false&serverTimezone=UTC&autoReconnect=true&&failOverReadOnly=false
username: root
password: 123456
slave2:
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://192.168.1.3:3306/yefeng?useSSL=false&serverTimezone=UTC&autoReconnect=true&&failOverReadOnly=false
username: root
password: 123456
logging:
level:
com.yefeng.mapper: debug
mybatis:
configuration:
map-underscore-to-camel-case: false
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath:mapper/*.xml
创建 DBTypeEnum 枚举类型
创建DBTypeEnum 枚举类型,用于切换数据源时,确定连接的是那个数据源
/**
* @Author: 叶枫
* @Date: 2024/06/16/20:40
* @Description: 数据库枚举类
*/
public enum DBTypeEnum {
MASTER, // 主数据库类型
SLAVE1, // 从数据库类型1
SLAVE2 // 从数据库类型2
}
配置数据源上下文
用于保存和获取当前线程使用的数据源类型
/**
* @Author: 叶枫
* @Date: 2024/06/16/20:41
* @Description: 数据源上下文类
*/
@Slf4j
public class DBContext {
private static final ThreadLocal<DBTypeEnum> dbContext = new ThreadLocal<>();
private static final AtomicInteger counter = new AtomicInteger(-1);
public static void set(DBTypeEnum dbType) {
dbContext.set(dbType);
}
public static DBTypeEnum get() {
log.info("dbContext==>{}", dbContext.get());
return dbContext.get();
}
public static void master() {
set(DBTypeEnum.MASTER);
log.info("切换到master库");
}
public static void slave() {
// 读库负载均衡(轮询方式)
int index = counter.getAndIncrement() % 2;
log.info("slave库访问线程数==>{}", counter.get());
if (counter.get() > 9999) {
counter.set(-1);
}
if (index == 0) {
set(DBTypeEnum.SLAVE1);
log.info("切换到SLAVE1库");
} else {
set(DBTypeEnum.SLAVE2);
log.info("切换到SLAVE2库");
}
}
}
配置动态数据源
该类继承了 AbstractRoutingDataSource
,用于实现动态数据源的切换。
AbstractRoutingDataSource
是 Spring 框架提供的一个抽象基类,专门用于实现数据源的动态路由。这个类继承自 javax.sql.DataSource
,允许开发者根据当前的执行环境或者业务逻辑动态地切换到不同的数据源。
作用:
- 数据源动态切换:
AbstractRoutingDataSource
根据定义的路由规则(如当前的事务是否是只读事 务),决定使用哪一个数据源。这在实现多租户系统或读写分离时非常有用,因为它允许同一个应用动态地针对不同的数据库操作,选择不同的数据源。 - 简化配置:它使得配置多个数据源变得简单,可以在一个地方集中管理所有的数据源配置。
- 透明访问:应用代码不需要关心当前使用的是哪个数据源,数据源的选择对业务逻辑是透明的。
/**
* @Author: 叶枫
* @Date: 2024/06/16/20:40
* @Description: 路由数据源
*/
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DBContext.get();
}
}
配置数据源切换切面
/**
* @Author: 叶枫
* @Date: 2024/06/16/20:41
* @Description: 切面类DataSourceAop
*/
@Aspect
@Component
@Slf4j
public class DataSourceAop {
@Pointcut("@annotation(com.yefeng.annotation.Master) " +
"|| execution(* com.yefeng.service..*.insert*(..)) " +
"|| execution(* com.yefeng.service..*.create*(..)) " +
"|| execution(* com.yefeng.service..*.save*(..)) " +
"|| execution(* com.yefeng.service..*.add*(..)) " +
"|| execution(* com.yefeng.service..*.update*(..)) " +
"|| execution(* com.yefeng.service..*.edit*(..)) " +
"|| execution(* com.yefeng.service..*.delete*(..)) " +
"|| execution(* com.yefeng.service..*.remove*(..))")
public void writePointcut() {
}
@Pointcut("!@annotation(com.yefeng.annotation.Master) " +
"&& (execution(* com.yefeng.service..*.select*(..)) " +
"|| execution(* com.yefeng.service..*.get*(..)))" +
"|| execution(* com.yefeng.service..*.query*(..)))")
public void readPointcut() {
}
@Before("writePointcut()")
public void write() {
log.info("切换到master进行写入");
DBContext.master();
}
@Before("readPointcut()")
public void read() {
log.info("切换到slave进行读取");
DBContext.slave();
}
}
数据库配置类
该类将主库和从库的配置信息,以枚举为Key,配置信息为Value注入到HashMap当中,以便 RoutingDataSource 获取到数据库配置信息。
/**
* @Author: 叶枫
* @Date: 2024/06/16/20:41
* @Description: 数据库配置类
*/
@Configuration
public class DataSourceConfigs {
@Bean
@("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);
RoutingDataSource routingDataSource = new RoutingDataSource();
routingDataSource.setDefaultTargetDataSource(masterDataSource); // 设置默认数据源
routingDataSource.setTargetDataSources(targetDataSources);
return routingDataSource;
}
}
MyBatis配置类
@EnableTransactionManagement
@Configuration
@MapperScan("com.yefeng.mapper")
public class MyBatisConfig {
@Resource(name = "myRoutingDataSource")
private DataSource myRoutingDataSource;
// 配置 MyBatis 的 SqlSessionFactory
@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);
}
}
CRUD
将以上配置信息弄好之后就是简单的CRUD了
Controller
/**
* @Author: 叶枫
* @Date: 2024/06/16/21:06
* @Description:
*/
@RestController
@RequestMapping("/")
@RequiredArgsConstructor
public class UserController {
private final IUserService userService;
@RequestMapping("/list")
public List<User> index() {
return userService.selectList();
}
@Master
@RequestMapping("/list1")
public List<User> index1() {
return userService.selectList();
}
}
Service
public interface IUserService {
/**
* 插入一条记录
*
* @param entity 实体对象
*/
int insert(User entity);
/**
* 根据 ID 删除
*
* @param id 主键ID
*/
int deleteById(Serializable id);
/**
* 根据 ID 修改
*
* @param entity 实体对象
*/
int updateById(User entity);
/**
* 根据 ID 查询
*
* @param id 主键ID
*/
User selectById(Serializable id);
List<User> selectList();
}
@Service
public class UserServiceImpl implements IUserService {
@Resource
private UserMapper userMapper;
/**
* 插入一条记录
*
* @param entity 实体对象
*/
@Override
public int insert(User entity) {
return userMapper.insert(entity);
}
/**
* 根据 ID 删除
*
* @param id 主键ID
*/
@Override
public int deleteById(Serializable id) {
return userMapper.deleteById(id);
}
/**
* 根据 ID 修改
*
* @param entity 实体对象
*/
@Override
public int updateById(User entity) {
return userMapper.updateById(entity);
}
/**
* 根据 ID 查询
*
* @param id 主键ID
*/
@Master
@Override
public User selectById(Serializable id) {
return userMapper.selectById(id);
}
@Override
public List<User> selectList() {
return userMapper.selectList();
}
}
Mapper
@Repository
public interface UserMapper {
/**
* 插入一条记录
*
* @param entity 实体对象
*/
int insert(User entity);
/**
* 根据 ID 删除
*
* @param id 主键ID
*/
int deleteById(Serializable id);
/**
* 根据 ID 修改
*
* @param entity 实体对象
*/
int updateById(User entity);
/**
* 根据 ID 查询
*
* @param id 主键ID
*/
User selectById(Serializable id);
List<User> selectList();
}
<?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.yefeng.mapper.UserMapper">
<select id="selectById" resultType="com.yefeng.model.User" parameterType="int">
SELECT *
from user
WHERE id = #{id}
</select>
<select id="selectList" resultType="com.yefeng.model.User">
SELECT *
from user
</select>
<insert id="insert" parameterType="com.yefeng.model.User">
INSERT into user(id, name, age, email)
VALUES (#{id}, #{name}, #{age}, #{email})
</insert>
<update id="updateById" parameterType="com.yefeng.model.User">
UPDATE user
SET name =#{name},
age =#{age},
email =#{email}
WHERE id = #{id}
</update>
<delete id="deleteById" parameterType="int">
DELETE
FROM user
WHERE id = #{id}
</delete>
</mapper>
测试结果