【Spring Boot 實戰】數據庫千萬級分庫分表和讀寫分離實戰

一. 前言

前幾天時間寫了如何使用Sharding-JDBC進行分庫分表和讀寫分離的例子,相信可以感覺到Sharding-JDBC的強大了,並且使用配置都很是乾淨。官方支持的功能還不少功能分佈式主鍵、強制路由等。這裏是最終版介紹下如何在分庫分表的基礎上集成讀寫分離的功能。java

推薦先閱讀:
SpringBoot 2.x ShardingSphere分庫分表實戰
SpringBoot 2.x ShardingSphere讀寫分離實戰node

二. 項目實戰

主從數據庫配置mysql

在配置前,咱們但願分庫分表規則和以前保持一致:git

基於user表,根據id進行分庫,若是id mod 2爲奇數則落在ds0庫,偶數則落在ds1庫
根據age進行分表,若是age mod 2爲奇數則落在user_0表,偶數則落在user_1表
複製代碼

讀寫分離規則:github

讀都落在從庫,寫落在主庫
複製代碼

由於使用咱們使用Sharding-JDBC Spring Boot Starter,因此仍是隻須要在properties配置文件配置主從庫的數據源便可算法

# 能夠看到配置四個數據源 分別是 主數據庫兩個 從數據庫兩個
sharding.jdbc.datasource.names=master0,master1,master0slave0,master1slave0
# 主第一個數據庫
sharding.jdbc.datasource.master0.type=com.zaxxer.hikari.HikariDataSource
sharding.jdbc.datasource.master0.hikari.driver-class-name=com.mysql.jdbc.Driver
sharding.jdbc.datasource.master0.jdbc-url=jdbc:mysql://192.168.0.4:3306/ds0?characterEncoding=utf-8&serverTimezone=Asia/Shanghai
sharding.jdbc.datasource.master0.username=test
sharding.jdbc.datasource.master0.password=12root
# 主第二個數據庫
sharding.jdbc.datasource.master1.type=com.zaxxer.hikari.HikariDataSource
sharding.jdbc.datasource.master1.hikari.driver-class-name=com.mysql.jdbc.Driver
sharding.jdbc.datasource.master1.jdbc-url=jdbc:mysql://192.168.0.4:3306/ds1?characterEncoding=utf-8&serverTimezone=Asia/Shanghai
sharding.jdbc.datasource.master1.username=test
sharding.jdbc.datasource.master1.password=12root
# 從第一個數據庫
sharding.jdbc.datasource.master0slave0.type=com.zaxxer.hikari.HikariDataSource
sharding.jdbc.datasource.master0slave0.hikari.driver-class-name=com.mysql.jdbc.Driver
sharding.jdbc.datasource.master0slave0.jdbc-url=jdbc:mysql://192.168.0.3:3306/ds0?characterEncoding=utf-8&serverTimezone=Asia/Shanghai
sharding.jdbc.datasource.master0slave0.username=test
sharding.jdbc.datasource.master0slave0.password=12root
# 從第一個數據庫
sharding.jdbc.datasource.master1slave0.type=com.zaxxer.hikari.HikariDataSource
sharding.jdbc.datasource.master1slave0.hikari.driver-class-name=com.mysql.jdbc.Driver
sharding.jdbc.datasource.master1slave0.jdbc-url=jdbc:mysql://192.168.0.3:3306/ds1?characterEncoding=utf-8&serverTimezone=Asia/Shanghai
sharding.jdbc.datasource.master1slave0.username=test
sharding.jdbc.datasource.master1slave0.password=12root

# 讀寫分離配置
# 從庫的讀取規則爲round_robin(輪詢策略),除了輪詢策略,還有支持random(隨機策略)
sharding.jdbc.config.masterslave.load-balance-algorithm-type=round_robin
# 邏輯主從庫名和實際主從庫映射關係
# 主數據庫0
sharding.jdbc.config.sharding.master-slave-rules.ds0.master-data-source-name=master0
# 從數據庫0
sharding.jdbc.config.sharding.master-slave-rules.ds0.slave-data-source-names=master0slave0
# 主數據庫1
sharding.jdbc.config.sharding.master-slave-rules.ds1.master-data-source-name=master1
# 從數據庫1
sharding.jdbc.config.sharding.master-slave-rules.ds1.slave-data-source-names=master1slave0


