摘要:而批處理,可以復用一條簡單,實現批量數據的寫入或更新,為系統帶來更低更穩定的耗時。批處理的簡要流程說明如下經業務中實踐,使用批處理方式的寫入或更新,比常規或性能更穩定,耗時也更低。
作者:陳維,轉轉優品技術部 RD。開篇
世界級的開源分布式數據庫 TiDB 自 2016 年 12 月正式發布第一個版本以來,業內諸多公司逐步引入使用,并取得廣泛認可。
對于互聯網公司,數據存儲的重要性不言而喻。在 NewSQL 數據庫出現之前,一般采用單機數據庫(比如 MySQL)作為存儲,隨著數據量的增加,“分庫分表”是早晚面臨的問題,即使有諸如 MyCat、ShardingJDBC 等優秀的中間件,“分庫分表”還是給 RD 和 DBA 帶來較高的成本;NewSQL 數據庫出現后,由于它不僅有 NoSQL 對海量數據的管理存儲能力、還支持傳統關系數據庫的 ACID 和 SQL,所以對業務開發來說,存儲問題已經變得更加簡單友好,進而可以更專注于業務本身。而 TiDB,正是 NewSQL 的一個杰出代表!
站在業務開發的視角,TiDB 最吸引人的幾大特性是:
支持 MySQL 協議(開發接入成本低);
100% 支持事務(數據一致性實現簡單、可靠);
無限水平拓展(不必考慮分庫分表)。
基于這幾大特性,TiDB 在業務開發中是值得推廣和實踐的,但是,它畢竟不是傳統的關系型數據庫,以致我們對關系型數據庫的一些使用經驗和積累,在 TiDB 中是存在差異的,現主要闡述“事務”和“查詢”兩方面的差異。
TiDB 事務和 MySQL 事務的差異 MySQL 事務和 TiDB 事務對比在 TiDB 中執行的事務 b,返回影響條數是 1(認為已經修改成功),但是提交后查詢,status 卻不是事務 b 修改的值,而是事務 a 修改的值。
可見,MySQL 事務和 TiDB 事務存在這樣的差異:
MySQL 事務中,可以通過影響條數,作為寫入(或修改)是否成功的依據;而在 TiDB 中,這卻是不可行的!
作為開發者我們需要考慮下面的問題:
同步 RPC 調用中,如果需要嚴格依賴影響條數以確認返回值,那將如何是好?
多表操作中,如果需要嚴格依賴某個主表數據更新結果,作為是否更新(或寫入)其他表的判斷依據,那又將如何是好?
原因分析及解決方案對于 MySQL,當更新某條記錄時,會先獲取該記錄對應的行級鎖(排他鎖),獲取成功則進行后續的事務操作,獲取失敗則阻塞等待。
對于 TiDB,使用 Percolator 事務模型:可以理解為樂觀鎖實現,事務開啟、事務中都不會加鎖,而是在提交時才加鎖。參見 這篇文章(TiDB 事務算法)。
其簡要流程如下:
在事務提交的 PreWrite 階段,當“鎖檢查”失敗時:如果開啟沖突重試,事務提交將會進行重試;如果未開啟沖突重試,將會拋出寫入沖突異常。
可見,對于 MySQL,由于在寫入操作時加上了排他鎖,變相將并行事務從邏輯上串行化;而對于 TiDB,屬于樂觀鎖模型,在事務提交時才加鎖,并使用事務開啟時獲取的“全局時間戳”作為“鎖檢查”的依據。
所以,在業務層面避免 TiDB 事務差異的本質在于避免鎖沖突,即,當前事務執行時,不產生別的事務時間戳(無其他事務并行)。處理方式為事務串行化。
TiDB 事務串行化在業務層,可以借助分布式鎖,實現串行化處理,如下:
基于 Spring 和分布式鎖的事務管理器拓展在 Spring 生態下,spring-tx 中定義了統一的事務管理器接口:PlatformTransactionManager,其中有獲取事務(getTransaction)、提交(commit)、回滾(rollback)三個基本方法;使用裝飾器模式,事務串行化組件可做如下設計:
其中,關鍵點有:
超時時間:為避免死鎖,鎖必須有超時時間;為避免鎖超時導致事務并行,事務必須有超時時間,而且鎖超時時間必須大于事務超時時間(時間差最好在秒級)。
加鎖時機:TiDB 中“鎖檢查”的依據是事務開啟時獲取的“全局時間戳”,所以加鎖時機必須在事務開啟前。
事務模板接口設計隱藏復雜的事務重寫邏輯,暴露簡單友好的 API:
TiDB 查詢和 MySQL 的差異在 TiDB 使用過程中,偶爾會有這樣的情況,某幾個字段建立了索引,但是查詢過程還是很慢,甚至不經過索引檢索。
索引混淆型(舉例)表結構:
CREATE TABLE `t_test` ( `id` bigint(20) NOT NULL DEFAULT "0" COMMENT "主鍵id", `a` int(11) NOT NULL DEFAULT "0" COMMENT "a", `b` int(11) NOT NULL DEFAULT "0" COMMENT "b", `c` int(11) NOT NULL DEFAULT "0" COMMENT "c", PRIMARY KEY (`id`), KEY `idx_a_b` (`a`,`b`), KEY `idx_c` (`c`) ) ENGINE=InnoDB;
查詢:如果需要查詢 (a=1 且 b=1)或 c=2 的數據,在 MySQL 中,sql 可以寫為:SELECT id from t_test where (a=1 and b=1) or (c=2);,MySQL 做查詢優化時,會檢索到 idx_a_b 和 idx_c 兩個索引;但是在 TiDB(v2.0.8-9)中,這個 sql 會成為一個慢 SQL,需要改寫為:
SELECT id from t_test where (a=1 and b=1) UNION SELECT id from t_test where (c=2);
小結:導致該問題的原因,可以理解為 TiDB 的 sql 解析還有優化空間。
冷熱數據型(舉例)表結構:
CREATE TABLE `t_job_record` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT "主鍵id", `job_code` varchar(255) NOT NULL DEFAULT "" COMMENT "任務code", `record_id` bigint(20) NOT NULL DEFAULT "0" COMMENT "記錄id", `status` tinyint(3) NOT NULL DEFAULT "0" COMMENT "執行狀態:0 待處理", `execute_time` bigint(20) NOT NULL DEFAULT "0" COMMENT "執行時間(毫秒)", PRIMARY KEY (`id`), KEY `idx_status_execute_time` (`status`,`execute_time`), KEY `idx_record_id` (`record_id`) ) ENGINE=InnoDB COMMENT="異步任務job"
數據說明:
a. 冷數據,status=1 的數據(已經處理過的數據);
b. 熱數據,status=0 并且 execute_time<= 當前時間 的數據。
慢查詢:對于熱數據,數據量一般不大,但是查詢頻度很高,假設當前(毫秒級)時間為:1546361579646,則在 MySQL 中,查詢 sql 為:
SELECT * FROM t_job_record where status=0 and execute_time<= 1546361579646
這個在 MySQL 中很高效的查詢,在 TiDB 中雖然也可從索引檢索,但其耗時卻不盡人意(百萬級數據量,耗時百毫秒級)。
原因分析:在 TiDB 中,底層索引結構為 LSM-Tree,如下圖:
當從內存級的 C0 層查詢不到數據時,會逐層掃描硬盤中各層;且 merge 操作為異步操作,索引數據更新會存在一定的延遲,可能存在無效索引。由于逐層掃描和異步 merge,使得查詢效率較低。
優化方式:盡可能縮小過濾范圍,比如結合異步 job 獲取記錄頻率,在保證不遺漏數據的前提下,合理設置 execute_time 篩選區間,例如 1 小時,sql 改寫為:
SELECT * FROM t_job_record where status=0 and execute_time>1546357979646 and execute_time<= 1546361579646
優化效果:耗時 10 毫秒級別(以下)。
關于查詢的啟發在基于 TiDB 的業務開發中,先摒棄傳統關系型數據庫帶來的對 sql 先入為主的理解或經驗,謹慎設計每一個 sql,如 DBA 所提倡:設計 sql 時務必關注執行計劃,必要時請教 DBA。
和 MySQL 相比,TiDB 的底層存儲和結構決定了其特殊性和差異性;但是,TiDB 支持 MySQL 協議,它們也存在一些共同之處,比如在 TiDB 中使用“預編譯”和“批處理”,同樣可以獲得一定的性能提升。
服務端預編譯在 MySQL 中,可以使用 PREPARE stmt_name FROM preparable_stm 對 sql 語句進行預編譯,然后使用 EXECUTE stmt_name [USING @var_name [, @var_name] ...] 執行預編譯語句。如此,同一 sql 的多次操作,可以獲得比常規 sql 更高的性能。
mysql-jdbc 源碼中,實現了標準的 Statement 和 PreparedStatement 的同時,還有一個ServerPreparedStatement 實現,ServerPreparedStatement 屬于PreparedStatement的拓展,三者對比如下:
容易發現,PreparedStatement 和 Statement 的區別主要區別在于參數處理,而對于發送數據包,調用服務端的處理邏輯是一樣(或類似)的;經測試,二者速度相當。其實,PreparedStatement 并不是服務端預處理的;ServerPreparedStatement 才是真正的服務端預處理,速度也較 PreparedStatement 快;其使用場景一般是:頻繁的數據庫訪問,sql 數量有限(有緩存淘汰策略,使用不宜會導致兩次 IO)。
批處理對于多條數據寫入,常用 sql 為 insert … values (…),(…);而對于多條數據更新,亦可以使用 update … case … when… then… end 來減少 IO 次數。但它們都有一個特點,數據條數越多,sql 越加復雜,sql 解析成本也更高,耗時增長可能高于線性增長。而批處理,可以復用一條簡單 sql,實現批量數據的寫入或更新,為系統帶來更低、更穩定的耗時。
對于批處理,作為客戶端,java.sql.Statement 主要定義了兩個接口方法,addBatch 和 executeBatch 來支持批處理。
批處理的簡要流程說明如下:
經業務中實踐,使用批處理方式的寫入(或更新),比常規 insert … values(…),(…)(或 update … case … when… then… end)性能更穩定,耗時也更低。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/17883.html
摘要:業務延遲和錯誤量對比接入數據庫后業務邏輯層服務接口耗時穩定無抖動,且沒有發生丟棄的情況上圖錯誤大多由數據訪問層服務隊列堆積發生請求丟棄造成。 作者:孫玄,轉轉公司首席架構師;陳東,轉轉公司資深工程師;冀浩東,轉轉公司資深 DBA。 公司及業務架構介紹 轉轉二手交易網 —— 把家里不用的東西賣了變成錢,一個幫你賺錢的網站。由騰訊與 58 集團共同投資。為海量用戶提供一個有擔保、便捷的二手...
閱讀 3061·2021-10-27 14:16
閱讀 2877·2021-09-24 10:33
閱讀 2284·2021-09-23 11:21
閱讀 3228·2021-09-22 15:14
閱讀 810·2019-08-30 15:55
閱讀 1674·2019-08-30 15:53
閱讀 1740·2019-08-29 11:14
閱讀 2190·2019-08-28 18:11