背景
之前面試的時候,有幾家公司都問了UUID,自增ID的優點以及缺點,個人當時只考慮到了索引方面(增刪查改的效率),沒有考慮到分佈式情況下的問題。
如果未來可能對數據進行合併、轉移,自增ID勢必會發生主鍵重複問題。
跨步自增可以解決分佈式問題,但是需要對充分考慮好跨域的步數。
UUID可以解決分佈式問題,但是因爲是varchar類型並且又很長,影響索引重組速度,又會大大影響增刪改效率(隨數據量變大到百萬千萬級時會慢慢接近自增ID)。
雪花算法可以得出一個int類型的ID,此ID可以直接進行排序,可以解決分佈式問題,但是生成時需要注意機器ID等參數,因爲生成的int值很大,在小項目運用時也會影響主鍵索引重組。
總之,根據當前數據量大小,再預測未來的數據量,然後選擇最合適的主鍵類型,像人員信息表這類數據,做大之後未來就極有可能會涉及分佈式和合表問題。
另外,如果說有一種情況,某表內的數據一開始在某個系統裏以自增爲主鍵的時候,插入時順便也生成一個UUID存放到一個臨時字段裏,數據轉移時將此表內的數據和結構先克隆爲另外一個新表,然後把新表的自增ID刪除,將UUID那個字段設置成主鍵後再合併到新系統的表中,這樣是不是即不影響之前的系統效率,也防止了分佈式主鍵衝突的問題?
測試部分:
1、準備表以及數據
UC_USER,自增ID爲主鍵,表結構類似如下:
CREATE TABLE `UC_USER` (
`ID` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`USER_NAME` varchar(100) DEFAULT NULL COMMENT '用戶名',
`USER_PWD` varchar(200) DEFAULT NULL COMMENT '密碼',
`BIRTHDAY` datetime DEFAULT NULL COMMENT '生日',
`NAME` varchar(200) DEFAULT NULL COMMENT '姓名',
`USER_ICON` varchar(500) DEFAULT NULL COMMENT '頭像圖片',
`SEX` char(1) DEFAULT NULL COMMENT '性別, 1:男,2:女,3:保密',
`NICKNAME` varchar(200) DEFAULT NULL COMMENT '暱稱',
`STAT` varchar(10) DEFAULT NULL COMMENT '用戶狀態,01:正常,02:凍結',
`USER_MALL` bigint(20) DEFAULT NULL COMMENT '當前所屬MALL',
`LAST_LOGIN_DATE` datetime DEFAULT NULL COMMENT '最後登錄時間',
`LAST_LOGIN_IP` varchar(100) DEFAULT NULL COMMENT '最後登錄IP',
`SRC_OPEN_USER_ID` bigint(20) DEFAULT NULL COMMENT '來源的聯合登錄',
`EMAIL` varchar(200) DEFAULT NULL COMMENT '郵箱',
`MOBILE` varchar(50) DEFAULT NULL COMMENT '手機',
`IS_DEL` char(1) DEFAULT '0' COMMENT '是否刪除',
`IS_EMAIL_CONFIRMED` char(1) DEFAULT '0' COMMENT '是否綁定郵箱',
`IS_PHONE_CONFIRMED` char(1) DEFAULT '0' COMMENT '是否綁定手機',
`CREATER` bigint(20) DEFAULT NULL COMMENT '創建人',
`CREATE_DATE` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '註冊時間',
`UPDATE_DATE` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '修改日期',
`PWD_INTENSITY` char(1) DEFAULT NULL COMMENT '密碼強度',
`MOBILE_TGC` char(64) DEFAULT NULL COMMENT '手機登錄標識',
`MAC` char(64) DEFAULT NULL COMMENT 'mac地址',
`SOURCE` char(1) DEFAULT '0' COMMENT '1:WEB,2:IOS,3:ANDROID,4:WIFI,5:管理系統, 0:未知',
`ACTIVATE` char(1) DEFAULT '1' COMMENT '激活,1:激活,0:未激活',
`ACTIVATE_TYPE` char(1) DEFAULT '0' COMMENT '激活類型,0:自動,1:手動',
PRIMARY KEY (`ID`),
UNIQUE KEY `USER_NAME` (`USER_NAME`),
KEY `MOBILE` (`MOBILE`),
KEY `IDX_MOBILE_TGC` (`MOBILE_TGC`,`ID`),
KEY `IDX_EMAIL` (`EMAIL`,`ID`),
KEY `IDX_CREATE_DATE` (`CREATE_DATE`,`ID`),
KEY `IDX_UPDATE_DATE` (`UPDATE_DATE`)
) ENGINE=InnoDB AUTO_INCREMENT=7122681 DEFAULT CHARSET=utf8 COMMENT='用戶表'
UC_USER_PK_VARCHAR表,字符串ID爲主鍵,採用uuid
CREATE TABLE `UC_USER_PK_VARCHAR_1` (
`ID` varchar(36) CHARACTER SET utf8mb4 NOT NULL DEFAULT '0' COMMENT '主鍵',
`USER_NAME` varchar(100) DEFAULT NULL COMMENT '用戶名',
`USER_PWD` varchar(200) DEFAULT NULL COMMENT '密碼',
`BIRTHDAY` datetime DEFAULT NULL COMMENT '生日',
`NAME` varchar(200) DEFAULT NULL COMMENT '姓名',
`USER_ICON` varchar(500) DEFAULT NULL COMMENT '頭像圖片',
`SEX` char(1) DEFAULT NULL COMMENT '性別, 1:男,2:女,3:保密',
`NICKNAME` varchar(200) DEFAULT NULL COMMENT '暱稱',
`STAT` varchar(10) DEFAULT NULL COMMENT '用戶狀態,01:正常,02:凍結',
`USER_MALL` bigint(20) DEFAULT NULL COMMENT '當前所屬MALL',
`LAST_LOGIN_DATE` datetime DEFAULT NULL COMMENT '最後登錄時間',
`LAST_LOGIN_IP` varchar(100) DEFAULT NULL COMMENT '最後登錄IP',
`SRC_OPEN_USER_ID` bigint(20) DEFAULT NULL COMMENT '來源的聯合登錄',
`EMAIL` varchar(200) DEFAULT NULL COMMENT '郵箱',
`MOBILE` varchar(50) DEFAULT NULL COMMENT '手機',
`IS_DEL` char(1) DEFAULT '0' COMMENT '是否刪除',
`IS_EMAIL_CONFIRMED` char(1) DEFAULT '0' COMMENT '是否綁定郵箱',
`IS_PHONE_CONFIRMED` char(1) DEFAULT '0' COMMENT '是否綁定手機',
`CREATER` bigint(20) DEFAULT NULL COMMENT '創建人',
`CREATE_DATE` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '註冊時間',
`UPDATE_DATE` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '修改日期',
`PWD_INTENSITY` char(1) DEFAULT NULL COMMENT '密碼強度',
`MOBILE_TGC` char(64) DEFAULT NULL COMMENT '手機登錄標識',
`MAC` char(64) DEFAULT NULL COMMENT 'mac地址',
`SOURCE` char(1) DEFAULT '0' COMMENT '1:WEB,2:IOS,3:ANDROID,4:WIFI,5:管理系統, 0:未知',
`ACTIVATE` char(1) DEFAULT '1' COMMENT '激活,1:激活,0:未激活',
`ACTIVATE_TYPE` char(1) DEFAULT '0' COMMENT '激活類型,0:自動,1:手動',
PRIMARY KEY (`ID`),
UNIQUE KEY `USER_NAME` (`USER_NAME`),
KEY `MOBILE` (`MOBILE`),
KEY `IDX_MOBILE_TGC` (`MOBILE_TGC`,`ID`),
KEY `IDX_EMAIL` (`EMAIL`,`ID`),
KEY `IDX_CREATE_DATE` (`CREATE_DATE`,`ID`),
KEY `IDX_UPDATE_DATE` (`UPDATE_DATE`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用戶表';
2、500W數據測試
2.1 錄入500W數據,自增ID節省一半磁盤空間
確定兩個表數據量
# 自增id爲主鍵的表
mysql> select count(1) from UC_USER;
+----------+
| count(1) |
+----------+
| 5720112 |
+----------+
1 row in set (0.00 sec)
mysql>
# uuid爲主鍵的表
mysql> select count(1) from UC_USER_PK_VARCHAR_1;
+----------+
| count(1) |
+----------+
| 5720112 |
+----------+
1 row in set (1.91 sec)
佔據的空間容量來看,自增ID比UUID小一半左右。
主鍵類型 |
數據文件大小 |
佔據容量 |
自增ID |
-rw-rw---- 1 mysql mysql 4.2G Aug 20 23:08 UC_USER_1.ibd |
4.2 G |
UUID |
-rw-rw---- 1 mysql mysql 8.8G Aug 20 18:20 UC_USER_PK_VARCHAR_1.ibd |
8.8 G |
4.2 單個數據走索引查詢,自增id和 uuid效率比是:(2~3):1
主鍵類型 |
SQL語句 |
執行時間 (秒) |
單條記錄查詢 |
||
自增ID |
SELECT SQL_NO_CACHE t.* FROM test.`UC_USER_1` t WHERE t.`MOBILE` ='14782121512'; |
0.069 |
UUID |
SELECT SQL_NO_CACHE t.* FROM test.`UC_USER_PK_VARCHAR_1` t WHERE t.`MOBILE` ='14782121512'; |
0.274 |
小範圍查詢 |
||
自增ID |
SELECT SQL_NO_CACHE t.* FROM test.`UC_USER_1` t WHERE t.`MOBILE` IN( '14782121512','13761460105'); |
0.050 |
UUID |
SELECT SQL_NO_CACHE t.* FROM test.`UC_USER_PK_VARCHAR_1` t WHERE t.`MOBILE` IN('14782121512','13761460105'); |
0.151 |
根據日期查詢 |
||
自增ID |
SELECT SQL_NO_CACHE t.* FROM test.`UC_USER_1` t WHERE t.`CREATE_DATE`='2013-11-24 10:26:36' ; |
0.269 |
UUID |
SELECT SQL_NO_CACHE t.* FROM test.`UC_USER_PK_VARCHAR_1` t WHERE t.`CREATE_DATE`='2013-11-24 10:26:43' ; |
0.810 |
4.3 範圍like查詢,自增ID性能優於UUID,比值(1.5~2):1
主鍵類型 |
SQL語句 |
執行時間 (秒) |
(1)模糊範圍查詢1000條數據,自增ID性能要好於UUID |
||
自增ID |
SELECT SQL_NO_CACHE t.* FROM test.`UC_USER` t WHERE t.`MOBILE` LIKE '147%' LIMIT 1000; |
2.398 |
UUID |
SELECT SQL_NO_CACHE t.* FROM test.`UC_USER_PK_VARCHAR_1` t WHERE t.`MOBILE` LIKE '147%' LIMIT 1000; |
5.872 |
(2)日期範圍查詢20條數據,自增ID稍微弱於UUID |
||
自增ID |
SELECT SQL_NO_CACHE t.* FROM test.`UC_USER_1` t WHERE t.`CREATE_DATE` > '2016-08-01 10:26:36' ORDER BY t.`UPDATE_DATE` DESC LIMIT 20; |
0.765 |
UUID |
SELECT SQL_NO_CACHE t.* FROM test.`UC_USER_PK_VARCHAR_1` t WHERE t.`CREATE_DATE` > '2016-08-01 10:26:36' ORDER BY t.`UPDATE_DATE` DESC LIMIT 20; |
1.090 |
(3)範圍查詢200條數據,自增ID性能要好於UUID |
||
自增ID |
SELECT SQL_NO_CACHE t.* FROM test.`UC_USER_1` t WHERE t.`CREATE_DATE` > '2016-07-01 10:26:36' ORDER BY t.`UPDATE_DATE` DESC LIMIT 200; |
1.569 |
UUID |
SELECT SQL_NO_CACHE t.* FROM test.`UC_USER_PK_VARCHAR_1` t WHERE t.`CREATE_DATE` > '2016-07-01 10:26:36' ORDER BY t.`UPDATE_DATE` DESC LIMIT 200; |
2.597 |
範圍查詢總數量,自增ID要好於UUID |
||
自增ID |
SELECT SQL_NO_CACHE COUNT(1) FROM test.`UC_USER_1` t WHERE t.`CREATE_DATE` > '2016-07-01 10:26:36' ; |
1.129 |
UUID |
SELECT SQL_NO_CACHE COUNT(1) FROM test.`UC_USER_PK_VARCHAR_1` t WHERE t.`CREATE_DATE` > '2016-07-01 10:26:36' ; |
2.302 |
4.4 寫入測試,自增ID比UUID效率高,比值(3~10):1
主鍵類型 |
SQL語句 |
執行時間 (秒) |
修改一天的記錄 |
||
自增ID |
UPDATE test.`UC_USER_1` t SET t.`MOBILE_TGC`='T2' WHERE t.`CREATE_DATE` > '2016-05-03 10:26:36' AND t.`CREATE_DATE` <'2016-05-04 00:00:00' ; |
2.685 |
UUID |
UPDATE test.`UC_USER_PK_VARCHAR_1` t SET t.`MOBILE_TGC`='T2' WHERE t.`CREATE_DATE` > '2016-05-03 10:26:36' AND t.`CREATE_DATE` <'2016-05-04 00:00:00' ; |
26.521 |
錄入數據 |
||
自增ID |
INSERT INTO test.`UC_USER_1`( ID, `USER_NAME`, `USER_PWD`, `BIRTHDAY`, `NAME`, `USER_ICON`, `SEX`, `NICKNAME`, `STAT`, `USER_MALL`, `LAST_LOGIN_DATE`, `LAST_LOGIN_IP`, `SRC_OPEN_USER_ID`, `EMAIL`, `MOBILE`, `IS_DEL`, `IS_EMAIL_CONFIRMED`, `IS_PHONE_CONFIRMED`, `CREATER`, `CREATE_DATE`, `UPDATE_DATE`, `PWD_INTENSITY`, `MOBILE_TGC`, `MAC`, `SOURCE`, `ACTIVATE`, `ACTIVATE_TYPE` ) SELECT NULL, CONCAT('110',`USER_NAME`,8), `USER_PWD`, `BIRTHDAY`, `NAME`, `USER_ICON`, `SEX`, `NICKNAME`, `STAT`, `USER_MALL`, `LAST_LOGIN_DATE`, `LAST_LOGIN_IP`, `SRC_OPEN_USER_ID`, `EMAIL`, CONCAT('110',TRIM(`MOBILE`)), `IS_DEL`, `IS_EMAIL_CONFIRMED`, `IS_PHONE_CONFIRMED`, `CREATER`, `CREATE_DATE`, `UPDATE_DATE`, `PWD_INTENSITY`, `MOBILE_TGC`, `MAC`, `SOURCE`, `ACTIVATE`, `ACTIVATE_TYPE` FROM `test`.`UC_USER_1` LIMIT 100; |
0.534 |
UUID |
INSERT INTO test.`UC_USER_PK_VARCHAR_1`( ID, `USER_NAME`, `USER_PWD`, `BIRTHDAY`, `NAME`, `USER_ICON`, `SEX`, `NICKNAME`, `STAT`, `USER_MALL`, `LAST_LOGIN_DATE`, `LAST_LOGIN_IP`, `SRC_OPEN_USER_ID`, `EMAIL`, `MOBILE`, `IS_DEL`, `IS_EMAIL_CONFIRMED`, `IS_PHONE_CONFIRMED`, `CREATER`, `CREATE_DATE`, `UPDATE_DATE`, `PWD_INTENSITY`, `MOBILE_TGC`, `MAC`, `SOURCE`, `ACTIVATE`, `ACTIVATE_TYPE` ) SELECT UUID(), CONCAT('110',`USER_NAME`,8), `USER_PWD`, `BIRTHDAY`, `NAME`, `USER_ICON`, `SEX`, `NICKNAME`, `STAT`, `USER_MALL`, `LAST_LOGIN_DATE`, `LAST_LOGIN_IP`, `SRC_OPEN_USER_ID`, `EMAIL`, CONCAT('110',TRIM(`MOBILE`)), `IS_DEL`, `IS_EMAIL_CONFIRMED`, `IS_PHONE_CONFIRMED`, `CREATER`, `CREATE_DATE`, `UPDATE_DATE`, `PWD_INTENSITY`, `MOBILE_TGC`, `MAC`, `SOURCE`, `ACTIVATE`, `ACTIVATE_TYPE` FROM `test`.`UC_USER_1` LIMIT 100; |
1.716 |
4.5、備份和恢復,自增ID性能優於UUID
主鍵類型 |
SQL語句 |
執行時間 (秒) |
Mysqldump備份 |
||
自增ID |
time mysqldump -utim -ptimgood -h192.168.121.63 test UC_USER_1> UC_USER_1.sql |
0m50.548s |
UUID |
time mysqldump -utim -ptimgood -h192.168.121.63 test UC_USER_PK_VARCHAR_1> UC_USER_PK_VARCHAR_1.sql |
0m58.590s |
MySQL恢復 |
||
自增ID |
time mysql -utim -ptimgood -h192.168.121.63 test < UC_USER_1.sql |
17m30.822s |
UUID |
time mysql -utim -ptimgood -h192.168.121.63 test < UC_USER_PK_VARCHAR_1.sql |
23m6.360s |
|
|
|
5、1000W總結
在1000W記錄表的測試下:
(1)普通單條或者20條左右的記錄檢索,自增主鍵效率是uuid主鍵的2到3倍;
(2)但是範圍查詢特別是上百成千條的記錄查詢,自增id的效率要大於uuid;
(3)在範圍查詢做統計彙總的時候,自增id主鍵的效率是uuid主鍵1.5到2倍;
(4)在存儲上面,自增id所佔的存儲空間是uuid的1/2;
(5)在寫入上面,自增ID主鍵的效率是UUID主鍵的3到10倍,相差比較明顯,特別是update小範圍之內的數據上面。
(6)在備份恢復上,自增ID主鍵稍微優於UUID。
6、MySQL分佈式架構的取捨
分佈式架構,意味着需要多個實例中保持一個表的主鍵的唯一性。這個時候普通的單表自增ID主鍵就不太合適,因爲多個mysql實例上會遇到主鍵全局唯一性問題。
6.1、自增ID主鍵+步長,適合中等規模的分佈式場景
在每個集羣節點組的master上面,設置(auto_increment_increment),讓目前每個集羣的起始點錯開 1,步長選擇大於將來基本不可能達到的切分集羣數,達到將 ID 相對分段的效果來滿足全局唯一的效果。
優點是:實現簡單,後期維護簡單,對應用透明。
缺點是:第一次設置相對較爲複雜,因爲要針對未來業務的發展而計算好足夠的步長;
規劃:
比如計劃總共N個節點組,那麼第i個節點組的my.cnf的配置爲:
auto_increment_offset i
auto_increment_increment N
假如規劃48個節點組,N爲48,現在配置第8個節點組,這個i爲8,第8個節點組的my.cnf裏面的配置爲:
auto_increment_offset 8
auto_increment_increment 48
6.2、UUID,適合小規模的分佈式環境
對於InnoDB這種聚集主鍵類型的引擎來說,數據會按照主鍵進行排序,由於UUID的無序性,InnoDB會產生巨大的IO壓力,而且由於索引和數據存儲在一起,字符串做主鍵會造成存儲空間增大一倍。
在存儲和檢索的時候,innodb會對主鍵進行物理排序,這對auto_increment_int是個好消息,因爲後一次插入的主鍵位置總是在最後。但是對uuid來說,這卻是個壞消息,因爲uuid是雜亂無章的,每次插入的主鍵位置是不確定的,可能在開頭,也可能在中間,在進行主鍵物理排序的時候,勢必會造成大量的 IO操作影響效率,在數據量不停增長的時候,特別是數據量上了千萬記錄的時候,讀寫性能下降的非常厲害。
優點:搭建比較簡單,不需要爲主鍵唯一性的處理。
缺點:佔用兩倍的存儲空間(在雲上光存儲一塊就要多花2倍的錢),後期讀寫性能下降厲害。
6.3、雪花算法自造全局自增ID,適合大數據環境的分佈式場景
由twitter公佈的開源的分佈式id算法snowflake(Java版本)
IdWorker.java:
package com.demo.elk;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class IdWorker {
protected static final Logger LOG = LoggerFactory.getLogger(IdWorker.class);
private long workerId;
private long datacenterId;
private long sequence = 0L;
private long twepoch = 1288834974657L;
private long workerIdBits = 5L;
private long datacenterIdBits = 5L;
private long maxWorkerId = -1L ^ (-1L << workerIdBits);
private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
private long sequenceBits = 12L;
private long workerIdShift = sequenceBits;
private long datacenterIdShift = sequenceBits + workerIdBits;
private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
private long sequenceMask = -1L ^ (-1L << sequenceBits);
private long lastTimestamp = -1L;
public IdWorker(long workerId, long datacenterId) {
// sanity check for workerId
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
LOG.info(String.format("worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d", timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId));
}
public synchronized long nextId() {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
LOG.error(String.format("clock is moving backwards. Rejecting requests until %d.", lastTimestamp));
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift) | (workerId << workerIdShift) | sequence;
}
protected long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
protected long timeGen() {
return System.currentTimeMillis();
}
}
測試生成ID的測試類,IdWorkerTest.java:
package com.demo.elk;
import java.util.HashSet; import java.util.Set;
public class IdWorkerTest {
static class IdWorkThread implements Runnable { private Set<Long> set; private IdWorker idWorker;
public IdWorkThread(Set<Long> set, IdWorker idWorker) { this.set = set; this.idWorker = idWorker; }
public void run() { while (true) { long id = idWorker.nextId(); System.out.println(" real id:" + id); if (!set.add(id)) { System.out.println("duplicate:" + id); } } } }
public static void main(String[] args) { Set<Long> set = new HashSet<Long>(); final IdWorker idWorker1 = new IdWorker(0, 0); final IdWorker idWorker2 = new IdWorker(1, 0); Thread t1 = new Thread(new IdWorkThread(set, idWorker1)); Thread t2 = new Thread(new IdWorkThread(set, idWorker2)); t1.setDaemon(true); t2.setDaemon(true); t1.start(); t2.start(); try { Thread.sleep(30000); } catch (InterruptedException e) { e.printStackTrace(); } } } |
7,總結
(1)單實例或者單節點組:
經過500W、1000W的單機表測試,自增ID相對UUID來說,自增ID主鍵性能高於UUID,磁盤存儲費用比UUID節省一半的錢。所以在單實例上或者單節點組上,使用自增ID作爲首選主鍵。
(2)分佈式架構場景:
20個節點組下的小型規模的分佈式場景,爲了快速實現部署,可以採用多花存儲費用、犧牲部分性能而使用UUID主鍵快速部署;
20到200個節點組的中等規模的分佈式場景,可以採用自增ID+步長的較快速方案。
200以上節點組的大數據下的分佈式場景,可以借鑑類似twitter雪花算法構造的全局自增ID作爲主鍵。