一、实现方式方式
读写分离要做的事情就是对于一条Sql语句,该去那个数据库执行;至于谁来做这件时间,无非有两种方式
- 中间件:我们可以使用中间来帮助我们路由这些SQL语句,你如说使用
MyCat
,MyCat启动以后就好像启动可以一个数据库一样,但是他不做数据库的的事情,他负责将不同的SQL语句发送到不同的数据库去执行; - 程序自己做:就是在程序执行某个方法的时候,通过切面的方式,进行修改这个方法所需要的数据连接。
在这里我们采用后者,因为前者,主要使用spring的路由数据源和AOP完成。
二、配置方式
1. 配置数据源
spring:
datasource:
master:
jdbc-url: jdbc:mysql://192.168.141.128:3306/my_db
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
slave1:
jdbc-url: jdbc:mysql://192.168.141.129:3306/my_db
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
slave2:
jdbc-url: jdbc:mysql://192.168.141.130:3306/my_db
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
2. 配置数据链接
@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;
}
}
3. MyBatis配置
@EnableTransactionManagement
@Configuration
public class MyBatisConfig {
@Resource(name = "myRoutingDataSource")
private DataSource myRoutingDataSource;
/**
* 配置sqlSession工厂
* @return
* @throws Exception
*/
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(myRoutingDataSource);
sqlSessionFactoryBean.setMapperLocations();
// 如果使用mapper.xml文件则需要配置此项
// sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
return sqlSessionFactoryBean.getObject();
}
/**
* 配置事务
* @return
*/
@Bean
public PlatformTransactionManager platformTransactionManager() {
return new DataSourceTransactionManager(myRoutingDataSource);
}
}
4. 使用枚举类型用做路由key
public enum DBTypeEnum {
MASTER, SLAVE1, SLAVE2;
}
5. 通过ThreadLocal将数据源设置到每个线程上下文中
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");
}
}
}
6. 继承AbstractRoutingDataSource,获取路由key
public class MyRoutingDataSource extends AbstractRoutingDataSource {
@Nullable
@Override
protected Object determineCurrentLookupKey() {
return DBContextHolder.get();
}
}
7. 使用aop,设置路由key
@Aspect
@Component
public class DataSourceAop {
/**
* 设置读切面
* 读取某包下以select和get开头,以及带有Master注解的方法
*/
@Pointcut("!@annotation(com.simple.mysql.router.config.Master) " +
"&& (execution(* com.simple.mysql.router.service..*.select*(..)) " +
"|| execution(* com.simple.mysql.router.service..*.get*(..)))")
public void readPointcut() {}
/**
* 设置写切面
* 读取某包下以insert、add、update、edit、delete、remove开头的方法,以及使用Master注解的方法
*/
@Pointcut("@annotation(com.simple.mysql.router.config.Master) " +
"|| execution(* com.simple.mysql.router.service..*.insert*(..)) " +
"|| execution(* com.simple.mysql.router.service..*.add*(..)) " +
"|| execution(* com.simple.mysql.router.service..*.update*(..)) " +
"|| execution(* com.simple.mysql.router.service..*.edit*(..)) " +
"|| execution(* com.simple.mysql.router.service..*.delete*(..)) " +
"|| execution(* com.simple.mysql.router.service..*.remove*(..))")
public void writePointcut() {}
@Before("readPointcut()")
public void read() {
DBContextHolder.slave();
}
@Before("writePointcut()")
public void write() {
DBContextHolder.master();
}
/**
* 另一种写法:if...else... 判断哪些需要读从数据库,其余的走主数据库
*/
// @Before("execution(* com.cjs.example.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. 补充一个Master注解
public @interface Master {
}
到这里,配置就结束了,可以启动项目看一下了,不过前提是配置过主从复制的哦。