垂直分表與水平分庫(kù)
垂直拆分的意思簸淀,就是把一個(gè)有很多字段的表給拆分成多個(gè)表,或者是多個(gè)庫(kù)上去毒返。每個(gè)庫(kù)表的結(jié)構(gòu)都不一樣租幕,每個(gè)庫(kù)表都包含部分字段。一般來(lái)說(shuō)拧簸,會(huì)將較少的訪問(wèn)頻率很高的字段放到一個(gè)表里去劲绪,然后將較多的訪問(wèn)頻率很低的字段放到另外一個(gè)表里去。因?yàn)閿?shù)據(jù)庫(kù)是有緩存的,你訪問(wèn)頻率高的行字段越少贾富,就可以在緩存里緩存更多的行歉眷,性能就越好。這個(gè)一般在表結(jié)構(gòu)設(shè)計(jì)的時(shí)候做的較多一些颤枪。
水平拆分的意思汗捡,就是把一個(gè)表的數(shù)據(jù)給弄到多個(gè)庫(kù)的多個(gè)表里去,但是每個(gè)庫(kù)的表結(jié)構(gòu)都一樣畏纲,只不過(guò)每個(gè)庫(kù)表放的數(shù)據(jù)是不同的扇住,所有庫(kù)表的數(shù)據(jù)加起來(lái)就是全部數(shù)據(jù)。水平拆分的意義霍骄,就是將數(shù)據(jù)均勻放更多的庫(kù)里台囱,然后用多個(gè)庫(kù)來(lái)扛更高的并發(fā),還有就是用多個(gè)庫(kù)的存儲(chǔ)容量來(lái)進(jìn)行擴(kuò)容读整。一般講的分庫(kù)分表就是指水平拆分的意思簿训。
兩種分庫(kù)分表方式
- 1 按照 range 來(lái)分。
就是每個(gè)庫(kù)一段連續(xù)的數(shù)據(jù)米间,這個(gè)一般是按比如時(shí)間范圍來(lái)的强品,但是這種一般較少用,因?yàn)楹苋菀桩a(chǎn)生熱點(diǎn)問(wèn)題屈糊,大量的流量都打在最新的數(shù)據(jù)上了的榛。range 來(lái)分,好處在于說(shuō)逻锐,擴(kuò)容的時(shí)候很簡(jiǎn)單夫晌,因?yàn)槟阒灰A(yù)備好,給每個(gè)月都準(zhǔn)備一個(gè)庫(kù)就可以了昧诱,到了一個(gè)新的月份的時(shí)候晓淀,自然而然,就會(huì)寫(xiě)新的庫(kù)了盏档;缺點(diǎn)凶掰,但是大部分的請(qǐng)求,都是訪問(wèn)最新的數(shù)據(jù)蜈亩。實(shí)際生產(chǎn)用 range懦窘,要看場(chǎng)景。 - 2 按照某個(gè)字段 hash 一下均勻分散稚配,這個(gè)較為常用畅涂。
hash 分發(fā),好處在于說(shuō)道川,可以平均分配每個(gè)庫(kù)的數(shù)據(jù)量和請(qǐng)求壓力午衰;壞處在于說(shuō)擴(kuò)容起來(lái)比較麻煩苹丸,會(huì)有一個(gè)數(shù)據(jù)遷移的過(guò)程,之前的數(shù)據(jù)需要重新計(jì)算 hash 值重新分配到不同的庫(kù)或表苇经。
數(shù)據(jù)遷移(單庫(kù)環(huán)境數(shù)據(jù)到分科分表環(huán)境數(shù)據(jù))
1 最土的方法就是直接晚上網(wǎng)站停機(jī),然后程序員通宵把數(shù)據(jù)遷移到新的庫(kù)里面去宦言,這也是最土的方法扇单,假設(shè)當(dāng)晚沒(méi)有遷移成功,那就得代碼回滾奠旺,第二天接著停機(jī) 再遷移數(shù)據(jù)
2 雙寫(xiě)方案
簡(jiǎn)單來(lái)說(shuō)蜘澜,就是在線上系統(tǒng)里面,之前所有寫(xiě)庫(kù)的地方响疚,增刪改操作鄙信,除了對(duì)老庫(kù)增刪改,都加上對(duì)新庫(kù)的增刪改忿晕,這就是所謂的雙寫(xiě)装诡,同時(shí)寫(xiě)倆庫(kù),老庫(kù)和新庫(kù)践盼。
然后系統(tǒng)部署之后鸦采,新庫(kù)數(shù)據(jù)差太遠(yuǎn),用之前說(shuō)的導(dǎo)數(shù)工具咕幻,跑起來(lái)讀老庫(kù)數(shù)據(jù)寫(xiě)新庫(kù)渔伯,寫(xiě)的時(shí)候要根據(jù)數(shù)據(jù)的最后修改的時(shí)間,將剩余的老數(shù)據(jù)遷入到新庫(kù)中(新庫(kù)中有數(shù)據(jù)就更新肄程,沒(méi)有就插入) 锣吼,這樣就不用通宵遷移數(shù)據(jù)了。
生成全局唯一id
- 1 數(shù)據(jù)庫(kù)自增 id
這個(gè)就是說(shuō)你的系統(tǒng)里每次得到一個(gè) id蓝厌,都是往一個(gè)庫(kù)的一個(gè)表里插入一條沒(méi)什么業(yè)務(wù)含義的數(shù)據(jù)玄叠,然后獲取一個(gè)數(shù)據(jù)庫(kù)自增的一個(gè) id。拿到這個(gè) id 之后再往對(duì)應(yīng)的分庫(kù)分表里去寫(xiě)入褂始。
這個(gè)方案的好處就是方便簡(jiǎn)單诸典,誰(shuí)都會(huì)用;缺點(diǎn)就是單庫(kù)生成自增 id崎苗,要是高并發(fā)的話(huà)狐粱,就會(huì)有瓶頸的;如果你硬是要改進(jìn)一下胆数,那么就專(zhuān)門(mén)開(kāi)一個(gè)服務(wù)出來(lái)肌蜻,這個(gè)服務(wù)每次就拿到當(dāng)前 id 最大值,然后自己遞增幾個(gè) id必尼,一次性返回一批 id蒋搜,然后再把當(dāng)前最大 id 值修改成遞增幾個(gè) id 之后的一個(gè)值篡撵;但是無(wú)論如何都是基于單個(gè)數(shù)據(jù)庫(kù)。(都到了分庫(kù)分表層面了豆挽,數(shù)據(jù)庫(kù)讀寫(xiě)壓力應(yīng)該蠻大的育谬,所以這個(gè)方案應(yīng)該不會(huì)使用)
-
2 設(shè)置數(shù)據(jù)庫(kù) sequence 或者表自增字段步長(zhǎng)
image.png
適合的場(chǎng)景:在用戶(hù)防止產(chǎn)生的 ID 重復(fù)時(shí),這種方案實(shí)現(xiàn)起來(lái)比較簡(jiǎn)單帮哈,也能達(dá)到性能目標(biāo)膛檀。但是服務(wù)節(jié)點(diǎn)固定,步長(zhǎng)也固定娘侍,將來(lái)如果還要增加服務(wù)節(jié)點(diǎn)咖刃,就不好搞了。
3 UUID
好處就是本地生成憾筏,不要基于數(shù)據(jù)庫(kù)來(lái)了嚎杨;不好之處就是,UUID 太長(zhǎng)了氧腰、占用空間大枫浙,作為主鍵性能太差了;更重要的是古拴,UUID 不具有有序性自脯,會(huì)導(dǎo)致 B+ 樹(shù)索引在寫(xiě)的時(shí)候有過(guò)多的隨機(jī)寫(xiě)操作(連續(xù)的 ID 可以產(chǎn)生部分順序?qū)懀€有斤富,由于在寫(xiě)的時(shí)候不能產(chǎn)生有順序的 append 操作膏潮,而需要進(jìn)行 insert 操作,將會(huì)讀取整個(gè) B+ 樹(shù)節(jié)點(diǎn)到內(nèi)存满力,在插入這條記錄后會(huì)將整個(gè)節(jié)點(diǎn)寫(xiě)回磁盤(pán)焕参,這種操作在記錄占用空間比較大的情況下,性能下降明顯油额。4 系統(tǒng)當(dāng)前時(shí)間
這個(gè)就是獲取當(dāng)前時(shí)間即可叠纷,但是問(wèn)題是,并發(fā)很高的時(shí)候潦嘶,比如一秒并發(fā)幾千涩嚣,會(huì)有重復(fù)的情況,這個(gè)是肯定不合適的掂僵『胶瘢基本就不用考慮了。
適合的場(chǎng)景:一般如果用這個(gè)方案锰蓬,是將當(dāng)前時(shí)間跟很多其他的業(yè)務(wù)字段拼接起來(lái)幔睬,作為一個(gè) id,如果業(yè)務(wù)上你覺(jué)得可以接受芹扭,那么也是可以的麻顶。你可以將別的業(yè)務(wù)字段值跟當(dāng)前時(shí)間拼接起來(lái)赦抖,組成一個(gè)全局唯一的編號(hào)。
5 snowflake 算法
1 bit:第一個(gè)數(shù)字表示正負(fù)位辅肾,0正1負(fù)队萤,因?yàn)樯傻挠肋h(yuǎn)是一個(gè)正常,這里這里永遠(yuǎn)是0矫钓。
41 bit:這個(gè)41位數(shù)字是當(dāng)前時(shí)間戳做一些計(jì)算浮禾,然后以41位的二進(jìn)制來(lái)表示。
5 bit:這5位表示機(jī)房id份汗,總共可以有32位機(jī)房。
5 bit:這5位表示機(jī)機(jī)器id蝴簇,每個(gè)機(jī)房可以擁有最多32個(gè)機(jī)器杯活。
12 bit:這個(gè)是用來(lái)記錄同一個(gè)毫秒內(nèi)產(chǎn)生的不同 id,12 bit 可以代表的最大正整數(shù)是 2^12 - 1 = 4096熬词,也就是說(shuō)可以用這個(gè) 12 bit 代表的數(shù)字來(lái)區(qū)分同一個(gè)毫秒內(nèi)的 4096 個(gè)不同的 id旁钧。
0 | 0001100 10100010 10111110 10001001 01011100 00 | 10001 | 1 1001 | 0000 00000000
public class IdWorker {
private long workerId;
private long datacenterId;
private long sequence;
public IdWorker(long workerId, long datacenterId, long sequence) {
// sanity check for workerId
// 這兒不就檢查了一下,要求就是你傳遞進(jìn)來(lái)的機(jī)房id和機(jī)器id不能超過(guò)32互拾,不能小于0
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));
}
System.out.printf(
"worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d",
timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId);
this.workerId = workerId;
this.datacenterId = datacenterId;
this.sequence = sequence;
}
private long twepoch = 1288834974657L;
private long workerIdBits = 5L;
private long datacenterIdBits = 5L;
// 這個(gè)是二進(jìn)制運(yùn)算歪今,就是 5 bit最多只能有31個(gè)數(shù)字,也就是說(shuō)機(jī)器id最多只能是32以?xún)?nèi)
private long maxWorkerId = -1L ^ (-1L << workerIdBits);
// 這個(gè)是一個(gè)意思颜矿,就是 5 bit最多只能有31個(gè)數(shù)字寄猩,機(jī)房id最多只能是32以?xún)?nèi)
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 long getWorkerId() {
return workerId;
}
public long getDatacenterId() {
return datacenterId;
}
public long getTimestamp() {
return System.currentTimeMillis();
}
public synchronized long nextId() {
// 這兒就是獲取當(dāng)前時(shí)間戳,單位是毫秒
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
System.err.printf("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) {
// 這個(gè)意思是說(shuō)一個(gè)毫秒內(nèi)最多只能有4096個(gè)數(shù)字
// 無(wú)論你傳遞多少進(jìn)來(lái)骑疆,這個(gè)位運(yùn)算保證始終就是在4096這個(gè)范圍內(nèi)田篇,避免你自己傳遞個(gè)sequence超過(guò)了4096這個(gè)范圍
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0;
}
// 這兒記錄一下最近一次生成id的時(shí)間戳,單位是毫秒
lastTimestamp = timestamp;
// 這兒就是將時(shí)間戳左移箍铭,放到 41 bit那兒泊柬;
// 將機(jī)房 id左移放到 5 bit那兒;
// 將機(jī)器id左移放到5 bit那兒诈火;將序號(hào)放最后12 bit兽赁;
// 最后拼接起來(lái)成一個(gè) 64 bit的二進(jìn)制數(shù)字,轉(zhuǎn)換成 10 進(jìn)制就是個(gè) long 型
return ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift)
| (workerId << workerIdShift) | sequence;
}
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
private long timeGen() {
return System.currentTimeMillis();
}
// ---------------測(cè)試---------------
public static void main(String[] args) {
// 最多32個(gè)機(jī)房冷守,每個(gè)機(jī)房最多32個(gè)機(jī)器
IdWorker worker = new IdWorker(1, 1, 1);
long s = System.currentTimeMillis();
//100萬(wàn)個(gè)id只需要3秒生成
for (int i = 0; i < 1000000 ; i++) {
System.out.println(worker.nextId());
}
long e = System.currentTimeMillis();
System.out.println(e-s);
}
}
怎么說(shuō)呢刀崖,大概這個(gè)意思吧,就是說(shuō) 41 bit 是當(dāng)前毫秒單位的一個(gè)時(shí)間戳拍摇,就這意思蒲跨;然后 5 bit 是你傳遞進(jìn)來(lái)的一個(gè)機(jī)房 id(但是最大只能是 32 以?xún)?nèi)),另外 5 bit 是你傳遞進(jìn)來(lái)的機(jī)器 id(但是最大只能是 32 以?xún)?nèi))授翻,剩下的那個(gè) 12 bit序列號(hào)或悲,就是如果跟你上次生成 id 的時(shí)間還在一個(gè)毫秒內(nèi)孙咪,那么會(huì)把順序給你累加,最多在 4096 個(gè)序號(hào)以?xún)?nèi)巡语。
所以你自己利用這個(gè)工具類(lèi)翎蹈,自己搞一個(gè)服務(wù),然后對(duì)每個(gè)機(jī)房的每個(gè)機(jī)器都初始化這么一個(gè)東西男公,剛開(kāi)始這個(gè)機(jī)房的這個(gè)機(jī)器的序號(hào)就是 0荤堪。然后每次接收到一個(gè)請(qǐng)求,說(shuō)這個(gè)機(jī)房的這個(gè)機(jī)器要生成一個(gè) id枢赔,你就找到對(duì)應(yīng)的 Worker 生成澄阳。
利用這個(gè) snowflake 算法,你可以開(kāi)發(fā)自己公司的服務(wù)踏拜,甚至對(duì)于機(jī)房 id 和機(jī)器 id碎赢,反正給你預(yù)留了 5 bit + 5 bit,你換成別的有業(yè)務(wù)含義的東西也可以的速梗。
這個(gè) snowflake 算法相對(duì)來(lái)說(shuō)還是比較靠譜的肮塞,所以你要真是搞分布式 id 生成,如果是高并發(fā)啥的姻锁,那么用這個(gè)應(yīng)該性能比較好枕赵,一般每秒幾萬(wàn)并發(fā)的場(chǎng)景,也足夠你用了位隶。
如何實(shí)現(xiàn) MySQL 的讀寫(xiě)分離拷窜?主從復(fù)制原理?解決 MySQL 主從同步的延時(shí)問(wèn)題涧黄?
Mysql讀寫(xiě)分離是基于主從復(fù)制來(lái)做的装黑,主庫(kù)將變更寫(xiě)入 binlog 日志,然后從庫(kù)連接到主庫(kù)之后弓熏,從庫(kù)有一個(gè) IO 線程恋谭,將主庫(kù)的 binlog 日志拷貝到自己本地,寫(xiě)入一個(gè) relay 中繼日志中挽鞠。接著從庫(kù)中有一個(gè) SQL 線程會(huì)從中繼日志讀取 binlog疚颊,然后執(zhí)行 binlog 日志中的內(nèi)容,也就是在自己本地再次執(zhí)行一遍 SQL信认,這樣就可以保證自己跟主庫(kù)的數(shù)據(jù)是一樣的材义。
從庫(kù)數(shù)據(jù)延時(shí)的原因: 就是從庫(kù)同步主庫(kù)數(shù)據(jù)的過(guò)程是串行化的,也就是說(shuō)主庫(kù)上并行的操作嫁赏,在從庫(kù)上會(huì)串行執(zhí)行其掂。所以這就是一個(gè)非常重要的點(diǎn)了,由于從庫(kù)從主庫(kù)拷貝日志以及串行執(zhí)行 SQL 的特點(diǎn)潦蝇,在高并發(fā)場(chǎng)景下款熬,從庫(kù)的數(shù)據(jù)一定會(huì)比主庫(kù)慢一些深寥,是有延時(shí)的。所以經(jīng)常出現(xiàn)贤牛,剛寫(xiě)入主庫(kù)的數(shù)據(jù)可能是讀不到的惋鹅,要過(guò)幾十毫秒,甚至幾百毫秒才能讀取到殉簸。主庫(kù)并發(fā)量越高闰集,從庫(kù)的延遲就越久。
另外:就是如果主庫(kù)突然宕機(jī)般卑,然后恰好數(shù)據(jù)還沒(méi)同步到從庫(kù)武鲁,那么有些數(shù)據(jù)可能在從庫(kù)上是沒(méi)有的,有些數(shù)據(jù)可能就丟失了蝠检。
Mysql為了緩解上面兩個(gè)問(wèn)題沐鼠,有兩個(gè)機(jī)制來(lái)緩解:半同步復(fù)制和并行復(fù)制
半同步復(fù)制:也叫 semi-sync 復(fù)制,指的就是主庫(kù)寫(xiě)入 binlog 日志之后蝇率,就會(huì)將強(qiáng)制此時(shí)立即將數(shù)據(jù)同步到從庫(kù),從庫(kù)將日志寫(xiě)入自己本地的 relay log 之后刽沾,接著會(huì)返回一個(gè) ack 給主庫(kù)本慕,主庫(kù)接收到至少一個(gè)從庫(kù)的 ack之后才會(huì)認(rèn)為寫(xiě)操作完成了。
并行復(fù)制侧漓,指的是從庫(kù)開(kāi)啟多個(gè)線程锅尘,并行讀取 relay log 中不同庫(kù)的日志,然后并行重放不同庫(kù)的日志布蔗,這是庫(kù)級(jí)別的并行藤违。
MySQL 主從同步延時(shí)問(wèn)題的解決方案
假設(shè)有以下邏輯代碼:1 先插入一條數(shù)據(jù)(走主庫(kù)),2 再把它查出來(lái)(走從庫(kù))纵揍,3 然后更新這條數(shù)據(jù) (走主庫(kù))顿乒。在主從復(fù)制環(huán)境下,當(dāng)主庫(kù)并發(fā)量寫(xiě)很高時(shí)泽谨,第二步獲取的結(jié)果有可能是空的璧榄,從而導(dǎo)致錯(cuò)誤的發(fā)生。
解決辦法:
- 分庫(kù)吧雹,將一個(gè)主庫(kù)拆分為多個(gè)主庫(kù)骨杂,每個(gè)主庫(kù)的寫(xiě)并發(fā)就減少了幾倍,此時(shí)主從延遲可以忽略不計(jì)雄卷。
- 打開(kāi) MySQL 支持的并行復(fù)制搓蚪,多個(gè)庫(kù)并行復(fù)制。但是 如果說(shuō)某個(gè)庫(kù)的寫(xiě)入并發(fā)就是特別高丁鹉,單庫(kù)寫(xiě)并發(fā)達(dá)到了 2000/s妒潭,并行復(fù)制還是沒(méi)意義悴能,因?yàn)椴⑿袕?fù)制是庫(kù)層面的。并行復(fù)制的意義并不是很大
- 重寫(xiě)代碼杜耙,寫(xiě)代碼的同學(xué)搜骡,要慎重,插入數(shù)據(jù)時(shí)立馬查詢(xún)可能查不到佑女。
- 如果確實(shí)是存在必須先插入记靡,立馬要求就查詢(xún)到,然后立馬就要反過(guò)來(lái)執(zhí)行一些操作团驱,對(duì)這個(gè)查詢(xún)?cè)O(shè)置直連主庫(kù)摸吠。但是不推薦這種方法,你要是這么搞嚎花,讀寫(xiě)分離的意義就喪失了寸痢。