SpringBoot 玩轉讀寫分離

環境概覽

框架 版本號
Spring Boot 1.5.12.RELEASE
Sharding-JDBC 2.0.3
MyBatis-Plus 2.2.0

前言介紹

Sharding-JDBC是噹噹網的一個開源項目,只需引入jar便可輕鬆實現讀寫分離與分庫分表。與MyCat不一樣的是,Sharding-JDBC致力於提供輕量級的服務框架,無需額外部署,底層是對JDBC進行加強,兼容各類鏈接池和ORM框架。不只如此還提供分佈式事務及分佈式治理功能,即將出世的3.X版本可能會提供更加全面的功能。有興趣的小夥伴們,能夠去了解下,這裏提供官方文檔GitHub地址碼雲地址html

讀寫分離

引自Sharding-JDBC官方文檔java

面對日益增長的系統訪問量,數據庫的吞吐量面臨着巨大瓶頸。 對於同一時間有大量併發讀操做和較少寫操做類型的應用系統來講,將單一的數據庫拆分爲主庫和從庫,主庫負責處理事務性的增刪改操做,從庫負責處理查詢操做,可以有效的避免由數據更新致使的行鎖,使得整個系統的查詢性能獲得極大的改善。 經過一主多從的配置方式,能夠將查詢請求均勻的分散到多個數據副本,可以進一步的提高系統的處理能力。 使用多主多從的方式,不但可以提高系統的吞吐量,還可以提高系統的可用性,能夠達到在任何一個數據庫宕機,甚至磁盤物理損壞的狀況下仍然不影響系統的正常運行。mysql

雖然讀寫分離能夠提高系統的吞吐量和可用性,但同時也帶來了數據不一致的問題,這包括多個主庫之間的數據一致性,以及主庫與從庫之間的數據一致性的問題。而且,讀寫分離也帶來了與數據分片一樣的問題,它一樣會使得應用開發和運維人員對數據庫的操做和運維變得更加複雜。透明化讀寫分離所帶來的影響,讓使用方儘可能像使用一個數據庫同樣使用主從數據庫,是讀寫分離中間件的主要功能。git

讀寫分離,簡單來講,就是將DML交給主數據庫去執行,將更新結果同步至各個從數據庫保持主從數據一致,DQL分發給從數據庫去查詢,從數據庫只提供讀取查詢操做。讀寫分離特別適用於讀多寫少的場景下,經過分散讀寫到不一樣的數據庫實例上來提升性能,緩解單機數據庫的壓力。github

這裏解釋一下什麼是DML和DQL?SQL語言四大分類:DQL、DML、DDL、DCL。算法

  • DQL(Data QueryLanguage):數據查詢語言,好比select查詢語句
  • DML(Data Manipulation Language):數據操縱語言,好比insert、delete、update更新語句
  • DDL():數據定義語言,好比create/drop/alter等語句
  • DCL():數據控制語言,好比grant/rollback/commit等語句

實現步驟

實現步驟很是簡單,僅需兩步,便可在代碼上實現讀寫分離功能,感受很是帶勁。spring

1.引入jar包sql

<dependency>
    <groupId>io.shardingjdbc</groupId>
    <artifactId>sharding-jdbc-core-spring-boot-starter</artifactId>
    <version>2.0.3</version>
</dependency>

2.配置讀寫分離數據庫

sharding:
  jdbc:
    # 配置真實數據源
    datasource:
      names: ds_master_0,ds_slave_0_1,ds_slave_0_2
      # 配置主庫
      ds_master_0:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.jdbc.Driver
        jdbc-url: jdbc:mysql://ip:3306/test?useSSL=false&useUnicode=true&characterEncoding=utf8&autoReconnect=true
        username: username
        password: password
        maxPoolSize: 20
      # 配置第一個從庫
      ds_slave_0_1:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.jdbc.Driver
        jdbc-url: jdbc:mysql://ip:3307/test?useSSL=false&useUnicode=true&characterEncoding=utf8&autoReconnect=true
        username: username
        password: password
        maxPoolSize: 20
      # 配置第二個從庫
      ds_slave_0_2:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.jdbc.Driver
        jdbc-url: jdbc:mysql://ip:3308/test?useSSL=false&useUnicode=true&characterEncoding=utf8&autoReconnect=true
        username: username
        password: password
        maxPoolSize: 20
    # 配置讀寫分離
    config:
      masterslave:
        # 配置從庫選擇策略,提供輪詢與隨機,這裏選擇用輪詢
        load-balance-algorithm-type: round_robin
        name: ds_m_1_s_2
        master-data-source-name: ds_master_0
        slave-data-source-names: ds_slave_0_1,ds_slave_0_2
      sharding:
        props:
          # 開啓SQL顯示,默認值: false,注意:僅配置讀寫分離時不會打印日誌!!!
          sql:
            show: true

準備測試

在測試開始以前,咱們先明確一點,因爲只配置了讀寫分離,即便上文中配置了sql.show=true也不會有日誌打印出來(若是配置了分庫/分表就不會有這種狀況),那麼咱們怎麼知道數據庫操做究竟是走的主庫仍是主庫呢?怎麼知道若是走從庫有沒有遵循輪詢算法走的具體是哪一個從庫呢?安全

帶着上述的疑問,追溯源碼進入MasterSlaveDataSource這個類中(友情提示:IDEA連續按兩次shift在彈框中輸入MasterSlaveDataSource便可查看該類),主要關注其中的getDataSource()方法。下面貼出關鍵源碼。

