關注我,獲取更多分享。也歡迎你分享給更多的朋友。
背景
MySQL中有各種各樣的自增ID。例如我們最常見的表的自增ID,Xid,事務的ID,線程的ID,表的編號ID,binlog日誌文件的ID等等。這些ID都是有它自己的增長規律的,並不是隨機生成的。MySQL的整體功能設計,有很多地方都依賴於這些ID的增長規律。
接下來我們選擇幾個經常遇到的來分析一下。
自增ID的數據類型
我們在使用自增ID的時候,定義自增ID欄位的類型為int,而int類型是一個大類,它有可以細分為tinyint、smallint、mediumit、int、bigint5中類型。
每一種int類型的取值範圍是不同的,如下表格所示:
單位換算規則
上面的表格中,我們提到的佔用空間的大小問題,不同的整型數據類型所佔用的磁碟存儲空間是不同的。具體的換算用到的單位如下:
1PB(拍字節)=1024TB(太字節),簡寫為T1TB=1024GB(吉字節),簡寫為G1GB=1024MB(兆字節),簡寫為M1MB=1024KB(千字節),簡寫為K1KB=1024Byte(字節),簡寫為B1Byte=8Bit(位),簡寫為b1Bit = 1個二進位數字,值為0或者1自增ID取值範圍
結合上面的計算轉換關係。我們使用tinyint來舉例說明它的取值範圍是怎麼計算來的。
tinyint佔用1個byte,也就是8個bit,1byte=8bit,即為:一個字節等於8位。
無符號位的計算方式
一個8位的無符號二進位能存放的二進位數值範圍是[00000000~11111111],將其轉換為十進位就是[0,255]。
下面說一下轉換的過程是怎麼樣的。二進位中的數據非0即為1,逢二進一,00000000為最小的二進位數,11111111為最大的二進位數。
這就是無符號的時候,一個8位的二進位數所能存儲數據範圍轉換為十進位數據存儲範圍的過程。所以一個8位的二進位數能存儲的無符號的十進位數的範圍是[0,255]
有符號位的計算方式
那有符號的時候,該怎麼計算呢?
在二進位中,正號用0表示,負號用1表示,並且需要把正負號放在二進位的最高位,也就是最左邊的位置,剩餘右邊的7個位置用來表示二進位的具體數值。那麼一個有正負號的8位二進位取值範圍就是[11111111,01111111]。
去掉左側第一位用來標記正負號的位置,還剩餘7個位置,這7個位置都是1的時候是最大的二進位數。如果前面使用一個負號(此時用1表示)就是最小的二進位數,如果前面增加一個正號(此時用0表示)就是最大的二進位數。所以一個有正負號的8位的二進位數的取值範圍為:[11111111,01111111]。
怎麼有符號的最小值是-127,而不是-128呢?在計算機中,表示負值是用補碼。
為什麼有符號的tinyint的最小值是-128?雖然「-0」也是「0」,但根據正、反、補碼體系,「-0」的補碼和「+0」是不同的,這樣就出現兩個補碼代表一個數值的情況。為了將補碼與數字一一對應,所以人為規定「0」一律用「+0」代表。同時為了充分利用資源,就將原來本應該表示「-0」的補碼規定為代表-128。
int和int(11)有什麼區別
這裡插一個題外話。因為我看很多同事在創建表的時候,習慣性的對int類型的欄位指定一個長度單位。
例如:int(11)是他們經常使用的方式。
那麼我們在定義表中自增主鍵或某個int類型的欄位的時候,寫成int和int(11)有什麼區別?
int(11)中的11表示int類型所能存儲的最小值的顯示寬度。
注意,這裡是顯示寬度,表示可以顯示多長的int類型的值。我們從上面表中可以知道,int類型的取值範圍為:-2147483648~2147483647,其中的最小值-2147483648
的長度為11,這裡的int(11)表示可以顯示出這個最小值的完整內容,而最大值2147483647的長度為10,因為最小值長度為11數值都可以顯示出來,那麼最大值的長度為10的數值肯定也可以顯示出來。
當我們定義了一個int(11)類型欄位後,如果後面不指定UNSIGNED ZEROFILL關鍵字,這個欄位和int是一樣的。只有指定的UNSIGNED ZEROFILL之後,這個int(11)中的11才起到作用。他起到的作用就是和UNSIGNED ZEROFILL配合使用,將我們插入的數據,在不滿足長度的情況下,在前面補0。
比如我們定義了int(5) UNSIGNED ZEROFILL,那麼當我們插入的數據值1234的時候,它會在1234前面補上0,顯示為01234,僅此而已。
顯示寬度只用於顯示,並不能限制取值範圍和佔用空間。
如:int(3) 它也會佔用 4 個字節(byte)的存儲空間,並且它允許的最大值也不會是999,而是int整型所允許的取值範圍有符號的時候為[-2147483648,2147483647]或者無符號的時候為[0,4294967295]。
那麼int(4)、int(8)、int(11) 分別佔用幾個字節 ?他們也都是佔用4個字節byte,他們的取值範圍也都是[-2147483648,2147483647]有符號或者[0,4294967295]無符號,和int不指定長度一樣。
所以我們使用int類型的變量的時候,直接使用tinyint、smallint、mediumit、int、bigint中的某一種就可以,具體使用哪一種根據自己的業務量來定,而不需要為期指定長度。除非你的業務需求中需要在不足數據位數的時候,在前面補0,但是這個功能需要在定義欄位的時候結合UNSIGNED ZEROFILL關鍵字一起使用才有效果。
表自增主鍵的自增值
如果一張表的自增ID用完之後,我們再次向這個表中插入數據會怎麼樣呢?我們使用tinyint類型的自增主鍵舉例舉例來實驗一下。
創建測試的表mysql>CREATETABLE`t`(->`id`tinyintunsignedNOTNULLAUTO_INCREMENT,->PRIMARYKEY(`id`)->)ENGINE=InnoDBAUTO_INCREMENT=254DEFAULTCHARSET=utf8mb4;Query OK,0rows affected (0.02 sec)
插入測試數據mysql>insertinto t(id)values(null);Query OK,1row affected (0.01 sec)
再次插入數據mysql>insert into t(id)values (null);ERROR 1062(23000): Duplicate entry '255'forkey'PRIMARY'mysql>
查看表中自增值是多少mysql>showcreatetable t\G***************************1.row***************************Table: tCreateTable: CREATETABLE`t`(`id`tinyint(3)unsignedNOTNULLAUTO_INCREMENT,PRIMARYKEY(`id`))ENGINE=InnoDBAUTO_INCREMENT=255DEFAULTCHARSET=utf8mb41rowinset(0.01 sec)
查看表中的數據,結果如下mysql>select*from t;+-----+| id |+---------------+|255|+--------+1rowinset(0.02 sec)
整個過程截圖如下,從中我可以可以看出,當自增主鍵的值,達到最大值之後,我們再次向表中插入數據的時候。自增鍵的自增值將不會再次增加,一直保持最大值不在變化,我們獲取到的自增值也一直是最大值。所以當我們再次插入數據的時候回出現主鍵衝突的錯誤。
如果我們的業務流量比較大,擔心自增值會被用完,我們可以把自增鍵的欄位類型設置為8個byte的bigint,這個類型的值,在理論上是不會用完的,但是與此同時,你要付出的存儲空間也會別int大一倍。這樣就可以避免因為自增主鍵的自增值被用完後,再次插入數據的時候查詢主鍵衝突錯誤信息。
MySQL全局的自增值row_id
我們在創建表的時候,如果不為表指定任何主鍵,那麼MySQL會給這個表創建一個隱藏的自增ID主鍵,並且這個隱藏的自增ID的取值是從一個全局變量dict_sys.row_id中獲取。這個變量是所有沒有主鍵的表共享的。
這個變量佔用6個byte,它的取值範圍是2481,因為這個值對所有沒有主鍵的表共享,如果你的MySQL資料庫中,有很多沒有主鍵的表,並且有很多的數據在這些表中,那麼這個值是有可能達到最大值的。
如果這個全局變量的值達到了最大值,它就會從0開始從新開始計算。這就導致了沒有主鍵的表中的數據可能會被覆蓋的可能性。試想一下,如果一個表沒有主鍵只有一列varchar類型的欄位col_a,我們想裡面插入數據的時候。當插入到最大行的時候,它會從0開始計算,此時我們插入an+1的時候,就會回到第一行a1的這個行上,會把a1這個行的數據內容被覆蓋為an+1,以此類推,a2會被an+2覆蓋掉。
所以建議所有的表都要設置一個主鍵,避免這個隱藏的全局自增值到達最大的2481之後會覆蓋掉之前插入的數據。有了自增主鍵,即便是超過了自增值,在插入數據的時候,會有主鍵衝突的錯誤,這比不通知我們直接把數據給覆蓋掉要好很多。
Xid
在MySQL的innodb數據表進行更新操作的時候,會涉及到redolog的兩階段提交和binlog日誌的配合。以此來達到數據在邏輯上的一致性,從而保證了在MySQL資料庫崩潰異常重啟後,innodb表可以恢復已經正常提交的事務,這也就是我們經常所說的innodb的crash-safe的能力。
Xid是有MySQL的Server層維護的。
Xid是binlog文件中常見的一個ID,因為binlog是server層維護的日誌,所以Xid也是由MySQL的Server層維護的。它在binlog文件中標識一個唯一的事務。
但是在不同的binlog文件中,這個Xid是有可能相同的。因為這個ID是來自於MySQL執行各種SQL語句的時候的查詢編號,MySQL在為所有的SQL語句會分配一個唯一的編號,這個編號來自於全局變量:global_query_id。而global_query_id,它是維護在內存當中。它是佔8個字節的bigint類型,最大值為:2的64次方減1。這就意味著,如果MySQL重啟了,那麼這個變量的值將會丟失,重啟後這個值將會重新從0開始累加。
所以SQL語句的編號將會重新從0開始累加,這個查詢語句的編號會賦值給對應的事務編號,但是binlog文件再MySQL重啟後,會重新使用新的binlog日誌文件。所以在同一個日誌文件中,Xid是不可能相同的。
說Xid在同一個binlog日誌文件中不可能相同的說法也不算太嚴謹,因為如果這個global_query_id達到最大值2的64次方減1之後,從新從0累計也有可能導致同一個binlog文件中的Xid的值重複。但是這個可能性幾乎為0,因為我們的binlog日誌文件在達到一定的大小後也會重新開啟一個新的binlog日誌文件。這個是有參數max_binlog_size控制的。
Innodb的事務ID
InnoDB的事務ID是指:trx_id。
和Xid不同,trx_id是由InnoDB引擎自己維護的。它的最大值為2的48次方減1。如果到達它的最大值之後,會從0開始累加。這個值再MySQL重啟之後不會清零,它做了持久化的操作,所以重啟後的MySQL事務ID是可以累積上一次的值的。
這可能潛在的隱藏一個bug,如果trx_id到達最大之後,重新從0累加,這就導致了事務的id重複了,這樣在MySQL的MVCC多版本數據控制和一致性事務讀取的時候,就可能會發生髒讀。但是可以忽略這個bug,因為這個值已經很大了,不會那麼快就出現這個bug。
trx_id的值來自於innodb內部自己維護的max_trx_id全局變量。每次需要申請新的trx_id的時候,就獲得當前max_trx_id的值,然後再把max_trx_id的值加1為下次準備。注意:只讀事務不會佔用max_trx_id的值。
對於正在執行的事務,可以在information_schema.innodb_trx表中看到對應的事務信息,已經當前事務trx_id的值。
在MySQL的MVCC多版本控制的一致性事務視圖在實現的過程中,就依賴於這個trx_id的值,因為它代表了每一行被修改數據的版本號,在每一行數據被修改後,都會拿當前修改這一行數據的事務的trx_id作為當前數據的版本號。當一個事務讀到一行數據的時候,判斷這個數據是否可見的方法,就是通過事務的一致性視圖與這行數據的 trx_id 做對比。
線程ID
線程ID是指:thread_id,我們平時執行show processlist;命令的時候就可以顯示出這個線程ID。如下所示:
thread_id的取值來自於系統保存的一個全局變量thread_id_counter,每新建一個連接,就將 thread_id_counter 賦值給這個新連接的線程變量。
它的大小是4個字節,最大值為:2的32次方減1,到達最大值之後,他會重新從0累加。但是它也不會重複,因為他們使用了唯一數組的設計理念,如下:
do{new_id= thread_id_counter++;}while(!thread_ids.insert_unique(new_id).second);
總結
表的自增 id 達到上限後,再申請時它的值就不會改變,進而導致繼續插入數據時報主鍵衝突的錯誤。row_id 達到上限後,則會歸 0 再重新遞增,如果出現相同的 row_id,後寫的數據會覆蓋之前的數據。Xid 只需要不在同一個 binlog 文件中出現重複值即可。雖然理論上會出現重複值,但是概率極小,可以忽略不計。InnoDB 的 max_trx_id 遞增值每次 MySQL 重啟都會被保存起來,所以我們文章中提到的髒讀的例子就是一個必現的 bug,好在留給我們的時間還很充裕。thread_id 是我們使用中最常見的,而且也是處理得最好的一個自增 id 邏輯了。關注我,獲取更多分享。