開發經驗總結: 讀寫分離簡單實現

背景

file

使用mysql的代理中間件,某些接口如果主從同步延遲大,容易出現邏輯問題。所以程序中沒有直接使用這個中間件。

依賴程序邏輯,如果有一些接口可以走讀庫,需要一個可以顯示指定讀庫的方式來連接讀庫,降低主庫的壓力。

所以需要一個能配置多個數據源,通過注解或者代碼方式設置讀還是寫數據源的方法。

實現方式

使用MybatisPlus的多數據源切換的能力。

github地址: https://github.com/baomidou/dynamic-datasource

已經封裝好了。直接使用就行。

介紹

dynamic-datasource-spring-boot-starter 是一個基于springboot的快速集成多數據源的啟動器。

其支持 Jdk 1.7+, SpringBoot 1.5.x 2.x.x 3.x.x
JPA用戶不建議使用,JPA自帶事務,無法連續切庫。

特性:

  • 支持 數據源分組 ,適用于多種場景 純粹多庫 讀寫分離 一主多從 混合模式。
  • 支持數據庫敏感配置信息 加密(可自定義) ENC()。
  • 支持每個數據庫獨立初始化表結構schema和數據庫database。
  • 支持無數據源啟動,支持懶加載數據源(需要的時候再創建連接)。
  • 支持 自定義注解 ,需繼承DS(3.2.0+)。
  • 提供并簡化對Druid,HikariCp,BeeCp,Dbcp2的快速集成。
  • 提供對Mybatis-Plus,Quartz,ShardingJdbc,P6sy,Jndi等組件的集成方案。
  • 提供 自定義數據源來源 方案(如全從數據庫加載)。
  • 提供項目啟動后 動態增加移除數據源 方案。
  • 提供Mybatis環境下的 純讀寫分離 方案。
  • 提供使用 spel動態參數 解析數據源方案。內置spel,session,header,支持自定義。
  • 支持 多層數據源嵌套切換 。(ServiceA >>> ServiceB >>> ServiceC)。
  • 提供 基于seata的分布式事務方案
  • 提供 本地多數據源事務方案。

我們需要的是純讀寫分離 方案,它已經內置了。

功能范圍:

  1. 本框架只做 切換數據源 這件核心的事情,并不限制你的具體操作,切換了數據源可以做任何CRUD。
  2. 配置文件所有以下劃線 分割的數據源 首部 即為組的名稱,相同組名稱的數據源會放在一個組下。
  3. 切換數據源可以是組名,也可以是具體數據源名稱。組名則切換時采用負載均衡算法切換。
  4. 默認的數據源名稱為 master ,你可以通過 spring.datasource.dynamic.primary 修改。
  5. 方法上的注解優先于類上注解。
  6. DS支持繼承抽象類上的DS,暫不支持繼承接口上的DS。

使用步驟

1.引入依賴。

<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
  <version>${version}</version>
</dependency>

2.配置數據源

spring:
  datasource:
    dynamic:
      primary: master #設置默認的數據源或者數據源組,默認值即為master
      strict: false #嚴格匹配數據源,默認false. true未匹配到指定數據源時拋異常,false使用默認數據源
      datasource:
        master:
          url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic
          username: root
          password: 123456
          driver-class-name: com.mysql.jdbc.Driver # 3.2.0開始支持SPI可省略此配置
        slave_1:
          url: jdbc:mysql://xx.xx.xx.xx:3307/dynamic
          username: root
          password: 123456
          driver-class-name: com.mysql.jdbc.Driver
        slave_2:
          url: ENC(xxxxx) # 內置加密,使用請查看詳細文檔
          username: ENC(xxxxx)
          password: ENC(xxxxx)
          driver-class-name: com.mysql.jdbc.Driver
       #......省略
       #以上會配置一個默認庫master,一個組slave下有兩個子庫slave_1,slave_2

3.使用注解切換數據源。

@DS 可以注解在方法上或類上,同時存在就近原則 方法上注解 優先于 類上注解

file
@Service
@DS("slave")
public class UserServiceImpl implements UserService {

  @Autowired
  private JdbcTemplate jdbcTemplate;

  public List selectAll() {
    return  jdbcTemplate.queryForList("select * from user");
  }
  
  @Override
  @DS("slave_1")
  public List selectByCondition() {
    return  jdbcTemplate.queryForList("select * from user where age >10");
  }
}

業務集成

我們的程序使用的經典的三層結構,即MVC .

畫板

看到我們的框架是已經集成了dynamicSource的。如果沒有,則加一下依賴即可。

以crm服務為例子。寫了一些測試代碼,進行驗證測試。

package com.pig4cloud.pigx.crm.controller;


import com.baomidou.dynamic.datasource.annotation.DS;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.common.security.annotation.Inner;
import com.pig4cloud.pigx.crm.mapper.AiAgentMapper;
import com.pig4cloud.pigx.crm.service.AiAgentService;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequiredArgsConstructor
@Tag(description = "test read write", name = "測試讀寫分離")
@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
@Valid
@Slf4j
public class TestDsController {

