AliSQL情人節版本Release:開源Sequence Engine

摘要: AliSQL開源Sequence Engine; 單調遞增的唯一值,是在持久化資料庫系統中常見的需求,無論是單節點中的業務主鍵,還是分散式系統中的全局唯一值,亦或是多系統中的冪等控制。

AliSQL開源Sequence Engine

Introduction

單調遞增的唯一值,是在持久化資料庫系統中常見的需求,無論是單節點中的業務主鍵,還是分散式系統中的全局唯一值,亦或是多系統中的冪等控制。不同的資料庫系統有不同的實現方法,比如MySQL提供的AUTO_INCREMENT,Oracle,SQL Server提供SEQUENCE等。

在MySQL資料庫中,如果業務系統希望封裝唯一值,比如增加日期,用戶等信息,AUTO_INCREMENT的方法會帶來很大的不便,在實際的系統設計的時候, 也存在不同的折中方法,比如:

  • 序列值由Application或者Proxy來生成,不過弊端很明顯,狀態帶到應用端,增加了擴容和縮容的複雜度。

  • 序列值由資料庫通過模擬的表來生成,但需要中間件來封裝和簡化獲取唯一值的邏輯。

AliSQL自主實現了SEQUENCE ENGINE,通過引擎的設計方法,儘可能的兼容其他資料庫的使用方法,簡化獲取序列值複雜度。

Github開源地址:alibaba/AliSQL: AliSQL is a MySQL branch originated from Alibaba Group. Fetch document from Release Notes at bottom.

Description

AliSQL開源的SEQUENCE,實現了MySQL存儲引擎的設計介面,但底層的數據仍然使用現有的存儲引擎,比如InnoDB或者MyISAM來保存持久化數據,以便儘可能的保證現有的外圍工具比如XtraBackup等工具的兼容,所以SEQUENCE ENGINE僅僅是一個邏輯引擎。

對sequence對象的訪問通過SEQUENCE handler介面,這一層邏輯引擎主要實現NEXTVAL的滾動,CACHE的管理等,最後透傳給底層的基表數據引擎,實現最終的數據訪問。

下面我們透過語法來看下AliSQL SEQUENCE的使用。

Syntax

1. CREATE SEQUENCE Syntax:

CREATE SEQUENCE [IF NOT EXISTS] schema.sequence_name [START WITH <constant>] [MINVALUE <constant>] [MAXVALUE <constant>] [INCREMENT BY <constant>] [CACHE <constant> | NOCACHE] [CYCLE | NOCYCLE] ;

SEQUENCE OPTIONS:

  • START

    Sequence的起始值

  • MINVALUE

    Sequence的最小值,如果這一輪結束並且是cycle的,那麼下一輪將從MINVALUE開始

  • MAXVALUE

    Sequence的最大值,如果到最大值並且是nocycle的,那麼將會得到以下報錯:

    ERROR HY000: Sequence db.seq has been run out.

  • INCREMENT BY

    Sequence的步長

  • CACHE/NOCACHE

    Cache的大小,為了性能考慮,可以設置cache的size比較大,但如果遇到實例重啟,cache內的值會丟失

  • CYCLE/NOCYCLE

    表示sequence如果用完了後,是否允許從MINVALUE重新開始

例如:

create sequence s start with 1 minvalue 1 maxvalue 9999999 increment by 1 cache 20 cycle;

2. SHOW SEQUENCE Syntax

SHOW CREATE [TABLE|SEQUENCE] schema.sequence_name;CREATE SEQUENCE schema.sequence_name ( `currval` bigint(21) NOT NULL COMMENT current value, `nextval` bigint(21) NOT NULL COMMENT next value, `minvalue` bigint(21) NOT NULL COMMENT min value, `maxvalue` bigint(21) NOT NULL COMMENT max value, `start` bigint(21) NOT NULL COMMENT start value, `increment` bigint(21) NOT NULL COMMENT increment value, `cache` bigint(21) NOT NULL COMMENT cache size, `cycle` bigint(21) NOT NULL COMMENT cycle state, `round` bigint(21) NOT NULL COMMENT already how many round) ENGINE=InnoDB DEFAULT CHARSET=latin1

由於SEQUENCE是通過真正的引擎表來保存的,所以SHOW COMMAND看到仍然是engine table。

3. QUERY STATEMENT Syntax

SELECT [NEXTVAL | CURRVAL | *] FROM schema.sequence_name;SELECT [NEXTVAL | CURRVAL | *] FOR schema.sequence_name;

這裡支持兩種訪問方式,FROM和FOR:

  • FROM clause: 兼容正常的SELECT查詢語句,返回的結果是基表的數據,不迭代NEXTVAL。
  • FOR clause:兼容SQL Server的方法,返回的結果是迭代後NEXTVAL的值。

