聊一聊双十一背后的技术 - 不一样的秒杀技术, 裸秒
聊一聊雙十一背后的技術(shù) - 不一樣的秒殺技術(shù), 裸秒
作者
digoal
日期
2016-11-17
標(biāo)簽
PostgreSQL , 秒殺 , 裸秒 , ad lock
雙十一背后的技術(shù)系列文章
《聊一聊雙十一背后的技術(shù) - 物流, 動(dòng)態(tài)路徑規(guī)劃》
《聊一聊雙十一背后的技術(shù) - 分詞和搜索》
《聊一聊雙十一背后的技術(shù) - 強(qiáng)奸式秒殺技術(shù)實(shí)現(xiàn)》
《聊一聊雙十一背后的技術(shù) - 毫秒分詞算啥, 試試正則和相似度》
云棲聚能聊 - 聊一聊雙十一背后的數(shù)據(jù)庫技術(shù)
背景
秒殺在商品交易中是一個(gè)永恒的話題,從雙十一,到一票難求,比的僅僅是手快嗎?
其實(shí)對(duì)于交易平臺(tái)來說,面對(duì)的不僅僅是人肉,還有很多腳本,外掛自動(dòng)化的搶購系統(tǒng),壓力可想而知。
秒殺的優(yōu)化手段很多,就拿數(shù)據(jù)庫來說,有用排隊(duì)機(jī)制的,有用異步消息的,有用交易合并的。
今天,我要給大家介紹一種更極端的秒殺應(yīng)對(duì)方法,裸秒。
(其實(shí)我很久以前就寫過類似的文章,趁雙十一跟大伙再練練)
目前可能只有PostgreSQL支持裸秒,也即是說,來吧,強(qiáng)暴我吧,一起上。 有點(diǎn)淫蕩,但是確實(shí)就是這么暴力。
PostgreSQL提供了一種ad lock,可以讓用戶盡情的釋放激情,以一臺(tái)32核64線程的機(jī)器為例,每秒可以獲取、探測(cè)約130萬次的ad lock。
試想一下,對(duì)單條記錄的秒殺操作,達(dá)到了單機(jī)100萬/s的處理能力后,秒殺算什么?100臺(tái)機(jī)器就能處理1億/s的秒殺請(qǐng)求,不行我的小心臟受不了了,下面聽我娓娓道來。
秒殺場(chǎng)景簡介
雖然秒殺已經(jīng)很普遍了,但是出于文章的完整性,還是簡單介紹一下秒殺的業(yè)務(wù)背景。
例如,Iphone的1元秒殺,如果我只放出1臺(tái)Iphone,我們把它看成一條記錄,秒殺開始后,誰先搶到(更新這條記錄的鎖),誰就算秒殺成功。
對(duì)數(shù)據(jù)庫來說,秒殺瓶頸在于并發(fā)的對(duì)同一條記錄的多次更新請(qǐng)求,只有一個(gè)或者少量請(qǐng)求是成功的,其他請(qǐng)求是以失敗或更新不到記錄而告終。
例如有100臺(tái)IPHONE參與秒殺,并發(fā)來搶的用戶有100萬,對(duì)于數(shù)據(jù)庫來說,最小粒度的為行鎖,當(dāng)有一個(gè)用戶在更新這條記錄時(shí),其他的999999個(gè)用戶是在等待中度過的,以此類推。
除了那100個(gè)幸運(yùn)兒,其他的用戶的等待都是無謂的,甚至它們不應(yīng)該到數(shù)據(jù)庫中來浪費(fèi)資源。
傳統(tǒng)的做法,使用一個(gè)標(biāo)記位來表示這條記錄是否已經(jīng)被更新,或者記錄更新的次數(shù)(幾臺(tái)Iphone)。
update tbl set xxx=xxx,upd_cnt=upd_cnt+1 where id=pk and upd_cnt+1<=5; -- 假設(shè)可以秒殺5臺(tái)這種方法的弊端:
獲得鎖的用戶在處理這條記錄時(shí),可能成功,也可能失敗,或者可能需要很長時(shí)間,(例如數(shù)據(jù)庫響應(yīng)慢)在它結(jié)束事務(wù)前,其他會(huì)話只能等著。
等待是非常不科學(xué)的,因?yàn)閷?duì)于沒有獲得鎖的用戶,等待是在浪費(fèi)時(shí)間。
常用的秒殺優(yōu)化手段
1. 一般的優(yōu)化處理方法是先使用for update nowait的方式來避免等待,即如果無法即可獲得鎖,那么就不等待。
begin; select 1 from tbl where id=pk for update nowait; -- 如果用戶無法即刻獲得鎖,則返回錯(cuò)誤。從而這個(gè)事務(wù)回滾。 update tbl set xxx=xxx,upd_cnt=upd_cnt+1 where id=pk and upd_cnt+1<=5; end;這種方法可以減少用戶的等待時(shí)間,因?yàn)闊o法即刻獲得鎖后就直接返回了。
2. 合并請(qǐng)求,即將多個(gè)更新合并到一個(gè)更新的請(qǐng)求,這種做法需要修改內(nèi)核,同時(shí)會(huì)破壞ACID,因?yàn)槿绻喜⒑蟮恼?qǐng)求失敗了,會(huì)導(dǎo)致合并中的所有人的請(qǐng)求失敗。(與分組提交不一樣,分組提交是不會(huì)破壞ACID的)。
那么接下來我們看看AD LOCK。
什么是ad lock
手冊(cè)中的說明,AD LOCK是一種面向用戶的輕量級(jí)鎖,鎖的目標(biāo)是一個(gè)整型,分為事務(wù)級(jí)和會(huì)話級(jí)的鎖,以及共享和排他鎖。
在單個(gè)DB內(nèi),只要鎖的整型值不一樣,就可以獲得鎖,如果值一樣,可以使用TRY來加鎖,沒有獲得則立即返回FALSE。
https://www.postgresql.org/docs/current/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
Table 9-87. Advisory Lock Functions
| pg_advisory_lock(key bigint) | void | Obtain exclusive session level advisory lock |
| pg_advisory_lock(key1 int, key2 int) | void | Obtain exclusive session level advisory lock |
| pg_advisory_lock_shared(key bigint) | void | Obtain shared session level advisory lock |
| pg_advisory_lock_shared(key1 int, key2 int) | void | Obtain shared session level advisory lock |
| pg_advisory_unlock(key bigint) | boolean | Release an exclusive session level advisory lock |
| pg_advisory_unlock(key1 int, key2 int) | boolean | Release an exclusive session level advisory lock |
| pg_advisory_unlock_all() | void Release | all session level advisory locks held by the current session |
| pg_advisory_unlock_shared(key bigint) | boolean | Release a shared session level advisory lock |
| pg_advisory_unlock_shared(key1 int, key2 int) | boolean | Release a shared session level advisory lock |
| pg_advisory_xact_lock(key bigint) | void | Obtain exclusive transaction level advisory lock |
| pg_advisory_xact_lock(key1 int, key2 int) | void | Obtain exclusive transaction level advisory lock |
| pg_advisory_xact_lock_shared(key bigint) | void | Obtain shared transaction level advisory lock |
| pg_advisory_xact_lock_shared(key1 int, key2 int) | void | Obtain shared transaction level advisory lock |
| pg_try_advisory_lock(key bigint) | boolean | Obtain exclusive session level advisory lock if available |
| pg_try_advisory_lock(key1 int, key2 int) | boolean | Obtain exclusive session level advisory lock if available |
| pg_try_advisory_lock_shared(key bigint) | boolean | Obtain shared session level advisory lock if available |
| pg_try_advisory_lock_shared(key1 int, key2 int) | boolean | Obtain shared session level advisory lock if available |
| pg_try_advisory_xact_lock(key bigint) | boolean | Obtain exclusive transaction level advisory lock if available |
| pg_try_advisory_xact_lock(key1 int, key2 int) | boolean | Obtain exclusive transaction level advisory lock if available |
| pg_try_advisory_xact_lock_shared(key bigint) | boolean | Obtain shared transaction level advisory lock if available |
| pg_try_advisory_xact_lock_shared(key1 int, key2 int) | boolean | Obtain shared transaction level advisory lock if available |
通常數(shù)據(jù)庫支持的最小粒度的鎖(指開放給用戶的)是行鎖,行鎖相比LWLOCK,SPINLOCK等是非常重的,所以傳統(tǒng)的行鎖在秒殺中會(huì)成為非常大的瓶頸,包括鎖的等待。
ad lock的用途
ad lock的用途,除了我接下來要說的秒殺,其實(shí)還有很多用途,例如
并發(fā)的安全性檢查,
遞歸調(diào)用中用于UPSERT的場(chǎng)景,
業(yè)務(wù)邏輯設(shè)計(jì)中用來確保原子操作等。
ad lock的性能指標(biāo)
因?yàn)锳D LOCK很輕量化,不需要訪問數(shù)據(jù),不需要執(zhí)行冗長的代碼,所以很高效。
32核64線程機(jī)器測(cè)試可以達(dá)到131萬次/s的鎖請(qǐng)求。
vi test.sql \set id random(1,100000000) select pg_try_advisory_xact_lock(:id);pgbench -M prepared -n -r -P 1 -f ./test.sql -c 96 -j 96 -T 100transaction type: ./test.sql scaling factor: 1 query mode: prepared number of clients: 96 number of threads: 96 duration: 100 s number of transactions actually processed: 131516823 latency average = 0.072 ms latency stddev = 0.070 ms tps = 1314529.211060 (including connections establishing) tps = 1315395.309707 (excluding connections establishing) script statistics:- statement latencies in milliseconds:0.001 \set id random(1,100000000)0.074 select pg_try_advisory_xact_lock(:id);ad lock用于秒殺的例子
在數(shù)據(jù)庫中,商品通常有唯一ID,我們可以對(duì)這個(gè)ID加鎖,(當(dāng)然,如果對(duì)不同的表這個(gè)ID有重疊的可能,我們可以加偏移量或者其他的手段來達(dá)到無沖突)。
加鎖成功才會(huì)去對(duì)行加鎖,執(zhí)行更新,這樣就能規(guī)避掉無效的行鎖等待,以及冗長的查詢代碼。
使用 AD LOCK 對(duì)單條記錄的并發(fā)更新處理QPS可以達(dá)到39.1萬/s,被秒殺的商品很快就會(huì)變成售罄狀態(tài),不會(huì)再浪費(fèi)數(shù)據(jù)庫的資源。
create table test(id int primary key, crt_time timestamp); insert into test values (1); vi test.sql update test set crt_time=now() where id=1 and pg_try_advisory_xact_lock(1);pgbench -M prepared -n -r -P 1 -f ./test.sql -c 64 -j 64 -T 100transaction type: ./test.sql scaling factor: 1 query mode: prepared number of clients: 64 number of threads: 64 duration: 100 s number of transactions actually processed: 39104368 latency average = 0.163 ms latency stddev = 0.216 ms tps = 391012.743072 (including connections establishing) tps = 391175.983419 (excluding connections establishing) script statistics:- statement latencies in milliseconds:0.163 update test set crt_time=now() where id=1 and pg_try_advisory_xact_lock(1);此時(shí)數(shù)據(jù)庫主機(jī)還有66.2%的空閑CPU資源可用使用。
top - 13:12:43 up 51 days, 18:41, 2 users, load average: 1.12, 0.97, 0.78 Tasks: 1463 total, 28 running, 1435 sleeping, 0 stopped, 0 zombie Cpu(s): 24.5%us, 9.3%sy, 0.0%ni, 66.2%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st Mem: 529321832k total, 235226420k used, 294095412k free, 903076k buffers Swap: 0k total, 0k used, 0k free, 62067636k cached對(duì)比傳統(tǒng)的例子
傳統(tǒng)的消除等待的做法是這樣的,通過select for update nowait。
begin; select 1 from tbl where id=pk for update nowait; -- 如果用戶無法即刻獲得鎖,則返回錯(cuò)誤。從而這個(gè)事務(wù)回滾。 update tbl set xxx=xxx,upd_cnt=upd_cnt+1 where id=pk and upd_cnt+1<=5; end;在PG中,可以使用do語句,把以上合成到一個(gè)塊里面操作。
使用傳統(tǒng)的方法,每秒可以處理8.6萬。
vi test.sql do language plpgsql $$declare begin with t as (select * from test where id=1 for update nowait) update test set crt_time=now() from t where t.id=test.id; exception when others then return; end; $$ ;pgbench -M prepared -n -r -P 1 -f ./test.sql -c 64 -j 64 -T 100transaction type: ./test.sql scaling factor: 1 query mode: prepared number of clients: 64 number of threads: 64 duration: 100 s number of transactions actually processed: 8591222 latency average = 0.744 ms latency stddev = 0.713 ms tps = 85888.823884 (including connections establishing) tps = 85924.666940 (excluding connections establishing) script statistics:- statement latencies in milliseconds:0.744 do language plpgsql $$declare begin with t as (select * from test where id=1 for update nowait) update test set crt_time=now() from t where t.id=test.id; exception when others then return; end; $$ ;CPU剩余54.5%
top - 13:13:48 up 51 days, 18:42, 2 users, load average: 8.14, 2.69, 1.37 Tasks: 1464 total, 21 running, 1442 sleeping, 0 stopped, 1 zombie Cpu(s): 41.7%us, 3.8%sy, 0.0%ni, 54.5%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st Mem: 529321832k total, 235256052k used, 294065780k free, 903176k buffers Swap: 0k total, 0k used, 0k free, 62068308k cached單個(gè)商品真實(shí)扣減吞吐
測(cè)試單個(gè)商品的真實(shí)扣減吞吐,我們看到QUERY QPS很高,但是真實(shí)的扣減當(dāng)然不會(huì)有那么高,因?yàn)闆]有拿到鎖就返回了。
postgres=# create table upd(id int primary key, cnt int8); postgres=# insert into upd values(1,0);vi t0.sql update upd set cnt=cnt-1 where id=1 and pg_try_advisory_xact_lock(1); \sleep 10 us.... vi t7.sql update upd set cnt=cnt-1 where id=1 and pg_try_advisory_xact_lock(1); \sleep 80 uspgbench -M prepared -n -r -P 1 -f t0.sql -f t1.sql -f t2.sql -f t3.sql -f t4.sql -f t5.sql -f t6.sql -f t7.sql -c 64 -j 64 -T 100postgres=# select * from upd;id | cnt ----+---------1 | -611249 (1 row)單個(gè)商品,每秒扣減6112.49次,通常參與秒殺的商品,庫存都不會(huì)很多,通常在一千以內(nèi),否則就不叫秒殺了,用戶也犯不著來秒殺。
所以這個(gè)值是完全滿足現(xiàn)實(shí)需求的。
整個(gè)平臺(tái)所有商品真實(shí)扣減吞吐
假設(shè)整個(gè)平臺(tái)有1000萬商品,測(cè)試一下使用這種方法的整體扣減吞吐。
postgres=# create table upd(id int primary key, cnt int8); postgres=# insert into upd select generate_series(1,10000000), 0;vi test.sql \set id random(1,10000000) update upd set cnt=cnt-1 where id=:id and pg_try_advisory_xact_lock(:id);pgbench -M prepared -n -r -P 1 -f ./test.sql -c 64 -j 64 -T 100postgres=# select sum(cnt) from upd;sum ------------27233112 (1 row)查看cnt得到真實(shí)的扣減情況,整個(gè)平臺(tái)來說,每秒約扣減272331.12個(gè)商品,即每秒有27萬個(gè)商品售出。
ad lock相比其他秒殺優(yōu)化的優(yōu)勢(shì)
使用AD LOCK可以使得CPU開銷最小化,等待最小化,從本文的測(cè)試CASE來看,單條記錄的更新可以達(dá)到39.1萬/s。
傳統(tǒng)的手段只能達(dá)到8.6萬/s。
使用AD LOCK不破壞ACID,單個(gè)請(qǐng)求單個(gè)事務(wù),不影響其他的事務(wù)。
合并優(yōu)化,本質(zhì)上是破壞了ACID的,如果合并失敗,會(huì)導(dǎo)致所有相關(guān)的請(qǐng)求失敗。
如果你對(duì)PG感興趣,可以再了解一下
《德哥的PostgreSQL私房菜 - 史上最屌PG資料合集》
Count
本文為云棲社區(qū)原創(chuàng)內(nèi)容,未經(jīng)允許不得轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)發(fā)送郵件至yqeditor@list.alibaba-inc.com;如果您發(fā)現(xiàn)本社區(qū)中有涉嫌抄襲的內(nèi)容,歡迎發(fā)送郵件至:yqgroup@service.aliyun.com 進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),本社區(qū)將立刻刪除涉嫌侵權(quán)內(nèi)容。
【云棲快訊】阿里巴巴小程序繁星計(jì)劃,20億補(bǔ)貼第一彈云應(yīng)用立即開通購買,限量從速!??詳情請(qǐng)點(diǎn)擊
來源:https://yq.aliyun.com/articles/64351
與50位技術(shù)專家面對(duì)面20年技術(shù)見證,附贈(zèng)技術(shù)全景圖總結(jié)
以上是生活随笔為你收集整理的聊一聊双十一背后的技术 - 不一样的秒杀技术, 裸秒的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 面包机做面包为什么硬?
- 下一篇: 好零友零食这段时间开店有哪些政策?