这是我参与「第五届青训营」伴学笔记创作活动的第10天。
前言
今天青训营的课程是秒杀系统的设计,而且久违地看到了老师用Java,很亲切。关于秒杀系统,其实大概在一年多以前我在做谷粒商城的时候(这个应该很多人都做过),就已经接触了秒杀系统。因此,今天的笔记主要谈谈我对秒杀系统的想法。
设计要点
秒杀系统具有以下的特点:
- 高性能:秒杀涉及大量的并发读和并发写,因此支持高并发访问这点非常关键;
- 一致性:秒杀商品减库存的实现方式同样关键,有限数量的商品在同一时刻被很多倍的请求同时来减库存,在大并发更新的过程中都要保证数据的准确性;
- 高可用:秒杀时会在一瞬间涌入大量的流量,为了避免系统宕机,保证高可用,需要做好流量限制。
1.设计思路
关键点在于将原本的下单支付服务拆分成两个上下游服务进行操作。
比如现在有10件商品要秒杀,可以放到缓存中,读写时不要加锁。 当并发量大的时候,假如有25个人秒杀成功,这样后面的人就可以直接抛秒杀结束的静态页面。进去的25个人中有15个人是不可能获得商品的。所以可以根据进入的先后顺序只能前10个人购买成功。后面15个人就抛商品已秒杀完。
第一步 可以在每台web服务器的秒杀业务处理模块里做个计数器=待秒商品总数,计数器 >= 0的继续做后续处理,< 0的直接返回秒杀结束页面,这样经过第一步的处理只剩下进入10*服务器数量的请求数量。
第二步,缓存里以商品id作为key,value放个10,每个web服务器在接到每个请求的同时,向缓存服务器发起请求,利用缓存操作返回值, >=0的继续处理,其余的返回秒杀失败页面,这样经过第二步的处理只剩下服务器中最快到达的10个请求。
第三步,向服务器发起下单操作事务。
第四步,服务器向商品所在的数据库请求减库存操作(操作数据库时可以 "update table set count=count-1 where id=商品id and count>0;" update 成功记录数为1,再向订单数据库添加订单记录,都成功后提交整个事务,否则的话提示秒杀失败,用户进入支付流程。
@Override
public int createOrder(int sid) throws Exception {
// 校验库存
Stock stock = checkStock(sid);
// 扣库存(无锁)
saleStock(stock);
// 生成订单
int res = createOrder(stock);
return res;
}
private Stock checkStock(int sid) throws Exception {
Stock stock = stockService.getStockById(sid);
if (stock.getCount() < 1) {
throw new RuntimeException("库存不足");
}
return stock;
}
private int saleStock(Stock stock) {
stock.setSale(stock.getSale() + 1);
stock.setCount(stock.getCount() - 1);
return stockService.updateStockById(stock);
}
private int createOrder(Stock stock) throws Exception {
StockOrder order = new StockOrder();
order.setSid(stock.getId());
order.setName(stock.getName());
order.setCreateTime(new Date());
int res = orderMapper.insertSelective(order);
if (res == 0) {
throw new RuntimeException("创建订单失败");
}
return res;
}
// 扣库存
@Update("UPDATE stock SET count = #{count, jdbcType = INTEGER}, name = #{name, jdbcType = VARCHAR}, " + "sale = #{sale,jdbcType = INTEGER},version = #{version,jdbcType = INTEGER} " + "WHERE id = #{id, jdbcType = INTEGER}")
2.优化思路
2-1.前端
简单记录下前端的一些优化手段,具体地实现方式可以去搜索了解下:
- 限流:通过答题或者验证码,来分散用户的请求。
- 禁止重复提交:限定每个用户发起一次秒杀后,需等待才可以发起另一次请求,从而减少用户的重复请求。
- 本地标记:用户成功秒杀到商品后,就把提交按钮置为不可用或不可见状态,禁止用户再次提交请求。
- 动静分离:将前端静态数据直接缓存到离用户最近的地方,比如用户浏览器、CDN 或者服务端的缓存中来减少服务器的请求。
- 有损服务:最后一招,在接近前端池承载能力的水位上限的时候,随机拒绝部分请求来保护服务的可用性。
2-2.后端
后端上优化的手段很多,但从大方向上就是使用内存处理数据和排队处理请求,比如下面这样:
将存库从MySQL前移到Redis中,所有的写操作放到内存中,由于Redis中不存在锁故不会出现互相等待,并且由于Redis的写性能和读性能都远高于MySQL,这就解决了高并发下的性能问题。然后通过队列等异步手段,将变化的数据异步写入到DB中。
小结
秒杀系统的重点在于处理高并发下的业务请求以及数据一致性。因此,在设计秒杀系统时,我们要根据秒杀系统的特点去“对症下药”。