mysql> select * from s;+---------+---------+----------+---------------------+-------+-----------+-------+-------+-------+| currval | nextval | minvalue | maxvalue | start | increment | cache | cycle | round |+---------+---------+----------+---------------------+-------+-----------+-------+-------+-------+| 0 | 30004 | 1 | 9223372036854775807 | 1 | 1 | 10000 | 0 | 0 |+---------+---------+----------+---------------------+-------+-----------+-------+-------+-------+1 row in set (0.00 sec)mysql> select * for s;+---------+---------+----------+---------------------+-------+-----------+-------+-------+-------+| currval | nextval | minvalue | maxvalue | start | increment | cache | cycle | round |+---------+---------+----------+---------------------+-------+-----------+-------+-------+-------+| 0 | 20014 | 1 | 9223372036854775807 | 1 | 1 | 10000 | 0 | 0 |+---------+---------+----------+---------------------+-------+-----------+-------+-------+-------+

4. 兼容性

因為要兼容MYSQLDUMP的備份方式,所以支持另外一種CREATE SEQUENCE方法,即:通過創建SEQUENCE表和INSERT一行初始記錄的方式, 比如:

CREATE SEQUENCE schema.sequence_name ( `currval` bigint(21) NOT NULL COMMENT current value, `nextval` bigint(21) NOT NULL COMMENT next value, `minvalue` bigint(21) NOT NULL COMMENT min value, `maxvalue` bigint(21) NOT NULL COMMENT max value, `start` bigint(21) NOT NULL COMMENT start value, `increment` bigint(21) NOT NULL COMMENT increment value, `cache` bigint(21) NOT NULL COMMENT cache size, `cycle` bigint(21) NOT NULL COMMENT cycle state, `round` bigint(21) NOT NULL COMMENT already how many round) ENGINE=InnoDB DEFAULT CHARSET=latin1INSERT INTO schema.sequence_name VALUES(0,0,1,9223372036854775807,1,1,10000,1,0);COMMIT;

但強烈建議使用native的CREATE SEQUENCE方法。

5. 語法限制

  • Sequence不支持subquery和join
  • FOR clause只支持sequence表,普通引擎表不支持
  • 可以使用SHOW CREATE TABLE或者SHOW CREATE SEQUENCE來訪問SEQUENCE結構,但不能使用SHOW CREATE SEQUENCE訪問普通表
  • 不支持CREATE TABLE的時候指定SEQUENCE引擎,sequence表只能通過CREATE SEQUENCE的語法來創建

High level architecture

1. Sequence initialization

Sequence對象的創建,會轉化成擁有固定[CURRVAL, NEXTVAL, MINVALUE, MAXVALUE, START, INCREMENT, CACHE, CYCLE, ROUND]這9個欄位的引擎表,並根據CREATE SEQUENCE clause的定義,初始化了一條數據,所以sequence對象實質上是擁有一條記錄的存儲引擎表,SLAVE複製的BINLOG使用CREATE SEQUENCE ...語句生成的QUERY EVENT來完成。

2. Sequence interface

SEQUENCE handler實現了一部分的handler interface,並定義了兩個重要的屬性,SEQUENCE_SHARE和BASE_TABLE_FILE,SEQUENCE_SHARE保存著共享的sequence對象屬性和CACHE的值,NEXTVAL的值首先從cache中獲取,只有在cache使用完了,才會查詢基表。

BASE_TABLE_FILE是基表的handler,對持久化的數據的訪問和修改,都通過BASE_TABLE_FILE handler進行訪問。

3. Sequence cache

Sequence對象的CACHE值保存在SEQUENCE_SHARE中,使用SEQUENCE_SHARE::MUTEX進行保護,所有對cache的訪問是串列的。比如cache size是20,那麼SEQUENCE_SHARE中只是保存一個cache_end值,當訪問的NEXTVAL到了cache_end,就會從基表中獲取下一個batch放到cache中。NEXTVAL根據INCREMENT BY設置的步長進行迭代。

4. Sequence update

當cache用完了之後,會從基表中獲取下一個batch,這樣會更新基表中的記錄,查詢會轉化成更新語句,

其更新的主要步驟如下:

  1. 升級SEQUENCE的MDL_SHARE_READ METADATA LOCK 到 MDL_SHARE_WRITE級別
  2. 持有GLOBAL MDL_INTENSIVE_EXCLUSIVE METADATA LOCK
  3. 開啟AUTONOMOUS TRANSACTION
  4. 更新記錄並生成BINLOG EVENT
  5. 持有COMMIT METADATA LOCK
  6. XA提交AUTONOMOUS TRANSACTION 並釋放MDL鎖

5. Autonomous transaction

因為nextval不支持ROLLBACK重用,所以必須重啟一個自治事務來脫離事務上下文,

其步驟如下:

  1. 備份當前基表引擎的事務上下文
  2. 備份當前BINLOG引擎的上下文
  3. SEQUENCE和BINLOG分別註冊AUTONOMOUS TRANSACTION
  4. 等更新完成,XA提交AUTONOMOUS TRANSACTION
  5. 還原當前事務上下文

6. Sequence read only

因為SEQUENCE的SELECT語句會轉換成UPDATE語句,所以SELECT NEXTVAL FOR s statement須持有 MDL_SHARE_WRITE 和 GLOBAL MDL_INTENSIVE_EXCLUSIVE METADATA LOCK 進行,以便在READ ONLY的時候,阻塞對sequence對象的訪問。

