利用Sharding-JDBC解決數據庫讀寫分離查詢延時問題

前言

一般熟知 Mysql 數據庫的朋友知道,當表的數據量達到千萬級時,SQL 查詢會逐漸變的緩慢起來,往往會成為一個系統的瓶頸所在。為了提升程序的性能,除了在表字段建立索引(如主鍵索引、唯一索引、普通索引等)、優化程序代碼以及 SQL 語句等常規手段外,利用數據庫主從讀寫分離(Master/Slave)架構,是一個不錯的選擇。但是在這種分離架構中普遍存在一個共性問題:數據讀寫一致性問題。

數據讀寫一致性問題

主從庫同步邏輯

主庫 Master 負責“寫”,會把數據庫的 BinLog 日誌記錄通過 I/O 線程異步操作同步到從庫(負責“讀”),這樣每當業務系統發送 select 語句時,會直接路由到從庫去查詢數據,而不是主庫。

利用Sharding-JDBC解決數據庫讀寫分離查詢延時問題

但是這種同步邏輯有一個比較嚴重的缺陷:數據延時問題

我們可以想象一下這樣的場景:

當一段程序在更新完數據後,需要立即查詢更新後的數據,那麼真的能查詢到更新後的數據嗎?

答案是:不一定!

這是因為主從數據同步時是異步操作,主從同步期間會存在數據延時問題,平常主庫寫數據量比較少的情況下,偶爾會遇到查詢不到數據的情況。但是隨著時間的推移,當使用系統的用戶增多時,會發現這種查詢不到數據的情況會變的越來越糟糕。

Sharding-JDBC

想必大家並不陌生,Sharding-JDBC 定位為輕量級 Java 框架,在 Java 的 JDBC 層提供的額外服務。

利用Sharding-JDBC解決數據庫讀寫分離查詢延時問題

它使用客戶端直連數據庫,以 jar 包形式提供服務,無需額外部署和依賴,可理解為增強版的 JDBC 驅動,完全兼容 JDBC 和各種 ORM 框架。

  • 適用於任何基於 JDBC 的 ORM 框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template 或直接使用 JDBC。
  • 支持任何第三方的數據庫連接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP 等。
  • 支持任意實現 JDBC 規範的數據庫,目前支持 MySQL,Oracle,SQLServer,PostgreSQL 以及任何遵循 SQL92 標準的數據庫。

讀寫分離特性

  • 提供了一主多從的讀寫分離配置,可獨立使用,也可配合分庫分表使用。
  • 同個調用線程,執行多條語句,其中一旦發現有非讀操作,後續所有讀操作均從主庫讀取。
  • Spring命名空間。
  • 基於Hint的強制主庫路由。

ShardingSphere-JDBC 官方提供 HintManager 分片鍵值管理器, 通過調用hintManager.setMasterRouteOnly() 強制路由到主庫查詢,這樣就解決了數據延時問題,無論什麼時候都能夠從主庫 Master 查詢到最新數據,而不用走從庫查詢。

<code>HintManager hintManager = HintManager.etInstance() ; 
hintManager.setMasterRouteOnly();
/<code>

實際案例

核心依賴

<code><dependency>
<groupId>io.shardingjdbc</groupId>
<artifactId>sharding-jdbc-core</artifactId>
<version>${sharding-jdbc.version}</version>
</dependency>/<code>

數據庫配置

<code>sharding:
jdbc:
data-sources:
mvip:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://${ha.basedb.mvip.ip}:${ha.basedb.mvip.port}/unicom_portal?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false
username: ${ha.basedb.mvip.username}
password: ${ha.basedb.mvip.password}
svip:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://${ha.basedb.svip.ip}:${ha.basedb.svip.port}/unicom_portal?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false
username: ${ha.basedb.svip.username}
password: ${ha.basedb.svip.password}
master-slave-rule:
name: ds_ms
master-data-source-name: mvip
slave-data-source-names: svip
load-balance-algorithm-type: round_robin/<code>

源初始化配置類

<code>@Data 

