秒杀场景简介
虽然秒杀已经很普遍了,但是出于文章的完整性,还是简单介绍一下秒杀的业务背景。
例如,Iphone的1元秒杀,如果我只放出1台Iphone,我们把它看成一条记录,秒杀开始后,谁先抢到(更新这条记录的锁),谁就算秒杀成功。
对数据库来说,秒杀瓶颈在于并发的对同一条记录的多次更新请求,只有一个或者少量请求是成功的,其他请求是以失败或更新不到记录而告终。
例如有100台IPHONE参与秒杀,并发来抢的用户有100万,对于数据库来说,最小粒度的为行锁,当有一个用户在更新这条记录时,其他的999999个用户是在等待中度过的,以此类推。
除了那100个幸运儿,其他的用户的等待都是无谓的,甚至它们不应该到数据库中来浪费资源。
传统的做法,使用一个标记位来表示这条记录是否已经被更新,或者记录更新的次数(几台Iphone)。
update tbl set xxx=xxx,upd_cnt=upd_cnt+1 where id=pk and upd_cnt+1<=5; -- 假设可以秒杀5台
这种方法的弊端:
获得锁的用户在处理这条记录时,可能成功,也可能失败,或者可能需要很长时间,(例如数据库响应慢)在它结束事务前,其他会话只能等着。
等待是非常不科学的,因为对于没有获得锁的用户,等待是在浪费时间。
常用的秒杀优化手段
1. 一般的优化处理方法是先使用for update nowait的方式来避免等待,即如果无法即可获得锁,那么就不等待。
begin;
select 1 from tbl where id=pk for update nowait; -- 如果用户无法即刻获得锁,则返回错误。从而这个事务回滚。
update tbl set xxx=xxx,upd_cnt=upd_cnt+1 where id=pk and upd_cnt+1<=5;
end;
这种方法可以减少用户的等待时间,因为无法即刻获得锁后就直接返回了。
第二种方案
秒杀场景的核心问题是在更新热点商品的库存后到commit
之间即使有1~2ms
延迟就大大降低了并发程度,所以将热点数据放在事务最后一条更新并进行自动提交事务可大大提高事务的吞吐量。
建表SQL:
create table item_order (
id bigint not null,
item_id bigint not null,
order_id bigint not null,
order_count int not null
);
create table item (
id bigint not null,
count int not null
);
产生一个订购如下:
item_id: 123
order_id: 456
order_count: 1
事务处理:
start transaction
insert into item_order (NEXT_ID, 123, 456, 1); // 插入一个名细,不阻赛事务,用于跨库事务和对账
update /*+ [auto_commit affect_rows 1] */ item set count=count-1 where count >= 1 and id = 123; // 减库存
// 可省略的 commit
思路2
另一种设计方法,不需要修改定制数据库,建表SQL:
create table item (
id bigint not null,
count int not null,
last_order_id bigint not null,
last_order_count int not null
);
产生的订单和上面的一样,这时候生成的事务处理如下:
// 注意这里不起事务,直接入库
update item set count = count - 1, order_id = 456, last_order_id = 456, last_order_count = 1 where count >= 1 and id = 123; // 减库存,同时写入名细
根据update
返回的记录数就可以判断减库存是否成功。剩下的问题是如何取得之前item_order
中的明细数据呢?
答案是抓取数据库的binlog
,达到之前item_order
类似的较果。