做者:陳維,轉轉優品技術部 RD。
世界級的開源分佈式數據庫 TiDB 自 2016 年 12 月正式發佈第一個版本以來,業內諸多公司逐步引入使用,並取得普遍承認。java
對於互聯網公司,數據存儲的重要性不言而喻。在 NewSQL 數據庫出現以前,通常採用單機數據庫(好比 MySQL)做爲存儲,隨着數據量的增長,「分庫分表」是遲早面臨的問題,即便有諸如 MyCat、ShardingJDBC 等優秀的中間件,「分庫分表」仍是給 RD 和 DBA 帶來較高的成本;NewSQL 數據庫出現後,因爲它不只有 NoSQL 對海量數據的管理存儲能力、還支持傳統關係數據庫的 ACID 和 SQL,因此對業務開發來講,存儲問題已經變得更加簡單友好,進而能夠更專一於業務自己。而 TiDB,正是 NewSQL 的一個傑出表明!mysql
站在業務開發的視角,TiDB 最吸引人的幾大特性是:算法
基於這幾大特性,TiDB 在業務開發中是值得推廣和實踐的,可是,它畢竟不是傳統的關係型數據庫,以至咱們對關係型數據庫的一些使用經驗和積累,在 TiDB 中是存在差別的,現主要闡述「事務」和「查詢」兩方面的差別。spring
在 TiDB 中執行的事務 b,返回影響條數是 1(認爲已經修改爲功),可是提交後查詢,status 卻不是事務 b 修改的值,而是事務 a 修改的值。sql
可見,MySQL 事務和 TiDB 事務存在這樣的差別:數據庫
MySQL 事務中,能夠經過影響條數,做爲寫入(或修改)是否成功的依據;而在 TiDB 中,這倒是不可行的!緩存
做爲開發者咱們須要考慮下面的問題:異步
對於 MySQL,當更新某條記錄時,會先獲取該記錄對應的行級鎖(排他鎖),獲取成功則進行後續的事務操做,獲取失敗則阻塞等待。分佈式
對於 TiDB,使用 Percolator 事務模型:能夠理解爲樂觀鎖實現,事務開啓、事務中都不會加鎖,而是在提交時才加鎖。參見 這篇文章(TiDB 事務算法)。性能
其簡要流程以下:
在事務提交的 PreWrite 階段,當「鎖檢查」失敗時:若是開啓衝突重試,事務提交將會進行重試;若是未開啓衝突重試,將會拋出寫入衝突異常。
可見,對於 MySQL,因爲在寫入操做時加上了排他鎖,變相將並行事務從邏輯上串行化;而對於 TiDB,屬於樂觀鎖模型,在事務提交時才加鎖,並使用事務開啓時獲取的「全局時間戳」做爲「鎖檢查」的依據。
因此,在業務層面避免 TiDB 事務差別的本質在於避免鎖衝突,即,當前事務執行時,不產生別的事務時間戳(無其餘事務並行)。處理方式爲事務串行化。
在業務層,能夠藉助分佈式鎖,實現串行化處理,以下:
在 Spring 生態下,spring-tx 中定義了統一的事務管理器接口:PlatformTransactionManager
,其中有獲取事務(getTransaction)、提交(commit)、回滾(rollback)三個基本方法;使用裝飾器模式,事務串行化組件可作以下設計:
其中,關鍵點有:
隱藏複雜的事務重寫邏輯,暴露簡單友好的 API:
在 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
)性能更穩定,耗時也更低。