@ConfigurationProperties(prefix = "sharding.jdbc")
public class MasterSlaveConfig {
private Map<String, DruidDataSource> dataSources = new HashMap<>();
private MasterSlaveRuleConfiguration masterSlaveRule;
}
@ConditionalOnClass(DruidDataSource.class)
@EnableConfigurationProperties(MasterSlaveConfig.class)
@ConditionalOnProperty({
"sharding.jdbc.data-sources.mvip.url",
"sharding.jdbc.master-slave-rule.master-data-source-name"
})
static class ShardingDruid extends DruidConfig {
@Autowired
private MasterSlaveConfig masterSlaveConfig;
@Bean("masterSlaveDataSource")
public DataSource dataSource() throws SQLException {
masterSlaveConfig.getDataSources().forEach((k, v) -> configDruidParams(v));
Map<String, DataSource> dataSourceMap = Maps.newHashMap();
dataSourceMap.putAll(masterSlaveConfig.getDataSources());
DataSource dataSource = MasterSlaveDataSourceFactory.createDataSource(dataSourceMap, masterSlaveConfig.getMasterSlaveRule(), Maps.newHashMap());
return dataSource;
}
@Bean
public PlatformTransactionManager txManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
private void configDruidParams(DruidDataSource druidDataSource) {
druidDataSource.setMaxActive(20);
druidDataSource.setInitialSize(1);
// 配置獲取連接等待超時的時間
druidDataSource.setMaxWait(10000);
druidDataSource.setMinIdle(1);
// 配置間隔多久才進行一次檢測,檢測需要關閉的空閒連接,單位是毫秒
druidDataSource.setTimeBetweenEvictionRunsMillis(60000);
// 配置一個連接在池中最小生存的時間,單位是毫秒 超過這個時間每次會回收默認3個連接
druidDataSource.setMinEvictableIdleTimeMillis(30000);
// 線上配置的mysql斷開閒置連接時間為1小時,數據源配置回收時間為3分鐘,以最後一次活躍時間開始算
druidDataSource.setMaxEvictableIdleTimeMillis(180000);

// 連接最大存活時間,默認是-1(不限制物理連接時間),從創建連接開始計算,如果超過該時間,則會被清理
druidDataSource.setPhyTimeoutMillis(15000);
druidDataSource.setValidationQuery("select 1");
druidDataSource.setTestWhileIdle(true);
druidDataSource.setTestOnBorrow(false);
druidDataSource.setTestOnReturn(false);
druidDataSource.setPoolPreparedStatements(true);
druidDataSource.setMaxOpenPreparedStatements(20);
druidDataSource.setUseGlobalDataSourceStat(true);
druidDataSource.setKeepAlive(true);
druidDataSource.setRemoveAbandoned(true);
druidDataSource.setRemoveAbandonedTimeout(180);
try {
druidDataSource.setFilters("stat,slf4j");
List filterList = new ArrayList<>();
filterList.add(wallFilter());
druidDataSource.setProxyFilters(filterList);
} catch (SQLException e) {
e.printStackTrace();
}
}
}/<code>


強制路由到主庫查詢關鍵代碼:

<code>public ArticleEntity getWithMasterDB(Long id, String wid) {
HintManager hintManager = HintManager.getInstance() ;
hintManager.setMasterRouteOnly();
ArticleEntity article = baseMapper.queryObject(id, wid);
}/<code>

通過強制路由到主庫查詢有個風險,對於更新並實時查詢業務場景比較多,如果都切到主庫查詢,勢必會對主庫服務器性能造成影響,可能還會影響到主從數據同步,所以要根據實際業務場景評估採用這種方式帶來的系統性能問題。

另外,如果業務層面可以做妥協的話,儘量減少這種更新並實時查詢方式,一種思路是實時更新庫,利用 Java Future 特性異步查詢(例如更新後,睡眠1-2秒再查詢),偽代碼如下:

<code>
Callable c1 = new Callable(){
@Override
public String call() throws Exception {
ArticleEntity articleEntity = null
try {
Thread.sleep(2000);
articleEntity = articleService.get(id)
} catch (InterruptedException e) {
e.printStackTrace();
}
return articleEntity;
}
};
FutureTask<ArticleEntity> f = new FutureTask<ArticleEntity>(c1);
new Thread(f).start();
ArticleEntity article = f.get()/<code>


後臺私信回覆 1024 免費領取微服務、SpringCloud&SpringBoot,微信小程序、Java面試等視頻資料。


分享到:


相關文章: