歡迎光臨
每天分享高質量文章

【死磕Sharding-jdbc】—讀寫分離

點選上方“Java技術驛站”,選擇“置頂公眾號”。

有內涵、有價值的文章第一時間送達!

讀寫分離支援項

  • 提供了一主多從的讀寫分離配置,可獨立使用,也可配合分庫分表使用。

  • 同一執行緒且同一資料庫連線內,如有寫入操作,以後的讀操作均從主庫讀取,用於保證資料一致性。

  • Spring名稱空間。

  • 基於Hint的強制主庫路由。

讀寫分離不支援範圍

  • 主庫和從庫的資料同步。

  • 主庫和從庫的資料同步延遲導致的資料不一致。

  • 主庫雙寫或多寫。

讀寫分離支援項和不支援範圍摘自sharding-jdbc使用指南☞讀寫分離

原始碼分析

先執行 sharding-jdbc-example-config-spring-masterslave模組中的的SQL指令碼 all_schema.sql,這裡有讀寫分離測試的需要的資料庫、表以及資料;

  • 兩個主資料庫 dbtbl_0_master dbtbl_1_master

  • 資料庫 dbtbl_0_master有兩個從庫 dbtbl_0_slave_0 dbtbl_0_slave_1,這個叢集體繫命名為 dbtbl_0

  • 資料庫 dbtbl_1_master有兩個從庫 dbtbl_1_slave_0 dbtbl_1_slave_1,這個叢集體繫命名為 dbtbl_1

SpringNamespaceWithMasterSlaveMain.java為入口,分析讀寫分離是如何實現的:

router()路由時,會嘗試讀寫分離:

  1. Collection<PreparedStatement> preparedStatements;

  2. if (SQLType.DDL == sqlType) {

  3.    // 路由這裡生成PreparedStatement時會選主從(如果是主從的話)

  4.    preparedStatements = generatePreparedStatementForDDL(each);

  5. } else {

  6.    // 路由這裡生成PreparedStatement時會選主從(如果是主從的話)

  7.    preparedStatements = Collections.singletonList(generatePreparedStatement(each));

  8. }

  9. routedStatements.addAll(preparedStatements);``` ```private PreparedStatement generatePreparedStatement(final SQLExecutionUnit sqlExecutionUnit) throws SQLException {

  10.        // 先獲取connection資料庫連線,然後得到PreparedStatement,獲取conntection時就會嘗試選主從(如果有主從的話)

  11.        Connection connection = getConnection().getConnection(sqlExecutionUnit.getDataSource(), routeResult.getSqlStatement().getType());

  12.        return connection.prepareStatement(... ...);

  13.    }``````// 資料源名稱與資料庫連線關係快取,例如:{dbtbl_0_master:Connection實體; dbtbl_1_master:Connection實體; dbtbl_0_slave_0:Connection實體; dbtbl_0_slave_1:Connection實體; dbtbl_1_slave_0:Connection實體; dbtbl_1_slave_1:Connection實體}

  14. private final Map<String, Connection> cachedConnections = new HashMap<>();

  15. /**

  16. * 根據資料源名稱得到資料庫連線

  17. */

  18. public Connection getConnection(final String dataSourceName, final SQLType sqlType) throws SQLException {

  19.    // 首先嘗試從local cache(map型別)中獲取,如果已經本地快取,那麼直接從本地快取中獲取

  20.    if (getCachedConnections().containsKey(dataSourceName)) {

  21.        return getCachedConnections().get(dataSourceName);

  22.    }

  23.    DataSource dataSource = shardingContext.getShardingRule().getDataSourceRule().getDataSource(dataSourceName);

  24.    Preconditions.checkState(null != dataSource, "Missing the rule of %s in DataSourceRule", dataSourceName);

  25.    String realDataSourceName;

  26.    // 如果是主從資料庫的話(例如xml中配置,那麼dbtbl_0就是主從資料源)

  27.    if (dataSource instanceof MasterSlaveDataSource) {

  28.        // 見後面的"主從資料源中根據負載均衡策略獲取資料源"的分析

  29.        NamedDataSource namedDataSource = ((MasterSlaveDataSource) dataSource).getDataSource(sqlType);

  30.        realDataSourceName = namedDataSource.getName();

  31.        // 如果主從資料庫元選出的資料源名稱(例如:dbtbl_1_slave_0)與資料庫連線已經被快取,那麼從快取中取出資料庫連線

  32.        if (getCachedConnections().containsKey(realDataSourceName)) {

  33.            return getCachedConnections().get(realDataSourceName);

  34.        }

  35.        dataSource = namedDataSource.getDataSource();

  36.    } else {

  37.        realDataSourceName = dataSourceName;

  38.    }

  39.    Connection result = dataSource.getConnection();

  40.    // 把資料源名稱與資料庫連線實體快取起來

  41.    getCachedConnections().put(realDataSourceName, result);

  42.    replayMethodsInvocation(result);

  43.    return result;

  44. }

