sharding-jdbc讀寫分離原理詳細(xì)解析
前言
很多時候,為了應(yīng)付DB的高并發(fā)讀寫,我們會采用讀寫分離技術(shù)。讀寫分離指的是利用數(shù)據(jù)庫主從技術(shù)(把數(shù)據(jù)復(fù)制到多個節(jié)點(diǎn)中),分散讀多個庫以支持高并發(fā)的讀,而寫只在master庫上。DB的主從技術(shù)只負(fù)責(zé)對數(shù)據(jù)進(jìn)行復(fù)制和同步,而讀寫分離技術(shù)需要業(yè)務(wù)應(yīng)用自身去實(shí)現(xiàn)。sharding-jdbc通過簡單的開發(fā),可以方便的實(shí)現(xiàn)讀寫分離技術(shù)。本篇主要介紹其實(shí)現(xiàn)的原理。
sharding-jdbc讀寫分離特性說明
sharding-jdbc官方對其支持的讀寫分離技術(shù)進(jìn)行了說明:
支持項(xiàng) 提供了一主多從的讀寫分離配置,可獨(dú)立使用,也可配合分庫分表使用。 同個調(diào)用線程,執(zhí)行多條語句,其中一旦發(fā)現(xiàn)有非讀操作,后續(xù)所有讀操作均從主庫讀取。 Spring命名空間。 基于Hint的強(qiáng)制主庫路由。
不支持范圍 主庫和從庫的數(shù)據(jù)同步。 主庫和從庫的數(shù)據(jù)同步延遲導(dǎo)致的數(shù)據(jù)不一致。 主庫雙寫或多寫。
簡單說明 sharding-jdbc實(shí)現(xiàn)讀寫分離技術(shù)的思路比較簡潔,不支持類似主庫雙寫或多寫這樣的特性,但目前來看,已經(jīng)可以滿足一般的業(yè)務(wù)需求了。
讀寫分離實(shí)現(xiàn)demo
庫和表的設(shè)計(jì)結(jié)構(gòu)如下:

