PostgreSQL 秒杀场景优化

前端之家收集整理的这篇文章主要介绍了PostgreSQL 秒杀场景优化前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。
秒杀场景的典型瓶颈在于对同一条记录的多次更新请求,然后只有一个或者少量请求是成功的,其他请求是以失败或更新不到告终。
例如,Iphone的1元秒杀,如果我只放出1台Iphone,我们把它看成一条记录,秒杀开始后,谁先抢到(更新这条记录的锁),谁就算秒杀成功。
例如:
使用一个标记位来表示这条记录是否已经被更新,或者记录更新的次数(几台Iphone)。

@H_301_14@update tbl set@H_301_14@ xxx=@H_301_14@xxx,@H_301_14@upd_cnt+1@H_301_14@ where@H_301_14@ idpk and@H_301_14@ upd_cnt1<=5;@H_301_14@ --@H_301_14@ 假设可以秒杀

这种方法的弊端:
获得锁的用户在处理这条记录时,可能成功,也可能失败,或者可能需要很长时间,(例如数据库响应慢)在它结束事务前,其他会话只能等着。
等待是非常不科学的,因为对于没有获得锁的用户,等待是在浪费时间。
所以一般的优化处理方法是先使用for update nowait的方式来避免等待,即如果无法即可获得锁,那么就不等待。

begin;
select@H_301_14@ from@H_301_14@ tbl for@H_301_14@ update nowait如果用户无法即刻获得锁,则返回错误。从而这个事务回滚。
end;

这种方法可以减少用户的等待时间,因为无法即刻获得锁后就直接返回了。
但是这种方法也存在一定的弊端,对于一个商品,如果可以秒杀多台的话,我们用1条记录来存储多台,降低了秒杀的并发性。
因为我们用的是行锁。
解决这个问题办法很多,最终就是要提高并发性,例如:
1. 分段秒杀,把商品数量打散,拆成多个段,从而提高并发处理能力。
总体来说,优化的思路是减少锁等待时间,避免串行,尽量并行。

优化到这里就结束了吗?显然没有,以上方法任意数据库都可以做到,如果就这样结束怎么体现Postgresql的特性呢?
Postgresql还提供了一个锁类型,advisory锁,这种锁比行锁更加轻量,支持会话级别和事务级别。(但是需要注意ID是全局的,否则会相互干扰,也就是说,所有参与秒杀或者需要用到advisory lock的ID需要在单个库内保持全局唯一)
例子:

update tbl set xxx=xxxupd_cnt+1 where idpk and upd_cnt1<=5pg_try_advisory_xact_lock(:id);

最后必须要对比一下for update nowait和advisory lock的性能
下面是在一台本地虚拟机上的测试。
新建一张秒杀表

@H_301_14@postgres=# \d t1
Table "public.t1"
Column | Type | Modifiers
--------+---------+-----------
id | integer | not null
info | text |
Indexes:
"t1_pkey" PRIMARY KEY,btree (id)

只有一条记录,不断的被更新
# select * from t1;
   
   
id | info
----+-------------------------------
1 | 2015-09-14 09:47:04.703904+08
(1 row)

压测for update nowait的方式:

@H_301_14@CREATE OR REPLACE FUNCTION public.@H_301_14@f1(@H_301_14@i_id integer)@H_301_14@
@H_301_14@RETURNS void@H_301_14@LANGUAGE plpgsql
@H_301_14@AS $function$
@H_301_14@declare
begin@H_301_14@
@H_301_14@ perform t1 i_id update t1 infonow()::@H_301_14@text i_id exception when@H_301_14@ others thenreturn
$function$
@H_301_14@postgres@digoal->@H_301_14@ cat test1sql
@H_301_14@\setrandom id 1
select@H_301_14@ f1(:@H_301_14@id);

压测advisory lock的方式:

@H_301_14@ cat testsql
1
update@H_301_14@=:@H_301_14@id pg_try_advisory_xact_lock);

清除压测统计数据:
# select pg_stat_reset();
pg_stat_reset
---------------
(1 row)
postgres=# select * from pg_stat_all_tables where relname='t1';
-[ RECORD 1 ]-------+-------
relid | 184731
schemaname | public
relname | t1
seq_scan | 0
seq_tup_read | 0
idx_scan | 0
idx_tup_fetch | 0
n_tup_ins | 0
n_tup_upd | 0
n_tup_del | 0
n_tup_hot_upd | 0
n_live_tup | 0
n_dead_tup | 0
n_mod_since_analyze | 0
last_vacuum |
last_autovacuum |
last_analyze |
last_autoanalyze |
vacuum_count | 0
autovacuum_count | 0
analyze_count | 0
autoanalyze_count | 0

压测结果:
pgbench -@H_301_14@M prepared n r P f ./@H_301_14@test1sql c 20@H_301_14@j T 60
......
@H_301_14@transaction type:@H_301_14@ Custom@H_301_14@ query
@H_301_14@scaling factorquery@H_301_14@ mode prepared
@H_301_14@number of clients20
number@H_301_14@ of threadsduration60@H_301_14@ s
@H_301_14@number of transactions actually processed792029
latency@H_301_14@ average1.505@H_301_14@ ms
@H_301_14@latency stddev4.275@H_301_14@tps 13196.542846@H_301_14@including connections establishing)
13257.270709@H_301_14@excluding connections establishingstatement latencies in@H_301_14@ milliseconds:
@H_301_14@ 0.002625@H_301_14@ \setrandom id 1
1.502420@H_301_14@ );
@H_301_14@# select * from pg_stat_all_tables where relname='t1';
idx_scan | 896963 // 大多数是无用功
idx_tup_fetch | 896963 n_tup_upd | 41775
n_tup_hot_upd | 41400
n_dead_tup | 928
n_mod_since_analyze | 41774
autoanalyze_count | 0
@H_301_14@test1392372
0.8512.47523194.83105423400.4115010.0025940.848536@H_301_14@ update t1 -[ RECORD 1 ]-------+--------
idx_scan | 1368933 // 大多数是无用功
idx_tup_fetch | 1368933 n_tup_upd | 54957
n_tup_hot_upd | 54489
n_dead_tup | 1048
n_mod_since_analyze | 54957
我们注意到,不管用哪种方法,都会浪费掉很多次的无用功扫描。
为了解决无用扫描的问题,可以使用以下函数。(当然,还有更好的方法是对用户透明。)
fdeclare
@H_301_14@ a_lock boolean@H_301_14@:=false;
into@H_301_14@ a_lock;
ifthen
@H_301_14@ update t1 endif1217195
0.9733.56320283.31400120490.1433630.0027030.970209@H_301_14@ fidx_scan | 75927
idx_tup_fetch | 75927
n_tup_upd | 75927
n_tup_hot_upd | 75902
n_dead_tup | 962
n_mod_since_analyze | 75927
除了吞吐率的提升,我们其实还看到真实的处理数(更新次数)也有提升,所以不仅仅是降低了等待延迟,实际上也提升了处理能力。
最后提供一个物理机上的数据参考,使用128个并发连接,同时对一条记录进行更新:
不做任何优化的并发处理能力:
query
@H_301_14@ prepared
128
100@H_301_14@ s
285673
44.806@H_301_14@ ms
45.7512855.547375@H_301_14@)
2855.856976@H_301_14@:
0.0025091
44.803299 使用for update nowait的并发处理能力:
6663253
1.9192.80466623.16944566630.3079990.0019341.917297 使用advisory lock后的并发处理能力:
19154754
0.6671.054191520.550924191546.2080510.0020850.664420 使用advisory lock,性能相比不做任何优化性能提升了约66倍,相比for update nowait性能提升了约1.8倍。
这种优化可以快速告诉用户是否能秒杀到此类商品,而不需要等待其他用户更新结束后才知道。所以大大降低了RT,提高了吞吐率。

猜你在找的Postgre SQL相关文章