    private final AiAgentMapper aiAgentMapper;

    private final AiAgentService aiAgentService;

    @GetMapping("/testD")
    @ResponseBody
    //@Inner(value = false)
    public R testD() {
        return R.ok(aiAgentMapper.selectList(null));
    }


    @GetMapping("/testRead")
    @ResponseBody
    //@Inner(value = false)
    @DS("read")
    public R testRead() {
        return R.ok(aiAgentMapper.selectList(null));
    }

    @GetMapping("/testMaster")
    @ResponseBody
    //@Inner(value = false)
    @DS("master")
    public R testMaster() {
        return R.ok(aiAgentMapper.selectList(null));
    }

    @GetMapping("/testHe")
    @ResponseBody
    //@Inner(value = false)
    @DS("master")
    public R testHe() {
        return R.ok(aiAgentService.listTest());
    }

    @GetMapping("/testHe2")
    @ResponseBody
    //@Inner(value = false)
    @DS("read")
    public R testHe2() {
        return R.ok(aiAgentService.listTest2());
    }


    @GetMapping(value = "/testHe3",produces = "application/json")
    @ResponseBody
    //@Inner(value = false)
    public R testHe3() {
        Map<String, Object> dataMap = new HashMap<>();

        dataMap.put("read", aiAgentService.listTest());
        dataMap.put("master", aiAgentService.listTest2());

        return R.ok(dataMap);
    }


}

對應的Service實現代碼:

@Service
@Slf4j
@RequiredArgsConstructor
public class AiAgentServiceImpl extends ServiceImpl<AiAgentMapper, AiAgentEntity> implements AiAgentService {

    @Override
    @DS("read")
    public List<AiAgentEntity> listTest() {
        return baseMapper.selectList(null);
    }

    @Override
    @DS("master")
    public List<AiAgentEntity> listTest2() {
        return baseMapper.selectList(null);
    }
}

測試用例和結果:

測試接口和場景 測試說明 預期結果 測試結果
/testD 不加任何注解 走主庫 - [x] 走主庫 ![]
file
/testRead 添加了讀庫注解 走讀庫 - [x] 走讀庫
file
/testMaster 添加了主庫注解 走主庫 - [x] 走主庫
file
/testHe 上層加主庫注解,下層加讀庫注解 走讀庫 - [x] 走讀庫
/testHe2 上層加讀庫注解,下層加寫庫注解 走主庫 - [x] 走主庫
/testHe3 在中層的方法分別加讀庫和寫庫注解 分別走讀庫和主庫 - [x] 分別走讀庫和讀庫
file

目前需要增加讀寫分離的服務。

服務 增加讀寫分離的原因 是否完成增加
crm 后臺列表查詢頁面, openAPI的讀接口,直接走讀庫; - [x]
email 后臺列表查詢頁面, openAPI的讀接口,直接走讀庫; - [ ]
phone 后臺列表查詢頁面, openAPI的讀接口,直接走讀庫; - [ ]
ssm 后臺列表查詢頁面, openAPI的讀接口,直接走讀庫; - [ ]
upms 后臺列表查詢頁面, openAPI的讀接口,直接走讀庫; - [ ]

源碼研究

3.6.0版本。基于這個進行分析。

1 自動裝配類

file

配置屬性:DynamicDataSourceProperties

屬性 說明
primary 默認的庫
strict 嚴格檢查
p6spy 日志輸出
seata 是否開啟seata
lazy 是否懶加載數據源
setaMode AT模式
<font style="color:#9876aa;background-color:#2b2b2b;">publicKey 加密key
datasource 數據源,可以單獨配置;
strategy 負載均衡策略
druid 數據源
hikari 數據源
beecp 數據源
dbcp2 數據源
aop aop配置,有默認值

DataSourceProperty

屬性說明 說明
poolName 連接池名稱
type 連接類型
driverClassName 驅動名稱
url 連接url
username 用戶名
password 密碼
jndiName jndi中的名稱
init 初始化配置
p6spy 日志輸出
seata 是否開啟seata
lazy 是否懶加載數據源
<font style="color:#9876aa;background-color:#2b2b2b;">publicKey 加密key
druid 數據源
hikari 數據源
beecp 數據源
dbcp2 數據源

DatasourceInitProperties

屬性名 說明
schema 建表腳本
data 數據腳本
<font style="color:#9876aa;background-color:#2b2b2b;">continueOnError 錯誤是否繼續
<font style="color:#9876aa;background-color:#2b2b2b;">separator 分隔符

2 自動裝配邏輯

file

基于切面實現。

小結

集成使用了mybatisPlus自帶的多數據源實現了讀寫分離,并探究了源碼實現過程。

詳細過程可以看我的視頻號。

原創不易,關注誠可貴,轉發價更高!轉載請注明出處,讓我們互通有無,共同進步,歡迎溝通交流。
我會持續分享Java軟件編程知識和程序員發展職業之路,歡迎關注!

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容