/**
     * Get data source from master-slave data source.
     *
     * @param sqlType SQL type
     * @return data source from master-slave data source
     */
    public NamedDataSource getDataSource(final SQLType sqlType) {
        if (isMasterRoute(sqlType)) {
            DML_FLAG.set(true);
            return new NamedDataSource(masterSlaveRule.getMasterDataSourceName(), masterSlaveRule.getMasterDataSource());
        }
        String selectedSourceName = masterSlaveRule.getStrategy().getDataSource(masterSlaveRule.getName(), 
                masterSlaveRule.getMasterDataSourceName(), new ArrayList<>(masterSlaveRule.getSlaveDataSourceMap().keySet()));
        DataSource selectedSource = selectedSourceName.equals(masterSlaveRule.getMasterDataSourceName())
                ? masterSlaveRule.getMasterDataSource() : masterSlaveRule.getSlaveDataSourceMap().get(selectedSourceName);
        Preconditions.checkNotNull(selectedSource, "");
        return new NamedDataSource(selectedSourceName, selectedSource);
    }

    private boolean isMasterRoute(final SQLType sqlType) {
        return SQLType.DQL != sqlType || DML_FLAG.get() || HintManagerHolder.isMasterRouteOnly();
    }

isMasterRoute() 方法判斷當前操做是否應該路由到主庫數據源,若是SQL類型是DML則返回true

getDataSource() 方法根據SQL類型返回一個數據源。若是SQL類型是DQL則經過配置的算法返回一個從庫數據源,若是SQL類型是DML則返回主庫數據源。

那麼瞭解了以上兩個方法後,經過打斷點DEBUG的方式,咱們能夠很容易的得知,執行SQL時到底走的是哪一個庫。

開始測試

這邊我準備了兩個測試接口,一個用於測試讀操做,一個用於測試寫操做。

@RestController
@RequestMapping("/users")
public class UserController {
    @Autowired
    private IUserService userService;

    /**
     * 查詢用戶列表
     * @return
     */
    @GetMapping
    public List<User> getUser() {
        return userService.selectList(null);
    }

    /**
     * 建立/修改用戶信息
     * @param user
     * @return
     */
    @PostMapping
    public User saveUser(@RequestBody User user) {
        return userService.insertOrUpdate(user) ? userService.selectById(user.getId()) : null;
    }
}

發起GET請求/users接口,指望經過輪詢算法去從庫中查詢獲取數據

第一次,經過上圖咱們能夠很容易發現SQL類型是DQL,走的是ds_slave_0_1從數據庫,且策略是輪詢策略

第二次,咱們能夠發現走的是ds_slave_0_2從數據庫,讀操做和輪詢算法都沒毛病

發起POST請求/users接口,指望從主庫中建立或修改用戶數據。

可見,寫操做時,走的是ds_master_0主數據庫。當userService.insertOrUpdate(user)執行成功返回true後,接着再執行userService.selectById(user.getId())時,又會走到ds_slave_0_1從庫讀取數據。寫操做也沒毛病,以上咱們的測試階段就大功告成了。

輪詢策略

有興趣的小夥伴能夠看下輪詢策略的源碼,很是的簡單。這裏貼出輪詢策略主要源碼

/**
 * Round-robin slave database load-balance algorithm.
 *
 * @author zhangliang
 */
public final class RoundRobinMasterSlaveLoadBalanceAlgorithm implements MasterSlaveLoadBalanceAlgorithm {
    
    private static final ConcurrentHashMap<String, AtomicInteger> COUNT_MAP = new ConcurrentHashMap<>();
    
    @Override
    public String getDataSource(final String name, final String masterDataSourceName, final List<String> slaveDataSourceNames) {
        AtomicInteger count = COUNT_MAP.containsKey(name) ? COUNT_MAP.get(name) : new AtomicInteger(0);
        COUNT_MAP.putIfAbsent(name, count);
        count.compareAndSet(slaveDataSourceNames.size(), 0);
        return slaveDataSourceNames.get(count.getAndIncrement() % slaveDataSourceNames.size());
    }
}

其內部經過併發容器ConcurrentHashMap與AtomicInteger的CAS保障高併發下計數線程安全,使用無鎖的方式比加鎖效率更高。

靈活性

Sharding-JDBC使用簡單,容易上手且十分靈活,不只可使用默認策略,還可使用自定義的策略。能夠說是對Java開發者十分的友好,經過寫Java代碼的方式就能夠實現更加深度的定製化路由規則。這裏若是想要自定義輪詢策略可使用以下配置來自定義的輪詢策略。

sharding:
  jdbc:
    config:
      masterslave:
        load-balance-algorithm-class-name: 自定義算法類的全限定名

注意點

在玩轉讀寫分離時,遇到以下幾個須要注意的地方

  1. Sharding-JDBC目前僅支持一主多從的結構
  2. Sharding-JDBC沒有提供主從同步的實現,該功能須要本身額外搭建,可參照《基於Docker搭建MySQL主從複製》簡易搭建測試使用
  3. 主庫和從庫的數據同步延遲致使的數據不一致問題須要本身去解決
  4. Sharding-JDBC雖然提供了打印SQL日誌的開關,可是若是僅配置了讀寫分離好像是沒有用的
  5. 文中配置使用的是HikariCP鏈接池,使用其餘鏈接池時,須要將jdbc-url配置名該爲url,不然可能會拋異常

總結

這一篇,簡單帶你們用Java代碼實現了讀寫分離,下一篇預計會帶你們玩一下數據庫的分庫分表。

相關文章
相關標籤/搜索