SpringBoot 玩轉讀寫分離 轉

摘要: 基於SpringBoot與Sharding-JDBC實現讀寫分離

環境概覽

框架 版本號
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地址

讀寫分離

引自Sharding-JDBC官方文檔

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

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

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

這裏解釋一下什麼是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等語句

實現步驟

實現步驟非常簡單,僅需兩步,即可在代碼上實現讀寫分離功能,感覺非常帶勁。

1.引入jar包

<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,否則可能會拋異常

作者:秋田君

來源:https://my.oschina.net/u/3773384/blog/1811333

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章