點選上方“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()路由時,會嘗試讀寫分離:
Collection<PreparedStatement> preparedStatements;
if (SQLType.DDL == sqlType) {
// 路由這裡生成PreparedStatement時會選主從(如果是主從的話)
preparedStatements = generatePreparedStatementForDDL(each);
} else {
// 路由這裡生成PreparedStatement時會選主從(如果是主從的話)
preparedStatements = Collections.singletonList(generatePreparedStatement(each));
}
routedStatements.addAll(preparedStatements);``` ```private PreparedStatement generatePreparedStatement(final SQLExecutionUnit sqlExecutionUnit) throws SQLException {
// 先獲取connection資料庫連線,然後得到PreparedStatement,獲取conntection時就會嘗試選主從(如果有主從的話)
Connection connection = getConnection().getConnection(sqlExecutionUnit.getDataSource(), routeResult.getSqlStatement().getType());
return connection.prepareStatement(... ...);
}``````// 資料源名稱與資料庫連線關係快取,例如:{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實體}
private final Map<String, Connection> cachedConnections = new HashMap<>();
/**
* 根據資料源名稱得到資料庫連線
*/
public Connection getConnection(final String dataSourceName, final SQLType sqlType) throws SQLException {
// 首先嘗試從local cache(map型別)中獲取,如果已經本地快取,那麼直接從本地快取中獲取
if (getCachedConnections().containsKey(dataSourceName)) {
return getCachedConnections().get(dataSourceName);
}
DataSource dataSource = shardingContext.getShardingRule().getDataSourceRule().getDataSource(dataSourceName);
Preconditions.checkState(null != dataSource, "Missing the rule of %s in DataSourceRule", dataSourceName);
String realDataSourceName;
// 如果是主從資料庫的話(例如xml中配置
,那麼dbtbl_0就是主從資料源)
if (dataSource instanceof MasterSlaveDataSource) {
// 見後面的"主從資料源中根據負載均衡策略獲取資料源"的分析
NamedDataSource namedDataSource = ((MasterSlaveDataSource) dataSource).getDataSource(sqlType);
realDataSourceName = namedDataSource.getName();
// 如果主從資料庫元選出的資料源名稱(例如:dbtbl_1_slave_0)與資料庫連線已經被快取,那麼從快取中取出資料庫連線
if (getCachedConnections().containsKey(realDataSourceName)) {
return getCachedConnections().get(realDataSourceName);
}
dataSource = namedDataSource.getDataSource();
} else {
realDataSourceName = dataSourceName;
}
Connection result = dataSource.getConnection();
// 把資料源名稱與資料庫連線實體快取起來
getCachedConnections().put(realDataSourceName, result);
replayMethodsInvocation(result);
return result;
}
主從資料源中根據負載均衡策略獲取資料源核心原始碼--MasterSlaveDataSource.java:
// 主資料源, 例如dbtbl_0_master對應的資料源
@Getter
private final DataSource masterDataSource;
// 主資料源下所有的從資料源,例如{dbtbl_0_slave_0:DataSource實體; dbtbl_0_slave_1:DataSource實體}
@Getter
private final Map<String, DataSource> slaveDataSources;
public NamedDataSource getDataSource(final SQLType sqlType) {
if (isMasterRoute(sqlType)) {
DML_FLAG.set(true);
// 如果符合主路由規則,那麼直接傳回主路由(不需要根據負載均衡策略選擇資料源)
return new NamedDataSource(masterDataSourceName, masterDataSource);
}
// 負載均衡策略選擇資料源名稱[後面會分析]
String selectedSourceName = masterSlaveLoadBalanceStrategy.getDataSource(name, masterDataSourceName, new ArrayList<>(slaveDataSources.keySet()));
DataSource selectedSource = selectedSourceName.equals(masterDataSourceName) ? masterDataSource : slaveDataSources.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();
}
主路由邏輯如下:
-
非查詢SQL(SQLType.DQL != sqlType)
-
當前資料源在當前執行緒訪問過主庫(資料源訪問過主庫就會透過ThreadLocal將DMLFLAG置為true,從而路由主庫)(DMLFLAG.get())
-
HintManagerHolder方式設定了主路由規則(HintManagerHolder.isMasterRouteOnly())
當前執行緒訪問過主庫後,後面的操作全部切主,是為了防止主從同步資料延遲導致寫操作後,讀不到最新的資料?我想應該是這樣的^^
主從負載均衡分析
從對 MasterSlaveDataSource.java
的分析可知,如果不符合強制主路由規則,那麼會根據負載均衡策略選多個slave中選取一個slave;MasterSlaveLoadBalanceStrategy介面有兩個實現類:RoundRobinMasterSlaveLoadBalanceStrategy和RandomMasterSlaveLoadBalanceStrategy,簡單分析其實現;
輪詢策略
輪詢方式的實現類為RoundRobinMasterSlaveLoadBalanceStrategy,核心原始碼如下:
public final class RoundRobinMasterSlaveLoadBalanceStrategy implements MasterSlaveLoadBalanceStrategy {
private static final ConcurrentHashMap<String, AtomicInteger> COUNT_MAP = new ConcurrentHashMap<>();
@Override
public String getDataSource(final String name, final String masterDataSourceName, final List<String> slaveDataSourceNames) {
// 每個叢集體系都有自己的計數器,例如dbtbl_0叢集,dbtbl_1叢集;如果COUNT_MAP中還沒有這個叢集體系,需要先初始化;
AtomicInteger count = COUNT_MAP.containsKey(name) ? COUNT_MAP.get(name) : new AtomicInteger(0);
COUNT_MAP.putIfAbsent(name, count);
// 如果輪詢計數器(AtomicInteger count)長到slave.size(),那麼歸零(防止計數器不斷增長下去)
count.compareAndSet(slaveDataSourceNames.size(), 0);
// 計數器遞增,根據計算器的值就是從slave集合中選中的標的slave的下標
return slaveDataSourceNames.get(count.getAndIncrement() % slaveDataSourceNames.size());
}
}
隨機策略
隨機方式的實現類為RandomMasterSlaveLoadBalanceStrategy,核心原始碼如下:
public final class RandomMasterSlaveLoadBalanceStrategy implements MasterSlaveLoadBalanceStrategy {
@Override
public String getDataSource(final String name, final String masterDataSourceName, final List<String> slaveDataSourceNames) {
// 取一個隨機數,就是從slave集合中選中的標的slave的下標
return slaveDataSourceNames.get(new Random().nextInt(slaveDataSourceNames.size()));
}
}
預設策略
@RequiredArgsConstructor
@Getter
public enum MasterSlaveLoadBalanceStrategyType {
// 輪詢策略
ROUND_ROBIN(new RoundRobinMasterSlaveLoadBalanceStrategy()),
// 隨機策略
RANDOM(new RandomMasterSlaveLoadBalanceStrategy());
private final MasterSlaveLoadBalanceStrategy strategy;
// 預設策略為輪詢
public static MasterSlaveLoadBalanceStrategyType getDefaultStrategyType() {
return ROUND_ROBIN;
}
}
END