數據庫的讀寫分離的好處有哪些?
1)將讀操作和寫操作分離到不同的數據庫上,避免主服務器出現性能瓶頸;
2) 主服務器進行寫操作時,不影響查詢應用服務器的查詢性能,降低阻塞,提高併發;
3) 數據擁有多個容災副本,提高數據安全性,同時當主服務器故障時,可立即切換到其他服務器,提高系統可用性;
說到mysql數據庫主從分離,需要注意以下3點:
1.主從數據庫數據一致(主從同步)
2.增刪改走主庫
3.查詢走從庫
一般來講,讀寫分離有兩種實現方式;第一種是依靠中間件(比如:MyCat,sharding),第二種是應用程序自己去做分離。這裏我們選擇程序自己來做,主要是利用Spring提供的路由數據源,以及AOP。
項目目錄結構:
首先引入相關依賴:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.1</version>
</dependency>
數據源配置類
@Configuration
public class DynamicDataSourceConfig {
private Logger logger = LogManager.getLogger(DynamicDataSourceConfig.class);
@Bean
@ConfigurationProperties("spring.datasource.druid.master")
public DataSource masterDataSource() {
logger.info("into master data source");
return DruidDataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.druid.slave")
public DataSource slaveDataSource() {
logger.info("into slave data source");
return DruidDataSourceBuilder.create().build();
}
@Bean
@Primary
public DataSource myRoutingDataSource() {
logger.info("into my routing data source");
Map<Object, Object> targetDataSources = new HashMap<>(16);
targetDataSources.put(DBTypeEnum.MASTER, masterDataSource());
targetDataSources.put(DBTypeEnum.SLAVE, slaveDataSource());
MyRoutingDataSource myRoutingDataSource = new MyRoutingDataSource();
myRoutingDataSource.setDefaultTargetDataSource(masterDataSource());
myRoutingDataSource.setTargetDataSources(targetDataSources);
return myRoutingDataSource;
}
sqlSessionFactory配置類
@EnableTransactionManagement
@Configuration
public class MybatisConfig {
@Resource
private DataSource myRoutingDataSource;
@Value("${mybatis.type.aliases.package}")
private String typeAliasPackage;
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
sqlSessionFactoryBean.setDataSource(myRoutingDataSource);
sqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath:mapper/*.xml"));
sqlSessionFactoryBean.setConfigLocation(resolver.getResource("classpath:mybatis-config.xml"));
sqlSessionFactoryBean.setTypeAliasesPackage(typeAliasPackage);
return sqlSessionFactoryBean.getObject();
}
/**
* 創建SqlSessionTemplate對象
*
* @param sqlSessionFactory 創建SqlSessionTemplate所需的SessionFactory對象
* @return 創建成功的SqlSessionTemplate對象
*/
@Bean
public SqlSessionTemplate createSqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
/**
* 創建事務用的Transaction Manager對象
*
* @return 創建成功的Transaction Manager對象
*/
@Bean
public PlatformTransactionManager createTransactionManager() {
return new DataSourceTransactionManager(myRoutingDataSource);
}
}
將數據源設置到本地線程ThreadLocal中
/**
* 通過ThreadLocal將數據源設置到每個線程上下文中
*/
public class DBContextHolder {
private static final ThreadLocal<DBTypeEnum> contextHolder = new ThreadLocal<>();
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() {
set(DBTypeEnum.SLAVE);
System.out.println("切換到slave");
}
}
AOP切面類
@Aspect
@Order(1)
@Component
public class DataSourceAop {
@Pointcut("!@annotation(com.carrefour.cn.cdm.permission.annotation.Master) " +
"&& execution(* com.carrefour.cn.cdm.permission.service..*.select*(..)) " +
"|| execution(* com.carrefour.cn.cdm.permission.service..*.find*(..)) " +
"|| execution(* com.carrefour.cn.cdm.permission.service..*.get*(..)))")
public void readPointcut() {
}
@Pointcut("@annotation(com.carrefour.cn.cdm.permission.annotation.Master) " +
"|| execution(* com.carrefour.cn.cdm.permission.service..*.insert*(..)) " +
"|| execution(* com.carrefour.cn.cdm.permission.service..*.add*(..)) " +
"|| execution(* com.carrefour.cn.cdm.permission.service..*.create*(..)) " +
"|| execution(* com.carrefour.cn.cdm.permission.service..*.update*(..)) " +
"|| execution(* com.carrefour.cn.cdm.permission.service..*.edit*(..)) " +
"|| execution(* com.carrefour.cn.cdm.permission.service..*.delete*(..)) " +
"|| execution(* com.carrefour.cn.cdm.permission.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();
// }
// }
}
新建一個Master註解類
如果需要查詢走主庫,可以直接在service層的方法上加上@Master即可。
單元測試結果
1.查詢由從庫切換爲主庫
2.查詢走從庫
3.新增/修改/刪除走主庫(修改、刪除這裏就不一一測試了)
至此,mysql數據庫主從分離就完成了。