# 分庫分表配置
# 水平拆分的數據庫(表) 配置分庫 + 分表策略 行表達式分片策略
# 分庫策略
sharding.jdbc.config.sharding.default-database-strategy.inline.sharding-column=id
sharding.jdbc.config.sharding.default-database-strategy.inline.algorithm-expression=ds$->{id % 2}
# 分表策略 其中user爲邏輯表 分表主要取決於age行
sharding.jdbc.config.sharding.tables.user.actual-data-nodes=ds$->{0..1}.user_$->{0..1}
sharding.jdbc.config.sharding.tables.user.table-strategy.inline.sharding-column=age
# 分片算法表達式
sharding.jdbc.config.sharding.tables.user.table-strategy.inline.algorithm-expression=user_$->{age % 2}

# 主鍵 UUID 18位數 若是是分佈式還要進行一個設置 防止主鍵重複
#sharding.jdbc.config.sharding.tables.user.key-generator-column-name=id

# 打印操做的sql以及庫表數據等
sharding.jdbc.config.props.sql.show=true
spring.main.allow-bean-definition-overriding=true
複製代碼

其餘項目配置不變,和以前保持一致便可spring

三. 測試

1.查詢所有數據庫

打開瀏覽器輸入 http://localhost:8080/selectsql

控制檯打印

2.插入數據

打開瀏覽器 分別訪問數據庫

http://localhost:8080/insert?id=1&name=lhd&age=12
http://localhost:8080/insert?id=2&name=lhd&age=13
http://localhost:8080/insert?id=3&name=lhd&age=14
http://localhost:8080/insert?id=4&name=lhd&age=15
複製代碼

控制檯打印express

結果和以前的同樣 根據分片算法和分片策略,不一樣的id以及age取模落入不一樣的庫表 達到了分庫分表

3.查詢所有數據

打開瀏覽器輸入 http://localhost:8080/select

控制檯打印

四. 問題

1. 沒法知道走的究竟是哪一個數據源
相信你們也發現了,當讀寫分離和分庫分表集成時
雖然咱們配置sql.show=true
可是控制檯最終打印不出所執行的數據源是哪一個
不知道是從庫仍是主庫
複製代碼
2.讀寫分離實現

讀寫分離的流程

獲取主從庫配置規則,數據源封裝成MasterSlaveDataSource
根據ShardingMasterSlaveRouter路由計算,獲得sqlRouteResult.getRouteUnits()單元列表,而後將結果addAll添加並返回
執行每一個RouteUnits的時候須要獲取鏈接,這裏根據輪詢負載均衡算法RoundRobinMasterSlaveLoadBalanceAlgorithm獲得從庫數據源,拿到鏈接後就開始執行具體的SQL查詢了,這裏經過PreparedStatementHandler.execute()獲得執行結果
結果歸併後返回
複製代碼

MasterSlaveDataSource.class

package io.shardingsphere.shardingjdbc.jdbc.core.datasource;

