PostgreSQL秒殺4種方法-增加批量流式加減庫存方法
標籤
PostgreSQL , 秒殺 , 批量扣減 , 流處理
背景
秒殺,即對同一個商品,消減庫存。
帶來的資料庫問題是熱點行,由於資料庫最細粒度的鎖通常是行鎖,同一行同一時刻只能被一個事務更新,其他事務如果要更新同一行,會等待行級排它鎖。
PostgreSQL中有幾種方法來處理秒殺:
1、暴力,直接更新
2、skip locked,跳過被鎖的行,直接返回,防止會話長時間等待。可以發起重試。
3、advisory lock,跳過被鎖的行,直接返回,防止會話長時間等待。可以發起重試。
4、把更新轉成寫入,批量消費,可以在核心層面實現(批量消耗),也可以在業務層面實現。
看一下幾種方法的效能。
create table t(
id int primary key, -- 商品ID
cnt int -- 庫存
);
insert into t values (1,2000000000);
都使用100個併發連線。
1、暴力更新
100併發
transaction type: ./test.sql
scaling factor: 1
query mode: prepared
number of clients: 100
number of threads: 100
duration: 120 s
number of transactions actually processed: 342042
latency average = 35.083 ms
latency stddev = 36.273 ms
tps = 2849.507392 (including connections establishing)
tps = 2849.837580 (excluding connections establishing)
script statistics:
- statement latencies in milliseconds:
35.083 update t set cnt=cnt-1 where id=1;
2併發
transaction type: ./test.sql
scaling factor: 1
query mode: prepared
number of clients: 2
number of threads: 2
duration: 120 s
number of transactions actually processed: 2819491
latency average = 0.085 ms
latency stddev = 0.009 ms
tps = 23495.740654 (including connections establishing)
tps = 23496.241610 (excluding connections establishing)
script statistics:
- statement latencies in milliseconds:
0.085 update t set cnt=cnt-1 where id=1;
2、skip locked row
skip locked是PG提供的一種語法,可以跳過被鎖的行。
update t set cnt=cnt-1 where ctid = any (array(select ctid from t where id=1 for update skip locked)) returning *;
transaction type: ./test.sql
scaling factor: 1
query mode: prepared
number of clients: 100
number of threads: 100
duration: 120 s
number of transactions actually processed: 6508322
latency average = 1.844 ms
latency stddev = 2.390 ms
tps = 54226.911876 (including connections establishing)
tps = 54233.143956 (excluding connections establishing)
script statistics:
- statement latencies in milliseconds:
1.843 update t set cnt=cnt-1 where ctid = any (array(select ctid from t where id=1 for update skip locked)) returning *;
3、advisory lock
advisory lock,更新時,鎖住PK,而不是ROW本身,如果未獲得鎖,直接返回。與skip locked類似,但是更加高效,因為不需要SEARCH ROW。
transaction type: ./test.sql
scaling factor: 1
query mode: prepared
number of clients: 100
number of threads: 100
duration: 120 s
number of transactions actually processed: 31690080
latency average = 0.379 ms
latency stddev = 0.671 ms
tps = 264047.289635 (including connections establishing)
tps = 264083.172081 (excluding connections establishing)
script statistics:
- statement latencies in milliseconds:
0.379 update t set cnt=cnt-1 where id=1 and pg_try_advisory_xact_lock(1);
4、流式批量更新
流式批量處理,將更新轉換為寫入,避免熱點行鎖,然後批量合併到庫存表。
但是需要注意,這個屬於非同步的方法,也就是說,可能導致庫存負數。不過消費足夠快的話,不會有太大問題。
1、建立FEED表,儲存使用者扣減庫存的記錄。
create table stat(
uid int, -- 使用者ID
id int, -- 商品ID
cnt int, -- 購買數量
crt_time timestamp default now() -- 寫入時間
);
2、建立使用者扣減庫存的函式,這裡面使用一個判斷,當庫存(也就是說,預設不關心還沒有合併到最終結果的那些消費記錄。)
create or replace function consume(int, int, int) returns int as $$
insert into stat (uid, id, cnt) select $1 as uid, $2 as id, $3 as cnt from t where id=$2 and cnt+$3>=0 returning cnt;
$$ language sql strict;
3、排程,比如每100毫秒排程一次,非同步合併
with tmp as (
delete from stat where ctid = any ( array (
select ctid from stat limit 1000000
)) returning *
),
t1 as (select id, sum(cnt) as cnt from tmp group by id)
update t set cnt=t.cnt+t1.cnt from t1 where t.id=t1.id;
4、排程(可以使用autovacuum自動排程),垃圾回收。
vacuum stat;
5、壓測
pgbench -M prepared -n -r -P 1 -f ./test.sql -c 100 -j 100 -T 120
transaction type: ./test.sql
scaling factor: 1
query mode: prepared
number of clients: 100
number of threads: 100
duration: 120 s
number of transactions actually processed: 17155235
latency average = 0.699 ms
latency stddev = 0.546 ms
tps = 142929.999871 (including connections establishing)
tps = 142949.652076 (excluding connections establishing)
script statistics:
- statement latencies in milliseconds:
0.702 select consume(1,1,1);
如果我們需要按先後順序合併,可以加個索引
create index idx_stat_2 on stat(crt_time);
合併SQL如下:
with tmp as (
delete from stat where ctid = any ( array (
select ctid from stat order by crt_time limit 1000000
)) returning *
),
t1 as (select id, sum(cnt) as cnt from tmp group by id)
update t set cnt=t.cnt+t1.cnt from t1 where t.id=t1.id;
效能如下:
transaction type: ./test.sql
scaling factor: 1
query mode: prepared
number of clients: 100
number of threads: 100
duration: 120 s
number of transactions actually processed: 10394002
latency average = 1.154 ms
latency stddev = 0.951 ms
tps = 86585.839187 (including connections establishing)
tps = 86597.281593 (excluding connections establishing)
script statistics:
- statement latencies in milliseconds:
1.155 select consume(1,1);
消費速度與寫入速度幾乎一致。只有排程延遲。
如果通過核心層面來實現的話,可以避免庫存負數這個問題,提高一定的效能,但是:為了不破壞原有的一致性和可靠性,同樣不能避免批量提交前,會話佔用資料庫連線的問題。
所以是有利有弊的。
另一方面,如果我們在內部實現同一個ID最多分配給兩個SERVER PROCESS執行,也能很好的解決這個問題。類似oracle的shared server mode,同時對id進行路由分配,至多給兩個SHARED PROCESS,從而每個ID保證2萬多的TPS。
類似的流計算案例
《阿里雲RDS PostgreSQL varbitx實踐 – 流式標籤 (閱後即焚流式批量計算) – 萬億級,任意標籤圈人,毫秒響應》
《PostgreSQL 流式統計 – insert on conflict 實現 流式 UV(distinct), min, max, avg, sum, count …》
《HTAP資料庫 PostgreSQL 場景與效能測試之 32 – (OLTP) 高吞吐資料進出(堆存、行掃、無需索引) – 閱後即焚(JSON + 函式流式計算)》
《HTAP資料庫 PostgreSQL 場景與效能測試之 31 – (OLTP) 高吞吐資料進出(堆存、行掃、無需索引) – 閱後即焚(讀寫大吞吐並測)》
《HTAP資料庫 PostgreSQL 場景與效能測試之 27 – (OLTP) 物聯網 – FEED日誌, 流式處理 與 閱後即焚 (CTE)》
《PostgreSQL 非同步訊息實踐 – Feed系統實時監測與響應(如 電商主動服務) – 分鐘級到毫秒級的實現》
小結
處理能力如下
1、暴力,直接更新
2849/s (100併發)
2.35萬/s (2併發)
2、skip locked,跳過被鎖的行,直接返回,防止會話長時間等待。可以發起重試。
5.4萬/s
3、advisory lock,跳過被鎖的行,直接返回,防止會話長時間等待。可以發起重試。
26.4萬/s
4、把更新轉成寫入,批量消費,可以在核心層面實現(批量消耗),也可以在業務層面實現。
14.3萬/s (亂序消費)
8.6萬/s (按順序消費)
核心層面來解決熱點,批量合併或者shared server process和根據ID分配(每個ID 2.35萬/s的處理吞吐已經夠用了,因為秒殺完後,庫存為負時,就沒有鎖衝突問題了),是最靠譜的。
相關文章
- Go 庫存扣減的幾種實現方法Go
- 秒殺系統中的扣減庫存和流量削峰
- 電商 秒殺 庫存問題
- 批量殺死MySQL連線的幾種方法詳解MySql
- PostgreSQL秒殺場景優化SQL優化
- JS加減乘除位移方法封裝JS封裝
- Redis秒殺實戰-微信搶紅包-秒殺庫存,附案例原始碼(Jmeter壓測)Redis原始碼JMeter
- 強大:MyBatis ,三種流式查詢方法MyBatis
- 超大整數的加減乘除計算方法
- 筆記:如何使用postgresql做順序扣減庫存筆記SQL
- 從京東618秒殺聊聊秒殺限流的多種實現
- Objective-C 中不帶加減號的方法Object
- 增加資料庫SGA的方法資料庫
- DataTable資料批量寫入資料庫三種方法比較資料庫
- 按照名字殺死程式的四種方法
- python庫批量安裝的方法Python
- linux下如何批量殺JAVA程式或某個程式方法LinuxJava
- 雲端計算秒殺電商秒殺
- 批次殺死MySQL連線的幾種方法MySql
- 減少SQL日誌的三種方法(轉)SQL
- 併發減庫存
- Python IDLE可以使用ctrl +[ 批量減少縮排,ctrl + ]批量增加縮排Python
- PostgreSQL建立自增主鍵的兩種方法SQL
- mac防止休眠的4種方法Mac
- 圖靈淘寶店4月秒殺預告!圖靈
- excel日期加減計算方法 excel計算日期時間差Excel
- 優化食品生產行業庫存管理的6種方法優化行業
- 搶購 庫存扣減
- 高併發秒殺系統架構詳解,不是所有的秒殺都是秒殺!架構
- 什麼,秒殺系統也有這麼多種!
- 批次殺死MySQL連線的四種方法詳解MySql
- LINUX 使用批量刪除檔案的幾種方法Linux
- 點評 ibatis+oracle 批量插入的三種方法.BATOracle
- 資料庫連線的方法種種資料庫
- linux批量殺掉程式linux批量殺掉程式Linux
- 實現 MyBatis 流式查詢的方法MyBatis
- JavaScript建立物件4種方法詳解JavaScript物件
- gpt4free 兩種部署方法GPT