在 TiDB 中,底層索引結構爲 LSM-Tree,以下圖:java
開篇mysql
世界級的開源分佈式數據庫 TiDB 自 2016 年 12 月正式發佈第一個版本以來,業內諸多公司逐步引入使用,並取得普遍承認。算法
對於互聯網公司,數據存儲的重要性不言而喻。在 NewSQL 數據庫出現以前,通常採用單機數據庫(好比 MySQL )做爲存儲,隨着數據量的增長,「分庫分表」是遲早面臨的問題,即便有諸如 MyCat、ShardingJDBC 等優秀的中間件,「分庫分表」仍是給 RD 和 DBA 帶來較高的成本; NewSQL 數據庫出現後,因爲它不只有 NoSQL 對海量數據的管理存儲能力、還支持傳統關係數據庫的 ACID 和 SQL,因此對業務開發來講,存儲問題已經變得更加簡單友好,進而能夠更專一於業務自己。而 TiDB,正是 NewSQL 的一個傑出表明!spring
站在業務開發的視角,TiDB 最吸引人的幾大特性是:sql
支持 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)性能更穩定,耗時也更低。