簡單的java代碼示例:
public final class MasterSlaveMain {
public static void main(final String[] args) throws SQLException {
DataSource dataSource = getShardingDataSource();
printSimpleSelect(dataSource);
}
private static void printSimpleSelect(final DataSource dataSource) throws SQLException {
String sql = "SELECT i.* FROM t_order o JOIN t_order_item i ON o.order_id=i.order_id WHERE o.user_id=? AND o.order_id=?";
try (
Connection conn = dataSource.getConnection();
PreparedStatement preparedStatement = conn.prepareStatement(sql)) {
preparedStatement.setInt(1, 10);
preparedStatement.setInt(2, 1001);
try (ResultSet rs = preparedStatement.executeQuery()) {
while (rs.next()) {
System.out.println(rs.getInt(1));
System.out.println(rs.getInt(2));
System.out.println(rs.getInt(3));
}
}
}
}
private static ShardingDataSource getShardingDataSource() throws SQLException {
DataSourceRule dataSourceRule = new DataSourceRule(createDataSourceMap());
TableRule orderTableRule = TableRule.builder("t_order").actualTables(Arrays.asList("t_order_0", "t_order_1")).dataSourceRule(dataSourceRule).build();
TableRule orderItemTableRule = TableRule.builder("t_order_item").actualTables(Arrays.asList("t_order_item_0", "t_order_item_1")).dataSourceRule(dataSourceRule).build();
ShardingRule shardingRule = ShardingRule.builder().dataSourceRule(dataSourceRule).tableRules(Arrays.asList(orderTableRule, orderItemTableRule))
.databaseShardingStrategy(new DatabaseShardingStrategy("user_id", new ModuloDatabaseShardingAlgorithm()))
.tableShardingStrategy(new TableShardingStrategy("order_id", new ModuloTableShardingAlgorithm())).build();
return new ShardingDataSource(shardingRule);
}
private static Map<String, DataSource> createDataSourceMap() throws SQLException {
Map<String, DataSource> result = new HashMap<>(2, 1);
Map<String, DataSource> slaveDataSourceMap1 = new HashMap<>(2, 1);
slaveDataSourceMap1.put("ds_0_slave_0", createDataSource("ds_0_slave_0"));
slaveDataSourceMap1.put("ds_0_slave_1", createDataSource("ds_0_slave_1"));
result.put("ds_0", MasterSlaveDataSourceFactory.createDataSource("ds_0", "ds_0_master", createDataSource("ds_0_master"), slaveDataSourceMap1));
Map<String, DataSource> slaveDataSourceMap2 = new HashMap<>(2, 1);
slaveDataSourceMap2.put("ds_1_slave_0", createDataSource("ds_1_slave_0"));
slaveDataSourceMap2.put("ds_1_slave_1", createDataSource("ds_1_slave_1"));
result.put("ds_1", MasterSlaveDataSourceFactory.createDataSource("ds_1", "ds_1_master", createDataSource("ds_1_master"), slaveDataSourceMap2));
return result;
}
private static DataSource createDataSource(final String dataSourceName) {
BasicDataSource result = new BasicDataSource();
result.setDriverClassName(com.mysql.jdbc.Driver.class.getName());
result.setUrl(String.format("jdbc:mysql://localhost:3306/%s", dataSourceName));
result.setUsername("root");
result.setPassword("123456");
return result;
}
}讀寫分離實(shí)現(xiàn)原理
一般我們是這樣來執(zhí)行sql語句的:
Connection conn = dataSource.getConnection(); PreparedStatement preparedStatement = conn.prepareStatement(sql); preparedStatement.executeQuery();
這是利用原生jdbc操作數(shù)據(jù)庫查詢語句的一般流程,獲取一個連接,然后生成Statement,最后再執(zhí)行查詢。那么sharding-jdbc是在哪一塊進(jìn)行擴(kuò)展從而實(shí)現(xiàn)讀寫分離的呢?
想一下,想要實(shí)現(xiàn)讀寫分離,必然會涉及到多個底層的Connection,從而構(gòu)造出不同連接下的Statement語句,而很多第三方軟件,如Spring,為了實(shí)現(xiàn)事務(wù),調(diào)用dataSource.getConnection()之后,在一次請求過程中,可能就不會再次調(diào)用getConnection方法了,所以在dataSource.getConnection中做讀寫擴(kuò)展是不可取的。為了更好的說明問題,看下面的例子:
Connection conn = getConnection(); PreparedStatement preparedStatement1 = conn.prepareStatement(sql1); preparedStatement1.executeQuery(); Connection conn2 = getConnection(); PreparedStatement preparedStatement2 = conn2.prepareStatement(sql2); preparedStatement2.executeUpdate();
一次請求過程中,為了實(shí)現(xiàn)事務(wù),一般的做法是當(dāng)線程第一次調(diào)用getConnection方法時,獲取一個底層連接,然后存儲到ThreadLocal變量中去,下次就直接在ThreadLocal中獲取了。為了實(shí)現(xiàn)一個事務(wù)中,針對一個數(shù)據(jù)源,既可能獲取到主庫連接,也可能獲取到從庫連接,還能夠切換,sharding-jdbc在PreparedStatement(實(shí)際上為ShardingPreparedStatement)的executeXX層進(jìn)行了主從庫的連接處理。
下圖為sharding-jdbc執(zhí)行的部分流程:

sharding-jdbc使用ShardingPreparedStatement來替代PreparedStatement,在執(zhí)行ShardingPreparedStatement的executeXX方法時,通過路由計(jì)算,得到PreparedStatementUnit單元列表,然后執(zhí)行后合并結(jié)果返回,而PreparedStatementUnit只不過封裝了原生的PreparedStatement。讀寫分離最關(guān)鍵的地方在上圖標(biāo)綠色的地方,也就是生成PreparedStatement的地方。
在使用SQLEcecutionUnit轉(zhuǎn)換為PreparedStatement的時候,有一個重要的步驟就是必須先獲取Connection,源碼如下:
public Connection getConnection(final String dataSourceName, final SQLType sqlType) throws SQLException {
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;
if (dataSource instanceof MasterSlaveDataSource) {
NamedDataSource namedDataSource = ((MasterSlaveDataSource) dataSource).getDataSource(sqlType);
realDataSourceName = namedDataSource.getName();
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;
}如果發(fā)現(xiàn)數(shù)據(jù)源對象為MasterSlaveDataSource類型,則會使用如下方式獲取真正的數(shù)據(jù)源:
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 static boolean isMasterRoute(final SQLType sqlType) {
return SQLType.DQL != sqlType || DML_FLAG.get() || HintManagerHolder.isMasterRouteOnly();
}有三種情況會認(rèn)為一定要走主庫:
1. 不是查詢類型的語句,比如更新字段
2. DML_FLAG變量為true的時候
3. 強(qiáng)制Hint方式走主庫
當(dāng)執(zhí)行了更新語句的時候,isMasterRoute()==true,這時候,Connection為主庫的連接,并且引擎會強(qiáng)制設(shè)置DML_FLAG的值為true,這樣一個請求后續(xù)的所有讀操作都會走主庫。 有些時候,我們想強(qiáng)制走主庫,這時候在請求最開始執(zhí)行Hint操作即可,如下所示:
HintManager hintManager = HintManager.getInstance(); hintManager.setMasterRouteOnly();
在獲取數(shù)據(jù)源的時候,如果走的是從庫,會使用從庫負(fù)載均衡算法類進(jìn)行處理,該類的實(shí)現(xiàn)比較簡單,如下所示:
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) {
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());
}
}其實(shí)就是一個簡單的輪循機(jī)制進(jìn)行從庫的負(fù)載均衡。
總結(jié)
sharding-jdbc進(jìn)行主從讀寫分離的特性實(shí)現(xiàn)比較簡潔易懂,對spring這種上層框架而言是無感知的,而且由于它是在路由得到SQLExecutionUtil后再處理的,所以使用了讀寫分離特性,可以同時使用分庫分表。
到此這篇關(guān)于sharding-jdbc讀寫分離原理詳細(xì)解析的文章就介紹到這了,更多相關(guān)sharding-jdbc讀寫分離內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot項(xiàng)目中使用Sharding-JDBC實(shí)現(xiàn)讀寫分離的詳細(xì)步驟
- sharding-jdbc 兼容 MybatisPlus動態(tài)數(shù)據(jù)源的配置方法
- SpringBoot集成Sharding-JDBC實(shí)現(xiàn)分庫分表方式
- sharding-jdbc實(shí)現(xiàn)分頁查詢的示例代碼
- SpringBoot+MybatisPlus實(shí)現(xiàn)sharding-jdbc分庫分表的示例代碼
- Sharding-jdbc報(bào)錯:Missing the data source name:‘m0‘解決方案
相關(guān)文章
Java中的JPA實(shí)體關(guān)系:JPA一對一,一對多(多對一),多對多
Java Persistence API(JPA)是Java平臺上的一個對象關(guān)系映射(ORM)規(guī)范,用于簡化數(shù)據(jù)庫操作,其中實(shí)體關(guān)系的映射是核心內(nèi)容之一,本文將深入淺出地探討JPA中的三種基本實(shí)體關(guān)系類型:一對一、一對多、多對多,揭示常見問題、易錯點(diǎn)及其避免策略,希望能幫助大家2024-06-06
Java基礎(chǔ)之詳解基本數(shù)據(jù)類型的使用
今天給大家?guī)淼氖顷P(guān)于Java基礎(chǔ)的相關(guān)知識,文章圍繞著基本數(shù)據(jù)類型的使用展開,文中有非常詳細(xì)的介紹及代碼示例,需要的朋友可以參考下2021-06-06
Spring Boot 3.x 集成 Eureka Server/Cl
隨著SpringBoot 3.x版本的開發(fā)嘗試,本文記錄了在集成Eureka Server/Client時所遇到的問題和解決方案,文中詳細(xì)介紹了搭建服務(wù)、配置文件和測試步驟,感興趣的朋友跟隨小編一起看看吧2024-09-09
SpringBoot+Resilience4j實(shí)現(xiàn)接口限流的示例代碼
Resilience4j 是一個用于實(shí)現(xiàn)熔斷、限流、重試等功能的輕量級庫,本文主要介紹了SpringBoot+Resilience4j實(shí)現(xiàn)接口限流的示例代碼,具有一定的參考價值,感興趣的可以了解一下2024-12-12
通俗易懂的Java常見限流算法具體實(shí)現(xiàn)
這篇文章主要介紹了Java常見限流算法具體實(shí)現(xiàn)的相關(guān)資料,包括漏桶算法、令牌桶算法、Nginx限流和Redis+Lua限流的實(shí)現(xiàn)原理和具體步驟,并比較了它們的優(yōu)點(diǎn)和缺點(diǎn),需要的朋友可以參考下2025-02-02

