读写分离就是对于一条SQL该选择哪一个数据库去执行,至于谁来做选择数据库这件事,主库一般用来执行“写”操作,从库用来执行“读”操作,从库可以有多个,主库从库之间的数据同步则是通过数据库间的异步线程进行通信。一般来说,读写分离有两种实现方式。第一种是依靠中间件MyCat,也就是说应用程序连接到中间件,中间件帮我们做SQL分离,去选择指定的数据源;第二种是应用程序自己去做分离。主要是利用Spring提供的路由数据源,以及AOP。
读写分离需要的基础环境的的搭建:Linux下MySQL实现主从复制
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!--SpringBoot集成Aop起步依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
spring:
datasource:
master:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://192.168.152.173:3306/yiyun?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT
username: root
password: 123456
slave1:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://192.168.152.174:3306/yiyun?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT
username: root
password: 123456
slave2:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://192.168.152.175:3306/yiyun?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT
username: root
password: 123456
props:
sql.show: true
masterslave:
# load-balance-algorithm-type是路由策略,round_robin表示轮询策略。
load-balance-algorithm-type: round_robin
# sharding.master-slave-rules是标明主库和从库,一定不要写错,否则写入数据到从库,就会导致无法同步。
sharding:
master-slave-rules:
master:
master-data-source-name: master
slave-data-source-names: slave1,slave2
新建一个枚举类型,枚举值我们定义为 MASTER, SLAVE1,SLAVE2
/**
* 主从类型
*
* @author: zhouzhou
* @date:2022/2/11 11:24
*/
public enum DataSourceEnum {
MASTER, SLAVE1 , SLAVE2;
}
/**
* @author: zhouzhou
* @date:2022/2/11 11:22
*
* 利用ThreadLocal封装的保存数据源上线的上下文context
*/
public class DataSourceContextHolder {
/**
* 确保创建的变量只能被同一个线程进行读和写的操作
*/
private static final ThreadLocal<DataSourceEnum> contextHolder = new ThreadLocal<>();
/**
* 原子操作类,确保线程安全
*/
private static final AtomicInteger counter = new AtomicInteger(-1);
/**
* 设置当前线程的枚举值
* @param dbType
*/
public static void set(DataSourceEnum dbType) {
contextHolder.set(dbType);
}
/**
* 获取当前线程的枚举值
* @return
*/
public static DataSourceEnum get() {
return contextHolder.get();
}
/**
* 切换到主库
*/
public static void master() {
set(DataSourceEnum.MASTER);
System.out.println("切换到master");
}
/**
* 切换到从库
*/
public static void slave() {
// 轮询 枚举值数量-1表示从库的数量
int index = counter.getAndIncrement() % (DataSourceEnum.values().length-1);
if (counter.get() > 9999) {
counter.set(-1);
}
set(DataSourceEnum.values()[index+1]);
System.out.println("切换到:"+DataSourceEnum.values()[index+1]);
}
/**
* 移除当前线程的枚举值
*/
public static void remove(){
contextHolder.remove();
}
}
然后新建一个叫DataSourceRouter的类,这个类需要继承AbstractRoutingDataSource这个抽象类,主要起路由到数据库的作用。在AbstractRoutingDataSource这个抽象类中,有一个叫targetDataSources的map,这里面可以存放我们的多个数据源。我们重写determineCurrentLookupKey方法,使用之前的DataSourceContextHolder去获取目前需要使用的是哪个库。
/**
* @author: zhouzhou
* @date:2022/2/11 11:21
*/
public class DataSourceRouter extends AbstractRoutingDataSource {
/**
* 最终的determineCurrentLookupKey返回的是从DataSourceContextHolder中拿到的,因此在动态切换数据源的时候注解
* 应该给DataSourceContextHolder设值
*
* @return
*/
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.get();
}
}
新建一个名叫DataSourceConfig的类,在这个类中,我们初始化三个数据库的DataSource,然后再将这三个DataSource加载到targetDataSources中。
/**
* 主从数据源配置
*
* @author: zhouzhou
* @date:2022/2/11 11:30
*/
@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("targetDataSources")
public DataSource myRoutingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("slave1DataSource") DataSource slave1DataSource,
@Qualifier("slave2DataSource") DataSource slave2DataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceEnum.MASTER, masterDataSource);
targetDataSources.put(DataSourceEnum.SLAVE1, slave1DataSource);
targetDataSources.put(DataSourceEnum.SLAVE2, slave1DataSource);
DataSourceRouter dataSourceRouter = new DataSourceRouter();
dataSourceRouter.setDefaultTargetDataSource(masterDataSource);
dataSourceRouter.setTargetDataSources(targetDataSources);
return dataSourceRouter;
}
}
Spring容器中现在有3个数据源。我们需要创建SqlSessionFactoryConfig配置类,去初始化sqlSessionFactory,同时将刚才加载的数据源targetDataSource自动注入进来,然后指定好mapper的路径,这样,我们的数据库加载就初始化完成了。
/**
* 由于Spring容器中现在有3个数据源,所以我们需要为事务管理器和MyBatis手动指定一个明确的数据源。
*
* @author: zhouzhou
* @date:2022/2/11 15:01
*/
@Configuration
@MapperScan(basePackages = "com.zsn.yiyun.mapper", sqlSessionFactoryRef = "sqlSessionFactory")
@EnableTransactionManagement
public class SqlSessionFactoryConfig {
@Resource(name = "targetDataSources")
private DataSource dataSource;
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
//设置mapper.xml文件所在位置
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:/mapper/*.xml"));
Objects.requireNonNull(sqlSessionFactoryBean.getObject()).getConfiguration().setMapUnderscoreToCamelCase(true);
return sqlSessionFactoryBean.getObject();
}
/**
* 事务管理器
* @return
*/
@Bean
public PlatformTransactionManager platformTransactionManager(){
return new DataSourceTransactionManager(dataSource);
}
}
AOP实现读写分离的方式就很多了,我们可以写注解@insert,@select,@update等等,通过注解去判断主从库,也可以直接通过方法名去判断。这里我们采用第二种方法去判断sql执行的数据库。
有一般情况就有特殊情况,特殊情况是某些情况下我们需要强制读主库,针对这种情况,我们定义一个主键,用该注解标注的就读主库
/**
* 特殊情况下我们需要强制读主库,针对这种情况,我们定义一个注解,用该注解标注的就读主库
*
* @author: zhouzhou
* @date:2022/2/11 14:34
*/
public @interface Master {
}
/**
* 默认情况下,所有的查询都走从库,插入/修改/删除走主库。我们通过方法名来区分操作类型(CRUD)
* @author: zhouzhou
* @date:2022/2/11 14:34
*/
@Aspect
@Component
public class DataSourceAop {
@Pointcut("!@annotation(com.zsn.yiyun.aop.datasource.Master) " +
"&& (execution(* com.zsn.yiyun.service.*.select*(..)) " +
"|| execution(* com.zsn.yiyun.service..*.find*(..)))")
public void readPointcut() {
}
@Pointcut("@annotation(com.zsn.yiyun.aop.datasource.Master) " +
"|| execution(* com.zsn.yiyun.service..*.save*(..)) " +
"|| execution(* com.zsn.yiyun.service..*.add*(..)) " +
"|| execution(* com.zsn.yiyun.service..*.update*(..)) " +
"|| execution(* com.zsn.yiyun.service..*.edit*(..)) " +
"|| execution(* com.zsn.yiyun..*.delete*(..)) " +
"|| execution(* com.zsn.yiyun..*.remove*(..))")
public void writePointcut() {
}
@Before("readPointcut()")
public void read() {
DataSourceContextHolder.slave();
}
@Before("writePointcut()")
public void write() {
DataSourceContextHolder.master();
}
}
至此,项目的基础搭建已基本完成,可实现读写分离。
参考:https://blog.csdn.net/mingwei_cheng/article/details/95232525