雙十一大促是怎么做MySQL熱點(diǎn)數(shù)據(jù)高效更新的?
MySQL的熱點(diǎn)數(shù)據(jù)更新問題,一直都是行業(yè)內(nèi)的一個難題,對于秒殺場景至關(guān)重要。一旦處理不好,就可能會導(dǎo)致數(shù)據(jù)庫被打垮。
通常來說,對于熱點(diǎn)問題,都是選擇使用Redis來抗,比如秒殺場景借助他的單線程高并發(fā)能力來做預(yù)扣減。
常規(guī)方案
但是,引入Redis又會帶來數(shù)據(jù)不一致的問題,進(jìn)而會導(dǎo)致超賣和少賣,如果一定要在MySQL這個層面上,抗住高并發(fā)的熱點(diǎn)數(shù)據(jù)并發(fā)更新,有什么方案呢?拿庫存扣減舉例
1、庫存拆分,把一個大的庫存拆分成多個小庫存,拆分后,一次扣減動作就可以分散到不同的庫、表中進(jìn)行,降低鎖粒度提升并發(fā)。
優(yōu)點(diǎn):實(shí)現(xiàn)較簡單
缺點(diǎn):存在碎片問題、庫存調(diào)控不方便
2、請求合并,把多個庫存扣減請求,合并成一個,進(jìn)行批量更新。
優(yōu)點(diǎn):簡單
缺點(diǎn):適用于異步場景,或者經(jīng)過分析后認(rèn)為可以合并的場景
3、把update轉(zhuǎn)換成insert,直接插入一次占用記錄,然后異步統(tǒng)計(jì)剩余庫存,或者通過SQL統(tǒng)計(jì)流水方式計(jì)算剩余庫存。
優(yōu)點(diǎn):沒有update,無鎖沖突
缺點(diǎn):insert時(shí)控制不好容易超賣、insert后剩余庫存不好統(tǒng)計(jì)
企業(yè)級方案
除了上面這三個方案外,重點(diǎn)介紹一個很多大公司在用的,扛了618/雙11等大促的高并發(fā)的秒殺的方案。
那就是改造MySQL
主要思路就是,針對于頻繁更新或秒殺類業(yè)務(wù)場景,大幅度優(yōu)化對于熱點(diǎn)行數(shù)據(jù)的update操作的性能。當(dāng)開啟熱點(diǎn)更新自動探測時(shí),系統(tǒng)會自動探測是否有單行的熱點(diǎn)更新,如果有,則會讓大量的并發(fā) update 排隊(duì)執(zhí)行,以減少大量行鎖造成的并發(fā)性能下降。(另外我出了一份Java面試寶典,類似的方案有很多)
也就是說,他們改造了MySQL數(shù)據(jù)庫,讓同一個熱點(diǎn)行的更新語句,在執(zhí)行層進(jìn)行排隊(duì)。這樣的排隊(duì)相比update的排隊(duì),要輕量級很多,因?yàn)樗恍枰孕恍枰獡屾i。
這個方案的好處就是開發(fā)不需要做額外的事情,只需要開啟熱點(diǎn)檢測就行了。缺點(diǎn)就是改造MySQL數(shù)據(jù)庫有成本。不過現(xiàn)在很多云上數(shù)據(jù)庫都支持了。
效果如何?
比如阿里云的數(shù)據(jù)庫在做過改造之后,就做過單行熱點(diǎn)數(shù)據(jù)更新測試。
本示例中,分別使用兩個實(shí)例進(jìn)行測試(高可用版和三節(jié)點(diǎn)企業(yè)版),規(guī)格碼為rds.mysql.st.v52和mysql.st.12xlarge.25。
- 實(shí)例版本:MySQL 5.7
- 實(shí)例規(guī)格:90核720GB(獨(dú)占物理機(jī)型)
- 實(shí)例系列:高可用版和三節(jié)點(diǎn)企業(yè)版
- 實(shí)例存儲類型:本地盤
- 實(shí)例模板:高性能參數(shù)模板
測試數(shù)據(jù)為單表,表內(nèi)100行記錄。表結(jié)構(gòu)如下:
CREATE TABLE `sbtest1`
(
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT
,`k` INT(10) UNSIGNED NOT NULL DEFAULT '0'
,`c` CHAR(120) NOT NULL DEFAULT ''
,`pad` CHAR(60) NOT NULL DEFAULT ''
,PRIMARY KEY (`id`)
,KEY `k_1` (`k`)
)
ENGINE=InnoDB AUTO_INCREMENT=101 DEFAULT
CHARSET=utf8 MAX_ROWS=1000000
對id=100的記錄進(jìn)行并發(fā)更新,SQL如下:
UPDATE sbtest1 SET k=k+1 WHERE id=100
測試的Lua腳本如下:
pathtest = string.match(test,"(.*/)")
if pathtest then
dofile(pathtest .."common.lua")
else
require("common")
end
function thread_init(thread_id)
set_vars()
end
function event(thread_id)
local table_name
table_name ="sbtest".. sb_rand_uniform(1, oltp_tables_count)
rs = db_query("begin")
rs = db_query("update /*+commit_on_success rollback_on_fail target_affect_row(1) */ sbtest1 SET k=k+1 WHERE id=100")
rs =db_query("commit")
end
測試結(jié)果
實(shí)例類型 | 單行記錄更新峰值(TPS) |
RDS高可用版 | 1.2萬 |
RDS三節(jié)點(diǎn)企業(yè)版 | 3.1萬 |
參考資料:
騰訊云數(shù)據(jù)庫MySQL熱點(diǎn)更新:
https://cloud.tencent.com/document/product/236/63239
阿里云數(shù)據(jù)庫Inventory Hint:
https://www.alibabacloud.com/help/zh/apsaradb-for-rds/latest/inventory-hint