一、實現方式方式
讀寫分離要做的事情就是對於一條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 {
}
到這裏,配置就結束了,可以啓動項目看一下了,不過前提是配置過主從複製的哦。