一、讀寫分離介紹
當(dāng)使用Spring Boot開發(fā)數(shù)據(jù)庫應(yīng)用時(shí),讀寫分離是一種常見的優(yōu)化策略。讀寫分離將讀操作和寫操作分別分配給不同的數(shù)據(jù)庫實(shí)例,以提高系統(tǒng)的吞吐量和性能。
讀寫分離實(shí)現(xiàn)主要是通過動(dòng)態(tài)數(shù)據(jù)源功能實(shí)現(xiàn)的,動(dòng)態(tài)數(shù)據(jù)源是一種通過在運(yùn)行時(shí)動(dòng)態(tài)切換數(shù)據(jù)庫連接的機(jī)制。它允許應(yīng)用程序根據(jù)不同的條件或配置選擇不同的數(shù)據(jù)源,以實(shí)現(xiàn)更靈活和可擴(kuò)展的數(shù)據(jù)庫訪問。
二、實(shí)現(xiàn)讀寫分離-基礎(chǔ)
1. 配置主數(shù)據(jù)庫和從數(shù)據(jù)庫的連接信息
# 主庫配置
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
2. 創(chuàng)建主數(shù)據(jù)庫和從數(shù)據(jù)庫的數(shù)據(jù)源配置類
通過不同的條件限制和配置文件前綴可以完成不同數(shù)據(jù)源的創(chuàng)建工作,不止是主從也可以是多個(gè)不同的數(shù)據(jù)庫
主庫數(shù)據(jù)源配置
@Configuration
@ConditionalOnProperty("spring.datasource.master.jdbc-url")
public class MasterDataSourceConfiguration {
@Bean("masterDataSource")
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
}
從庫數(shù)據(jù)源配置
@Configuration
@ConditionalOnProperty("spring.datasource.slave.jdbc-url")
public class SlaveDataSourceConfiguration {
@Bean("slaveDataSource")
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
}
3. 創(chuàng)建主從數(shù)據(jù)源枚舉
public enum DataSourceTypeEnum {
/**
* 主庫
*/
MASTER,
/**
* 從庫
*/
SLAVE,
;
}
4. 創(chuàng)建動(dòng)態(tài)路由數(shù)據(jù)源
這兒做了一個(gè)開關(guān),可以控制讀寫分離的開啟和關(guān)閉工作,可以講操作全部切換到主庫進(jìn)行。然后根據(jù)上下文中的數(shù)據(jù)源類型來返回不同的數(shù)據(jù)源類型枚舉
@Slf4j
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
@Value("${DB_RW_SEPARATE_SWITCH:false}")
private boolean dbRwSeparateSwitch;
@Override
protected Object determineCurrentLookupKey() {
if(dbRwSeparateSwitch && DataSourceTypeEnum.SLAVE.equals(DataSourceContextHolder.getDataSourceType())) {
log.info("DynamicRoutingDataSource 切換數(shù)據(jù)源到從庫");
return DataSourceTypeEnum.SLAVE;
}
log.info("DynamicRoutingDataSource 切換數(shù)據(jù)源到主庫");
// 根據(jù)需要指定當(dāng)前使用的數(shù)據(jù)源,這里可以使用ThreadLocal或其他方式來決定使用主庫還是從庫
return DataSourceTypeEnum.MASTER;
}
}
5. 創(chuàng)建動(dòng)態(tài)數(shù)據(jù)源配置類
將主數(shù)據(jù)庫和從數(shù)據(jù)庫的數(shù)據(jù)源添加到動(dòng)態(tài)數(shù)據(jù)源中,并可以通過枚舉創(chuàng)建一個(gè)數(shù)據(jù)源 map,這樣就可以通過上面的路由返回的枚舉來切換數(shù)據(jù)源
@Configuration
@ConditionalOnProperty("spring.datasource.master.jdbc-url")
public class DynamicDataSourceConfiguration {
@Bean("dataSource")
@Primary
public 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;
}
}
6. 創(chuàng)建DatasourceContextHolder類使用ThreadLocal存儲(chǔ)當(dāng)前線程的數(shù)據(jù)源類型
注意這兒有個(gè)潛在風(fēng)險(xiǎn)就是創(chuàng)建新的線程時(shí)會(huì)導(dǎo)致 ThreadLocal 中的數(shù)據(jù)無法正確讀取,如果涉及到在開啟新線程可以使用 TransmittableThreadLocal 來進(jìn)行父子線程數(shù)據(jù)的同步,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();
}
}
7. 創(chuàng)建自定義注解,用于標(biāo)記主和從數(shù)據(jù)源
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MasterDataSource {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface SlaveDataSource {
}
8. 創(chuàng)建切面類,攔截?cái)?shù)據(jù)庫操作,并根據(jù)注解設(shè)置切換數(shù)據(jù)源參數(shù)
@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();
}
}
9. 在Service層的方法上使用自定義注解標(biāo)記查詢數(shù)據(jù)源
@Service
public class TestService {
@Autowired
private TestDao testDao;
@SlaveDataSource
public Test test() {
return testDao.queryByPrimaryKey(11L);
}
}
10. 排除掉數(shù)據(jù)源自動(dòng)配置類
如果不排除自動(dòng)配置類會(huì)導(dǎo)致初始化多個(gè) dataSource 對(duì)象導(dǎo)致出現(xiàn)問題
SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
三、實(shí)現(xiàn)讀寫分離-進(jìn)階
1. 使用鏈接池,以Hikari為例
修改鏈接配置,加入鏈接池相關(guān)配置即可
# 主庫配置
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
2. 集成 mybatis 并在寫入時(shí)強(qiáng)制切換到主庫
不需要做任何配置,正常集成 mybatis 即可使用讀寫分離功能
可以通過 mybatis 的攔截器在寫入操作時(shí)強(qiáng)制切換到主庫
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
})
@Component
public class WriteInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 獲取 SQL 類型
DataSourceTypeEnum dataSourceType = DataSourceContextHolder.getDataSourceType();
if(DataSourceTypeEnum.SLAVE.equals(dataSourceType)) {
DataSourceContextHolder.setDataSourceType(DataSourceTypeEnum.MASTER);
}
try {
// 執(zhí)行 SQL
return invocation.proceed();
} finally {
// 恢復(fù)數(shù)據(jù)源 考慮到寫入后可能會(huì)反查,后續(xù)都走主庫
// DataSourceContextHolder.setDataSourceType(dataSourceType);
}
}
}
作者:京東健康?蘇曼文章來源:http://www.zghlxwxcb.cn/news/detail-746090.html
來源:京東云開發(fā)者社區(qū) 轉(zhuǎn)發(fā)請(qǐng)注明來源文章來源地址http://www.zghlxwxcb.cn/news/detail-746090.html
到了這里,關(guān)于SpringBoot 項(xiàng)目優(yōu)雅實(shí)現(xiàn)讀寫分離的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!