import io.shardingsphere.api.ConfigMapContext;
import io.shardingsphere.api.config.rule.MasterSlaveRuleConfiguration;
import io.shardingsphere.core.constant.properties.ShardingProperties;
import io.shardingsphere.core.rule.MasterSlaveRule;
import io.shardingsphere.shardingjdbc.jdbc.adapter.AbstractDataSourceAdapter;
import io.shardingsphere.shardingjdbc.jdbc.core.connection.MasterSlaveConnection;
import io.shardingsphere.transaction.api.TransactionTypeHolder;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import java.util.Map;
import java.util.Properties;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MasterSlaveDataSource extends AbstractDataSourceAdapter {
    private static final Logger log = LoggerFactory.getLogger(MasterSlaveDataSource.class);
    private final DatabaseMetaData databaseMetaData;
    private final MasterSlaveRule masterSlaveRule;
    private final ShardingProperties shardingProperties;

    public MasterSlaveDataSource(Map<String, DataSource> dataSourceMap, MasterSlaveRuleConfiguration masterSlaveRuleConfig, Map<String, Object> configMap, Properties props) throws SQLException {
        super(dataSourceMap);
        this.databaseMetaData = this.getDatabaseMetaData(dataSourceMap);
        if (!configMap.isEmpty()) {
            ConfigMapContext.getInstance().getConfigMap().putAll(configMap);
        }

        this.masterSlaveRule = new MasterSlaveRule(masterSlaveRuleConfig);
        // 從配置文件獲取配置的主從數據源
        this.shardingProperties = new ShardingProperties(null == props ? new Properties() : props);
    }

    // 獲取主從配置關係
    public MasterSlaveDataSource(Map<String, DataSource> dataSourceMap, MasterSlaveRule masterSlaveRule, Map<String, Object> configMap, Properties props) throws SQLException {
        super(dataSourceMap);
        this.databaseMetaData = this.getDatabaseMetaData(dataSourceMap);
        if (!configMap.isEmpty()) {
            ConfigMapContext.getInstance().getConfigMap().putAll(configMap);
        }

        this.masterSlaveRule = masterSlaveRule;
        this.shardingProperties = new ShardingProperties(null == props ? new Properties() : props);
    }

    // 獲取數據庫元數據
    private DatabaseMetaData getDatabaseMetaData(Map<String, DataSource> dataSourceMap) throws SQLException {
        Connection connection = ((DataSource)dataSourceMap.values().iterator().next()).getConnection();
        Throwable var3 = null;

        DatabaseMetaData var4;
        try {
            var4 = connection.getMetaData();
        } catch (Throwable var13) {
            var3 = var13;
            throw var13;
        } finally {
            if (connection != null) {
                if (var3 != null) {
                    try {
                        connection.close();
                    } catch (Throwable var12) {
                        var3.addSuppressed(var12);
                    }
                } else {
                    connection.close();
                }
            }

        }

        return var4;
    }

    public final MasterSlaveConnection getConnection() {
        return new MasterSlaveConnection(this, this.getShardingTransactionalDataSources().getDataSourceMap(), TransactionTypeHolder.get());
    }

    public DatabaseMetaData getDatabaseMetaData() {
        return this.databaseMetaData;
    }

    public MasterSlaveRule getMasterSlaveRule() {
        return this.masterSlaveRule;
    }

    public ShardingProperties getShardingProperties() {
        return this.shardingProperties;
    }
}

複製代碼

配置文件配置的主從規則
MasterSlaveRule.class

package io.shardingsphere.core.rule;

import com.google.common.base.Preconditions;
import io.shardingsphere.api.algorithm.masterslave.MasterSlaveLoadBalanceAlgorithm;
import io.shardingsphere.api.algorithm.masterslave.MasterSlaveLoadBalanceAlgorithmType;
import io.shardingsphere.api.config.rule.MasterSlaveRuleConfiguration;
import java.util.Collection;

public class MasterSlaveRule {
    //名稱(這裏是ds0和ds1)
    private final String name;
    //主庫數據源名稱(這裏是ds_master_0和ds_master_1)
    private final String masterDataSourceName;
    //所屬從庫列表,key爲從庫數據源名稱,value是真實的數據源
    private final Collection<String> slaveDataSourceNames;
    //主從庫負載均衡算法
    private final MasterSlaveLoadBalanceAlgorithm loadBalanceAlgorithm;
    //主從庫路由配置
    private final MasterSlaveRuleConfiguration masterSlaveRuleConfiguration;

複製代碼

輪詢負載均衡算算法 RoundRobinMasterSlaveLoadBalanceAlgorithm.class

package io.shardingsphere.api.algorithm.masterslave;

import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

//輪詢負載均衡策略,按照每一個從節點訪問次數均衡
public final class RoundRobinMasterSlaveLoadBalanceAlgorithm implements MasterSlaveLoadBalanceAlgorithm {
    private static final ConcurrentHashMap<String, AtomicInteger> COUNT_MAP = new ConcurrentHashMap();

    public RoundRobinMasterSlaveLoadBalanceAlgorithm() {
    }