主從資料源中根據負載均衡策略獲取資料源核心原始碼--MasterSlaveDataSource.java:

  1. // 主資料源, 例如dbtbl_0_master對應的資料源

  2. @Getter

  3. private final DataSource masterDataSource;

  4. // 主資料源下所有的從資料源,例如{dbtbl_0_slave_0:DataSource實體; dbtbl_0_slave_1:DataSource實體}

  5. @Getter

  6. private final Map<String, DataSource> slaveDataSources;

  7. public NamedDataSource getDataSource(final SQLType sqlType) {

  8.    if (isMasterRoute(sqlType)) {

  9.        DML_FLAG.set(true);

  10.        // 如果符合主路由規則,那麼直接傳回主路由(不需要根據負載均衡策略選擇資料源)

  11.        return new NamedDataSource(masterDataSourceName, masterDataSource);

  12.    }

  13.    // 負載均衡策略選擇資料源名稱[後面會分析]

  14.    String selectedSourceName = masterSlaveLoadBalanceStrategy.getDataSource(name, masterDataSourceName, new ArrayList<>(slaveDataSources.keySet()));

  15.    DataSource selectedSource = selectedSourceName.equals(masterDataSourceName) ? masterDataSource : slaveDataSources.get(selectedSourceName);

  16.    Preconditions.checkNotNull(selectedSource, "");

  17.    return new NamedDataSource(selectedSourceName, selectedSource);

  18. }

  19. // 主路由邏輯

  20. private boolean isMasterRoute(final SQLType sqlType) {

  21.    return SQLType.DQL != sqlType || DML_FLAG.get() || HintManagerHolder.isMasterRouteOnly();

  22. }

主路由邏輯如下:

  1. 非查詢SQL(SQLType.DQL != sqlType)

  2. 當前資料源在當前執行緒訪問過主庫(資料源訪問過主庫就會透過ThreadLocal將DMLFLAG置為true,從而路由主庫)(DMLFLAG.get())

  3. HintManagerHolder方式設定了主路由規則(HintManagerHolder.isMasterRouteOnly())

當前執行緒訪問過主庫後,後面的操作全部切主,是為了防止主從同步資料延遲導致寫操作後,讀不到最新的資料?我想應該是這樣的^^

主從負載均衡分析

從對 MasterSlaveDataSource.java的分析可知,如果不符合強制主路由規則,那麼會根據負載均衡策略選多個slave中選取一個slave;MasterSlaveLoadBalanceStrategy介面有兩個實現類:RoundRobinMasterSlaveLoadBalanceStrategyRandomMasterSlaveLoadBalanceStrategy,簡單分析其實現;

輪詢策略

輪詢方式的實現類為RoundRobinMasterSlaveLoadBalanceStrategy,核心原始碼如下:

  1. public final class RoundRobinMasterSlaveLoadBalanceStrategy implements MasterSlaveLoadBalanceStrategy {

  2.    private static final ConcurrentHashMap<String, AtomicInteger> COUNT_MAP = new ConcurrentHashMap<>();

  3.    @Override

  4.    public String getDataSource(final String name, final String masterDataSourceName, final List<String> slaveDataSourceNames) {

  5.        // 每個叢集體系都有自己的計數器,例如dbtbl_0叢集,dbtbl_1叢集;如果COUNT_MAP中還沒有這個叢集體系,需要先初始化;

  6.        AtomicInteger count = COUNT_MAP.containsKey(name) ? COUNT_MAP.get(name) : new AtomicInteger(0);

  7.        COUNT_MAP.putIfAbsent(name, count);

  8.        // 如果輪詢計數器(AtomicInteger count)長到slave.size(),那麼歸零(防止計數器不斷增長下去)

  9.        count.compareAndSet(slaveDataSourceNames.size(), 0);

  10.        // 計數器遞增,根據計算器的值就是從slave集合中選中的標的slave的下標

  11.        return slaveDataSourceNames.get(count.getAndIncrement() % slaveDataSourceNames.size());

  12.    }

  13. }

隨機策略

隨機方式的實現類為RandomMasterSlaveLoadBalanceStrategy,核心原始碼如下:

  1. public final class RandomMasterSlaveLoadBalanceStrategy implements MasterSlaveLoadBalanceStrategy {

  2.    @Override

  3.    public String getDataSource(final String name, final String masterDataSourceName, final List<String> slaveDataSourceNames) {

  4.        // 取一個隨機數,就是從slave集合中選中的標的slave的下標

  5.        return slaveDataSourceNames.get(new Random().nextInt(slaveDataSourceNames.size()));

  6.    }

  7. }

預設策略

  1. @RequiredArgsConstructor

  2. @Getter

  3. public enum MasterSlaveLoadBalanceStrategyType {

  4.    // 輪詢策略

  5.    ROUND_ROBIN(new RoundRobinMasterSlaveLoadBalanceStrategy()),

  6.    // 隨機策略

  7.    RANDOM(new RandomMasterSlaveLoadBalanceStrategy());

  8.    private final MasterSlaveLoadBalanceStrategy strategy;

  9.    // 預設策略為輪詢

  10.    public static MasterSlaveLoadBalanceStrategyType getDefaultStrategyType() {

  11.        return ROUND_ROBIN;

  12.    }

  13. }

END

贊(0)

分享創造快樂

© 2024 知識星球   網站地圖