SpringBoot 專案優雅實現讀寫分離

ITPUB社群發表於2023-11-22

來源:京東雲開發者


一、讀寫分離介紹

當使用Spring Boot開發資料庫應用時,讀寫分離是一種常見的最佳化策略。讀寫分離將讀操作和寫操作分別分配給不同的資料庫例項,以提高系統的吞吐量和效能。
讀寫分離實現主要是透過動態資料來源功能實現的,動態資料來源是一種透過在執行時動態切換資料庫連線的機制。它允許應用程式根據不同的條件或配置選擇不同的資料來源,以實現更靈活和可擴充套件的資料庫訪問。


二、實現讀寫分離-基礎

1. 配置主資料庫和從資料庫的連線資訊












# 主庫配置spring.datasource.master.jdbc-url=jdbc:mysql://ip:port/master?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=falsespring.datasource.master.username=masterspring.datasource.master.password=123456spring.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=falsespring.datasource.slave.username=slavespring.datasource.slave.password=123456spring.datasource.slave.driver-class-name=com.mysql.jdbc.Driver

2. 建立主資料庫和從資料庫的資料來源配置類

透過不同的條件限制和配置檔案字首可以完成不同資料來源的建立工作,不止是主從也可以是多個不同的資料庫
主庫資料來源配置

@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();    }}

3. 建立主從資料來源列舉














public enum DataSourceTypeEnum {    /**     * 主庫     */    MASTER,
   /**     * 從庫     */    SLAVE,   ;  }

4. 建立動態路由資料來源

這兒做了一個開關,可以控制讀寫分離的開啟和關閉工作,可以將操作全部切換到主庫進行。然後根據上下文中的資料來源型別來返回不同的資料來源型別列舉

















@Slf4jpublic 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 切換資料來源到從庫");            return DataSourceTypeEnum.SLAVE;        }        log.info("DynamicRoutingDataSource 切換資料來源到主庫");        // 根據需要指定當前使用的資料來源,這裡可以使用ThreadLocal或其他方式來決定使用主庫還是從庫        return DataSourceTypeEnum.MASTER;    }}

5. 建立動態資料來源配置類

將主資料庫和從資料庫的資料來源新增到動態資料來源中,並可以透過列舉建立一個資料來源 map,這樣就可以透過上面的路由返回的列舉來切換資料來源

















@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. 建立DatasourceContextHolder類使用ThreadLocal儲存當前執行緒的資料來源型別

注意這兒有個潛在風險就是建立新的執行緒時會導致 ThreadLocal 中的資料無法正確讀取,如果涉及到在開啟新執行緒可以使用 TransmittableThreadLocal 來進行父子執行緒資料的同步,git 地址:
















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. 建立自定義註解,用於標記主和從資料來源

@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)public @interface MasterDataSource {}@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)public @interface SlaveDataSource {}

8. 建立切面類,攔截資料庫操作,並根據註解設定切換資料來源引數




















@Aspect@Componentpublic 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層的方法上使用自定義註解標記查詢資料來源











@Servicepublic class TestService {    @Autowired    private TestDao testDao;
   @SlaveDataSource    public Test test() {        return testDao.queryByPrimaryKey(11L);    }}

10. 排除掉資料來源自動配置類

如果不排除自動配置類會導致初始化多個 dataSource 物件導致出現問題

SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})



三、實現讀寫分離-進階

1. 使用連結池,以Hikari為例

修改連結配置,加入連結池相關配置即可
































# 主庫配置spring.datasource.master.jdbc-url=jdbc:mysql://ip:port/master?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=falsespring.datasource.master.username=masterspring.datasource.master.password=123456spring.datasource.master.driver-class-name=com.mysql.jdbc.Driverspring.datasource.master.type=com.zaxxer.hikari.HikariDataSourcespring.datasource.master.hikari.name=masterspring.datasource.master.hikari.minimum-idle=5spring.datasource.master.hikari.idle-timeout=30spring.datasource.master.hikari.maximum-pool-size=10spring.datasource.master.hikari.auto-commit=truespring.datasource.master.hikari.pool-name=DatebookHikariCPspring.datasource.master.hikari.max-lifetime=1800000spring.datasource.master.hikari.connection-timeout=30000spring.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=falsespring.datasource.slave.username=rootspring.datasource.slave.password=123456spring.datasource.slave.driver-class-name=com.mysql.jdbc.Driverspring.datasource.slave.type=com.zaxxer.hikari.HikariDataSourcespring.datasource.slave.hikari.name=masterspring.datasource.slave.hikari.minimum-idle=5spring.datasource.slave.hikari.idle-timeout=30spring.datasource.slave.hikari.maximum-pool-size=10spring.datasource.slave.hikari.auto-commit=truespring.datasource.slave.hikari.pool-name=DatebookHikariCPspring.datasource.slave.hikari.max-lifetime=1800000spring.datasource.slave.hikari.connection-timeout=30000spring.datasource.slave.hikari.connection-test-query=SELECT 1

2. 整合 mybatis 並在寫入時強制切換到主庫

不需要做任何配置,正常整合 mybatis 即可使用讀寫分離功能
可以透過 mybatis 的攔截器在寫入操作時強制切換到主庫

@Intercepts({        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),})@Componentpublic 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 {            // 執行 SQL            return invocation.proceed();        } finally {            // 恢復資料來源  考慮到寫入後可能會反查,後續都走主庫            // DataSourceContextHolder.setDataSourceType(dataSourceType);        }    }}

-end-

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024420/viewspace-2996449/,如需轉載,請註明出處,否則將追究法律責任。

相關文章