7. Skip cache

這裡指兩種CACHE:

  • 一種是SEQUENCE的CACHE,可以使用SELECT NEXTVAL FORM Sequence_name來skip。
  • 另外一種是QUERY CACHE,所有的SEQUENCE都設置了不支持QUERY CACHE,這樣避免由於QUERY CACHE導致NEXTVAL沒有迭代。

8. Sequence backup

由於SEQUENCE是通過真正的引擎表來保存的,所以類似XtraBackup這樣的物理備份可以直接使用,而類似於MYSQLDUMP這樣的邏輯備份,SEQUENCE會備份成CREATE SEQUENCE語句和INSERT語句的組合來完成。

Next Release

本次開源了部分功能,下一次release將繼續開源SEQUENCE的部分功能:

  • 支持CURRVAL的訪問,CURRVAL表示當前session的上一次的NEXTVAL訪問的值。
  • 兼容更多資料庫的訪問方法,比如:

Oracle Syntax: SELECT sequence_name.nextval FROM DUAL;PostgreSQL Syntax: nextval(regclass); currval(regclass); setval(regclass, bigint);

Usage Scenario

1. 更具有業務含義的主鍵設計 .

例如:[八位日期 + 四位USER ID + sequence_number]的流水業務單據號的設計格式,可以通過SELECT NEXTVAL FOR Sequence和應用封裝的方式實現,相比較無意義的id數字,這種格式會帶來幾個優勢:

  • 保持和時間同步的有序性,有利於數據的歸檔,比如可以直接使用這種ID來進行按日/月/年RANGE分區, 無縫使用MySQL的partition特性
  • 增加USER的id信息,可以作為天然的分庫分表邏輯位, 提升數據節點可擴展性
  • 保持數字的有序性,保證InnoDB這種聚簇索引表的插入性能穩定

業界目前採用的設計方法:

  • Booking使用了AUTO_INCREMENT的方法, 先插入一個無業務含義的數字,然後使用last_insert_id()方法獲取ID值,最後在業務邏輯中使用這個ID值。 其劣勢就是必須先插入,並沒有辦法再修改這個無業務含義的id。
  • Twitter採用了另外一種格式,[41 bits timestamp + 10 bits configured machine ID + 12 bits sequence number], sequence number的生成機制沒有透露,machine ID的的設計,使用Zookeeper來管理的machine ID或者機器的MAC address。
  • UUID的方法,這種方式生成了一個隨機的唯一值,嚴重影響了插入的性能,並且增大了索引大小,降低了命中率,沒有任何優勢。

2. 分散式節點的唯一值設計

分散式SEQUENCE生成:

  • 可以為每一個節點設計sequence,比如為每個節點設計不同的INCREMENT BY步長來達到MySQL AUTO_INCREMENT中,設置auto_increment_increment和auto_increment_offset的效果,但相比較auto increment的全局配置,並且保存在my.cnf中的方法,SEQUENCE可以把這些配置當做sequence對象的屬性持久化保存下來,優勢明顯。但不推薦使用這種方法來設計唯一值,會給運維留下不少坑。
  • 使用類似twitter的方法,每一個節點上創建sequence,然後增加節點信息到sequence number中,生成唯一值。

集中式SEQUENCE生成:

  • 對於分散式節點中的ID需求,使用獨立的集中式的sequence服務來生成,但如果要保證持續可用,sequence服務仍然需要設計成多節點的,比如Flickr的Ticket Servers設計:

Sequence服務節點上創建Ticket表:

CREATE TABLE `Tickets64` ( `id` bigint(20) unsigned NOT NULL auto_increment, `stub` char(1) NOT NULL default , PRIMARY KEY (`id`), UNIQUE KEY `stub` (`stub`)) ENGINE=MyISAM+-------------------+------+| id | stub |+-------------------+------+| 72157623227190423 | a |+-------------------+------+

使用以下語句,生成ID值:

REPLACE INTO Tickets64 (stub) VALUES (a);SELECT LAST_INSERT_ID();

因為PHOTOS,COMMENTS,FAVORITES,TAGS都需要ID, 所以會建不同的ticket表來完成,為了保持持續可用,採用了:

TicketServer1:auto-increment-increment = 2auto-increment-offset = 1TicketServer2:auto-increment-increment = 2auto-increment-offset = 2

來保證高可用。

如果使用sequence對象,可以大大簡化ID的獲取邏輯,並更加安全。

相關文章

  • 【阿里雲資訊】性能大幅提升70%,阿里雲AliSQL邀請…
  • 快訊:阿里雲正式開啟AliSQL邀測 開源版本為AliS…
  • 【已更新內測入口】阿里雲宣布開放開源 AliSQL 數據…

推薦閱讀:

關於高並發解決問題的一點總結
從零開始實現資料庫系統(3)——並發控制
劉寅:TiDB 工具鏈和生態
資料庫(一)
循序漸進學習如何在 MariaDB 中配置主從複製

TAG:資料庫 | SQL | 開源 |