前言
实现mysql读写分离有两种,一种是基于代码实现(spring提供的抽象类AbstractRoutingDataSource),另外一种是基于中间件实现(MySQL Proxy、Mycat和Atlas等),本次使用第一种方式实现;
准备
安装mysql参考:centos7.5手动安装mysql5.6
主从复制配置参考:mysql主从复制配置
MySQL版本:mysql5.6
使用三台虚拟机:
192.168.1.111 master
192.168.1.112 slave
192.168.1.113 slave
pom文件
spring-boot-starter-parent版本2.1.0.RELEASE
org.springframework.boot
spring-boot-starter-web
org.projectlombok
lombok
org.springframework.boot
spring-boot-starter-aop
org.springframework.boot
spring-boot-starter-jdbc
org.springframework.boot
spring-boot-starter-web
org.mybatis.spring.boot
mybatis-spring-boot-starter
1.3.2
mysql
mysql-connector-java
org.apache.commons
commons-lang3
tk.mybatis
mapper-spring-boot-starter
2.1.4
com.alibaba
fastjson
1.2.51
配置文件application.yml
server:
port: 8088
spring:
datasource:
master:
jdbc-url: jdbc:mysql://192.168.1.111:3306/test
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
slave1:
jdbc-url: jdbc:mysql://192.168.1.112:3306/test
username: read
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
slave2:
jdbc-url: jdbc:mysql://192.168.1.113:3306/test
username: read
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
mapper:
mappers: tk.mybatis.mapper.common.Mapper,tk.mybatis.mapper.common.IdsMapper
identity: MYSQL
定义数据源全局持有对象(用于主从切换)
@Slf4j
public class DBContextHolder {
private static final ThreadLocal contextHolder = new ThreadLocal<>();
private static final AtomicInteger counter = new AtomicInteger(-1);
private static final int INCREMENT_MAX_COUNT = 9999;
public static void set(DBTypeEnum dbType) {
contextHolder.set(dbType);
}
public static DBTypeEnum get() {
return contextHolder.get();
}
public static void switchMaster() {
set(DBTypeEnum.MASTER);
log.info("数据源切换到master");
}
/**
* 两个slave轮询
*/
public static void switchSlave() {
int index = counter.getAndIncrement() % 2;
if (counter.get() > INCREMENT_MAX_COUNT) {
counter.set(-1);
}
if (index == 0) {
set(DBTypeEnum.SLAVE1);
log.info("数据源切换到slave1");
} else {
set(DBTypeEnum.SLAVE2);
log.info("数据源切换到slave2");
}
}
}
继承AbstractRoutingDataSource,定义路由数据源
@Slf4j
public class RoutingDataSource extends AbstractRoutingDataSource {
@Nullable
@Override
protected Object determineCurrentLookupKey() {
log.info("当前数据源key: {}", DBContextHolder.get());
return DBContextHolder.get();
}
}
数据源配置
@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();
}
/**
* 由上面的三个数据源组成的一个路由数据源,也就是说后面真正使用的其实是这个路由数据源
* @param masterDataSource master数据源
* @param slave1DataSource slave1数据源
* @param slave2DataSource slave2数据源
* @return
*/
@Bean
public DataSource routingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("slave1DataSource") DataSource slave1DataSource,
@Qualifier("slave2DataSource") DataSource slave2DataSource) {
Map 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使用路由数据源
@Configuration
@EnableTransactionManagement
public class TkMybatisConfig {
@Resource(name = "routingDataSource")
private DataSource routingDataSource;
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(routingDataSource);
return sqlSessionFactoryBean.getObject();
}
@Bean
public PlatformTransactionManager platformTransactionManager() {
return new DataSourceTransactionManager(routingDataSource);
}
}
定义master注解,用于强制使用主数据源
public @interface Master {
}
基于注解定义aop切点和切面
@Aspect
@Component
public class DataSourceAspect {
/**
* master注解的切入点(强制使用主数据源)
*/
@Pointcut("@annotation(com.felix.learn.datasource.annotation.Master)")
public void masterPointcut() {}
/**
* 读操作切入点
*/
@Pointcut("(execution(* com.felix.learn.datasource.service..*.select*(..)) " +
"|| execution(* com.felix.learn.datasource.service..*.get*(..)) " +
"|| execution(* com.felix.learn.datasource.service..*.find*(..)))")
public void readPointcut() {}
/**
* 写操作切入点
*/
@Pointcut("execution(* com.felix.learn.datasource.service..*.insert*(..)) " +
"|| execution(* com.felix.learn.datasource.service..*.add*(..)) " +
"|| execution(* com.felix.learn.datasource.service..*.update*(..)) " +
"|| execution(* com.felix.learn.datasource.service..*.edit*(..)) " +
"|| execution(* com.felix.learn.datasource.service..*.delete*(..)) " +
"|| execution(* com.felix.learn.datasource.service..*.remove*(..))")
public void writePointcut() {}
@Before("!masterPointcut() && readPointcut()")
public void read() {
DBContextHolder.switchSlave();
}
@Before("masterPointcut() || writePointcut()")
public void write() {
DBContextHolder.switchMaster();
}
}
写例子测试(只展示service层)
@Service
public class StudentServiceImpl implements StudentService {
@Resource
private StudentMapper studentMapper;
@Override
public Student findById(Integer id) {
return studentMapper.selectByPrimaryKey(id);
}
@Override
public int insertStudent(Student student) {
studentMapper.insertSelective(student);
return student.getId();
}
@Override
public int deleteStudent(Integer id) {
return studentMapper.deleteByPrimaryKey(id);
}
@Override
public int updateStudent(Student student) {
return studentMapper.updateByPrimaryKeySelective(student);
}
@Master
@Override
public List selectList() {
List students = studentMapper.selectAll();
return students;
}
}