SpringBoot 項目實現讀寫分離
一、讀寫分離介紹
當使用Spring Boot開發數據庫應用時,讀寫分離是一種常見的優化策略。讀寫分離將讀操作和寫操作分別分配給不同的數據庫實例,以提高系統的吞吐量和性能。
讀寫分離實現主要是通過動態數據源功能實現的,動態數據源是一種通過在運行時動態切換數據庫連接的機制。它允許應用程序根據不同的條件或配置選擇不同的數據源,以實現更靈活和可擴展的數據庫訪問。
二、實現讀寫分離-基礎
- 配置主數據庫和從數據庫的連接信息
# 主庫配置
spring.datasource.master.jdbc-url=jdbc:mysql://ip:port/master?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false
spring.datasource.master.username=master
spring.datasource.master.password=123456
spring.datasource.master.driver-class-name=com.mysql.jdbc.Driver# 從庫配置
spring.datasource.slave.jdbc-url=jdbc:mysql://ip:port/slave?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false
spring.datasource.slave.username=slave
spring.datasource.slave.password=123456
spring.datasource.slave.driver-class-name=com.mysql.jdbc.Driver
- 創建主數據庫和從數據庫的數據源配置類
通過不同的條件限制和配置文件前綴可以完成不同數據源的創建工作,不止是主從也可以是多個不同的數據庫
主庫數據源配置
@Configuration
@ConditionalOnProperty("spring.datasource.master.jdbc-url")
public class MasterDataSourceConfiguration {@Bean("masterDataSource")@ConfigurationProperties(prefix = "spring.datasource.master")public DataSource masterDataSource() {return DataSourceBuilder.create().build();}
}
從庫數據源配置
@Configuration
@ConditionalOnProperty("spring.datasource.slave.jdbc-url")
public class SlaveDataSourceConfiguration {@Bean("slaveDataSource")@ConfigurationProperties(prefix = "spring.datasource.slave")public DataSource slaveDataSource() {return DataSourceBuilder.create().build();}
}
- 創建主從數據源枚舉
public enum DataSourceTypeEnum {/*** 主庫*/MASTER,/*** 從庫*/SLAVE,;}
- 創建動態路由數據源
這兒做了一個開關,可以控制讀寫分離的開啟和關閉工作,可以將操作全部切換到主庫進行。然后根據上下文中的數據源類型來返回不同的數據源類型枚舉
@Slf4j
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {@Value("${DB_RW_SEPARATE_SWITCH:false}")private boolean dbRwSeparateSwitch;@Overrideprotected Object determineCurrentLookupKey() {if(dbRwSeparateSwitch && DataSourceTypeEnum.SLAVE.equals(DataSourceContextHolder.getDataSourceType())) {log.info("DynamicRoutingDataSource 切換數據源到從庫");return DataSourceTypeEnum.SLAVE;}log.info("DynamicRoutingDataSource 切換數據源到主庫");// 根據需要指定當前使用的數據源,這里可以使用ThreadLocal或其他方式來決定使用主庫還是從庫return DataSourceTypeEnum.MASTER;}
}
- 創建動態數據源配置類
將主數據庫和從數據庫的數據源添加到動態數據源中,并可以通過枚舉創建一個數據源 map,這樣就可以通過上面的路由返回的枚舉來切換數據源
@Configuration
@ConditionalOnProperty("spring.datasource.master.jdbc-url")
public class DynamicDataSourceConfiguration {@Bean("dataSource")@Primarypublic DataSource dynamicDataSource(DataSource masterDataSource, DataSource slaveDataSource) {Map<Object, Object> targetDataSources = new HashMap<>();targetDataSources.put(DataSourceTypeEnum.MASTER, masterDataSource);targetDataSources.put(DataSourceTypeEnum.SLAVE, slaveDataSource);DynamicRoutingDataSource dynamicDataSource = new DynamicRoutingDataSource();dynamicDataSource.setTargetDataSources(targetDataSources);dynamicDataSource.setDefaultTargetDataSource(masterDataSource);return dynamicDataSource;}
}
- 創建DatasourceContextHolder類使用ThreadLocal存儲當前線程的數據源類型
注意這兒有個潛在風險就是創建新的線程時會導致 ThreadLocal 中的數據無法正確讀取,如果涉及到在開啟新線程可以使用 TransmittableThreadLocal 來進行父子線程數據的同步,git 地址:https://github.com/alibaba/transmittable-thread-local
public class DataSourceContextHolder {private static final ThreadLocal<DataSourceTypeEnum> contextHolder = new ThreadLocal<>();public static void setDataSourceType(DataSourceTypeEnum dataSourceType) {contextHolder.set(dataSourceType);}public static DataSourceTypeEnum getDataSourceType() {return contextHolder.get();}public static void clearDataSourceType() {contextHolder.remove();}
}
- 創建自定義注解,用于標記主和從數據源
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MasterDataSource {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface SlaveDataSource {
}
- 創建切面類,攔截數據庫操作,并根據注解設置切換數據源參數
@Aspect
@Component
public class DataSourceAspect {@Before("@annotation(xxx.MasterDataSource)")public void setMasterDataSource(JoinPoint joinPoint) {DataSourceContextHolder.setDataSourceType(DataSourceTypeEnum.MASTER);}@Before("@annotation(xxx.SlaveDataSource)")public void setSlaveDataSource(JoinPoint joinPoint) {DataSourceContextHolder.setDataSourceType(DataSourceTypeEnum.SLAVE);}@After("@annotation(xxx.MasterDataSource) || @annotation(xxx.SlaveDataSource)")public void clearDataSource(JoinPoint joinPoint) {DataSourceContextHolder.clearDataSourceType();}
}
- 在Service層的方法上使用自定義注解標記查詢數據源
@Service
public class TestService {@Autowiredprivate TestDao testDao;@SlaveDataSourcepublic Test test() {return testDao.queryByPrimaryKey(11L);}
}
- 排除掉數據源自動配置類
如果不排除自動配置類會導致初始化多個 dataSource 對象導致出現問題
SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
三、實現讀寫分離-進階
- 使用鏈接池,以Hikari為例
修改鏈接配置,加入鏈接池相關配置即可
# 主庫配置
spring.datasource.master.jdbc-url=jdbc:mysql://ip:port/master?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false
spring.datasource.master.username=master
spring.datasource.master.password=123456
spring.datasource.master.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.master.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.master.hikari.name=master
spring.datasource.master.hikari.minimum-idle=5
spring.datasource.master.hikari.idle-timeout=30
spring.datasource.master.hikari.maximum-pool-size=10
spring.datasource.master.hikari.auto-commit=true
spring.datasource.master.hikari.pool-name=DatebookHikariCP
spring.datasource.master.hikari.max-lifetime=1800000
spring.datasource.master.hikari.connection-timeout=30000
spring.datasource.master.hikari.connection-test-query=SELECT 1# 從庫配置
spring.datasource.slave.jdbc-url=jdbc:mysql://ip:port/slave?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false
spring.datasource.slave.username=root
spring.datasource.slave.password=123456
spring.datasource.slave.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.slave.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.slave.hikari.name=master
spring.datasource.slave.hikari.minimum-idle=5
spring.datasource.slave.hikari.idle-timeout=30
spring.datasource.slave.hikari.maximum-pool-size=10
spring.datasource.slave.hikari.auto-commit=true
spring.datasource.slave.hikari.pool-name=DatebookHikariCP
spring.datasource.slave.hikari.max-lifetime=1800000
spring.datasource.slave.hikari.connection-timeout=30000
spring.datasource.slave.hikari.connection-test-query=SELECT 1
- 集成 mybatis 并在寫入時強制切換到主庫
不需要做任何配置,正常集成 mybatis 即可使用讀寫分離功能
可以通過 mybatis 的攔截器在寫入操作時強制切換到主庫
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
})
@Component
public class WriteInterceptor implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {// 獲取 SQL 類型DataSourceTypeEnum dataSourceType = DataSourceContextHolder.getDataSourceType();if(DataSourceTypeEnum.SLAVE.equals(dataSourceType)) {DataSourceContextHolder.setDataSourceType(DataSourceTypeEnum.MASTER);}try {// 執行 SQLreturn invocation.proceed();} finally {// 恢復數據源 考慮到寫入后可能會反查,后續都走主庫// DataSourceContextHolder.setDataSourceType(dataSourceType);}}
}