    public String getDataSource(String name, String masterDataSourceName, List<String> slaveDataSourceNames) {
        AtomicInteger count = COUNT_MAP.containsKey(name) ? (AtomicInteger)COUNT_MAP.get(name) : new AtomicInteger(0);
        COUNT_MAP.putIfAbsent(name, count);
        count.compareAndSet(slaveDataSourceNames.size(), 0);
        return (String)slaveDataSourceNames.get(Math.abs(count.getAndIncrement()) % slaveDataSourceNames.size());
    }
}

複製代碼

ShardingMasterSlaveRouter.class

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package io.shardingsphere.core.routing.router.masterslave;

import io.shardingsphere.core.constant.SQLType;
import io.shardingsphere.core.hint.HintManagerHolder;
import io.shardingsphere.core.routing.RouteUnit;
import io.shardingsphere.core.routing.SQLRouteResult;
import io.shardingsphere.core.rule.MasterSlaveRule;
import java.beans.ConstructorProperties;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;

public final class ShardingMasterSlaveRouter {
    private final Collection<MasterSlaveRule> masterSlaveRules;
    
    // 獲得最終的sql路由
    public SQLRouteResult route(SQLRouteResult sqlRouteResult) {
        Iterator var2 = this.masterSlaveRules.iterator();

        while(var2.hasNext()) {
            MasterSlaveRule each = (MasterSlaveRule)var2.next();
            this.route(each, sqlRouteResult);
        }

        return sqlRouteResult;
    }

    //進行計算篩選獲得最終sql路由
    private void route(MasterSlaveRule masterSlaveRule, SQLRouteResult sqlRouteResult) {
        Collection<RouteUnit> toBeRemoved = new LinkedList();
        Collection<RouteUnit> toBeAdded = new LinkedList();
        Iterator var5 = sqlRouteResult.getRouteUnits().iterator();

        while(var5.hasNext()) {
            RouteUnit each = (RouteUnit)var5.next();
            if (masterSlaveRule.getName().equalsIgnoreCase(each.getDataSourceName())) {
                toBeRemoved.add(each);
                if (this.isMasterRoute(sqlRouteResult.getSqlStatement().getType())) {
                    MasterVisitedManager.setMasterVisited();
                    toBeAdded.add(new RouteUnit(masterSlaveRule.getMasterDataSourceName(), each.getSqlUnit()));
                } else {
                    toBeAdded.add(new RouteUnit(masterSlaveRule.getLoadBalanceAlgorithm().getDataSource(masterSlaveRule.getName(), masterSlaveRule.getMasterDataSourceName(), new ArrayList(masterSlaveRule.getSlaveDataSourceNames())), each.getSqlUnit()));
                }
            }
        }
        //路由移除(查詢時 移除全部主庫)
        sqlRouteResult.getRouteUnits().removeAll(toBeRemoved);
        //添加從庫/主庫 具體事件定
        sqlRouteResult.getRouteUnits().addAll(toBeAdded);
    }

    // 判斷是否是主庫
    private boolean isMasterRoute(SQLType sqlType) {
        return SQLType.DQL != sqlType || MasterVisitedManager.isMasterVisited() || HintManagerHolder.isMasterRouteOnly();
    }

    @ConstructorProperties({"masterSlaveRules"})
    public ShardingMasterSlaveRouter(Collection<MasterSlaveRule> masterSlaveRules) {
        this.masterSlaveRules = masterSlaveRules;
    }
}

複製代碼

注: 判斷是否是主庫的規則爲:

private boolean isMasterRoute(SQLType sqlType) {
        return SQLType.DQL != sqlType || MasterVisitedManager.isMasterVisited() || HintManagerHolder.isMasterRouteOnly();
    }
複製代碼

SQL語言的判斷

SQL語言共分爲四大類:數據查詢語言DQL,數據操縱語言DML,數據定義語言DDL,數據控制語言DCL。

複製代碼

經過斷點,查詢所有數據時最終的sql路由爲

走的從庫的四個從表 前面的問題也就迎刃而解

目前讀寫分離和分庫分表就完成

源碼分析不對,若有錯誤請指點一二

源碼下載: github.com/LiHaodong88…

相關文章
相關標籤/搜索