点击上方 Java后端,选择 设为星标
优质文章,及时送达
1 秒杀业务分析
正常电子商务流程
-
查询商品;
-
创建订单;
-
扣减库存;
-
更新订单;
-
付款;
-
卖家发货;
秒杀业务的特性
-
低廉价格;
-
大幅推广;
-
瞬时售空;
-
一般是定时上架;
-
时间短、瞬时并发量高;
2 秒杀技术挑战
假设某网站秒杀活动只推出一件商品,预计会吸引1万人参加活动,也就说最大并发请求数是10000,秒杀系统需要面对的技术挑战有:
1、对现有网站业务造成冲击
秒杀活动只是网站营销的一个附加活动,这个活动具有时间短,并发访问量大的特点,如果和网站原有应用部署在一起,必然会对现有业务造成冲击,稍有不慎可能导致整个网站瘫痪。
解决方案:将秒杀系统独立部署,甚至使用独立域名,使其与网站完全隔离。
2、高并发下的应用、数据库负载
用户在秒杀开始前,通过不停刷新浏览器页面以保证不会错过秒杀,这些请求如果按照一般的网站应用架构,访问应用服务器、连接数据库,会对应用服务器和数据库服务器造成负载压力。
解决方案:重新设计秒杀商品页面,不使用网站原来的商品详细页面,页面内容静态化,用户请求不需要经过应用服务。
3、突然增加的网络及服务器带宽
假设商品页面大小200K(主要是商品图片大小),那么需要的网络和服务器带宽是2G(200K×10000),这些网络带宽是因为秒杀活动新增的,超过网站平时使用的带宽。
解决方案:因为秒杀新增的网络带宽,必须和运营商重新购买或者租借。为了减轻网站服务器的压力,需要将秒杀商品页面缓存在CDN,同样需要和CDN服务商临时租借新增的出口带宽。
4、直接下单
秒杀的游戏规则是到了秒杀才能开始对商品下单购买,在此时间点之前,只能浏览商品信息,不能下单。而下单页面也是一个普通的URL,如果得到这个URL,不用等到秒杀开始就可以下单了。
解决方案:为了避免用户直接访问下单页面URL,需要将改URL动态化,即使秒杀系统的开发者也无法在秒杀开始前访问下单页面的URL。办法是在下单页面URL加入由服务器端生成的随机数作为参数,在秒杀开始的时候才能得到。
5.如何控制秒杀商品页面购买按钮的点亮
购买按钮只有在秒杀开始的时候才能点亮,在此之前是灰色的。如果该页面是动态生成的,当然可以在服务器端构造响应页面输出,控制该按钮是灰色还是点亮,但是为了减轻服务器端负载压力,更好地利用CDN、反向代理等性能优化手段,该页面被设计为静态页面,缓存在CDN、反向代理服务器上,甚至用户浏览器上。秒杀开始时,用户刷新页面,请求根本不会到达应用服务器。
解决方案:使用JavaScript脚本控制,在秒杀商品静态页面中加入一个JavaScript文件引用,该JavaScript文件中包含秒杀开始标志为否;当秒杀开始的时候生成一个新的JavaScript文件(文件名保持不变,只是内容不一样),更新秒杀开始标志为是,加入下单页面的URL及随机数参数(这个随机数只会产生一个,即所有人看到的URL都是同一个,服务器端可以用redis这种分布式缓存服务器来保存随机数),并被用户浏览器加载,控制秒杀商品页面的展示。这个JavaScript文件的加载可以加上随机版本号(例如xx.js?v=32353823),这样就不会被浏览器、CDN和反向代理服务器缓存。
这个JavaScript文件非常小,即使每次浏览器刷新都访问JavaScript文件服务器也不会对服务器集群和网络带宽造成太大压力。
6.如何只允许第一个提交的订单被发送到订单子系统
由于最终能够成功秒杀到商品的用户只有一个,因此需要在用户提交订单时,检查是否已经有订单提交。如果已经有订单提交成功,则需要更新 JavaScript文件,更新秒杀开始标志为否,购买按钮变灰。事实上,由于最终能够成功提交订单的用户只有一个,为了减轻下单页面服务器的负载压力,可以控制进入下单页面的入口,只有少数用户能进入下单页面,其他用户直接进入秒杀结束页面。
解决方案:假设下单服务器集群有10台服务器,每台服务器只接受最多10个下单请求。在还没有人提交订单成功之前,如果一台服务器已经有十单了,而有的一单都没处理,可能出现的用户体验不佳的场景是用户第一次点击购买按钮进入已结束页面,再刷新一下页面,有可能被一单都没有处理的服务器处理,进入了填写订单的页面,可以考虑通过cookie的方式来应对,符合一致性原则。当然可以采用最少连接的负载均衡算法,出现上述情况的概率大大降低。
7.如何进行下单前置检查
下单服务器检查本机已处理的下单请求数目:
-
如果超过10条,直接返回已结束页面给用户;
-
如果未超过10条,则用户可进入填写订单及确认页面;
-
已超过秒杀商品总数,返回已结束页面给用户;
-
未超过秒杀商品总数,提交到子订单系统;
8.秒杀一般是定时上架
9.减库存的操作
10.库存会带来“超卖”的问题:售出数量多于库存数量
update auction_auctions set
quantity = #inQuantity#
where auction_id = #itemId# and quantity = #dbQuantity#
还有一种方式,会更好些,叫做尝试扣减库存,扣减库存成功才会进行下单逻辑:
update auction_auctions set
quantity = quantity-#count#
where auction_id = #itemId# and quantity >= #count#
11、秒杀器的应对
3 秒杀架构原则
尽量将请求拦截在系统上游
读多写少的常用多使用缓存
4 秒杀架构设计
-
第一个阶段是秒杀开始前某个时间到秒杀开始, 这个阶段可以称之为准备阶段,用户在准备阶段等待秒杀;
-
第二个阶段就是秒杀开始到所有参与秒杀的用户获得秒杀结果, 这个就称为秒杀阶段吧。
4.1 前端层设计
第一个是秒杀页面的展示
第二个是倒计时
浏览器层请求拦截
4.2 站点层设计
-
同一个uid,限制访问频度,做页面缓存,x秒内到达站点层的请求,均返回同一页面
-
同一个item的查询,例如手机车次,做页面缓存,x秒内到达站点层的请求,均返回同一页面
4.3 服务层设计
-
大哥,我是服务层,我清楚的知道小米只有1万部手机,我清楚的知道一列火车只有2000张车票,我透10w个请求去数据库有什么意义呢?对于写请求,做请求队列,每次只透过有限的写请求去数据层,如果均成功再放下一批,如果库存不够则队列里的写请求全部返回“已售完”;
-
对于读请求,还用说么?cache来抗,不管是memcached还是redis,单机抗个每秒10w应该都是没什么问题的;
-
用户请求分发模块:使用Nginx或Apache将用户的请求分发到不同的机器上。
-
用户请求预处理模块:判断商品是不是还有剩余来决定是不是要处理该请求。
-
用户请求处理模块:把通过预处理的请求封装成事务提交给数据库,并返回是否成功。
-
数据库接口模块:该模块是数据库的唯一接口,负责与数据库交互,提供RPC接口供查询是否秒杀结束、剩余数量等信息。
1.用户请求预处理模块
package seckill;
import org.apache.http.HttpRequest;
/**
* 预处理阶段,把不必要的请求直接驳回,必要的请求添加到队列中进入下一阶段.
*/
public class PreProcessor {
// 商品是否还有剩余
private static boolean reminds = true;
private static void forbidden() {
// Do something.
}
public static boolean checkReminds() {
if (reminds) {
// 远程检测是否还有剩余,该RPC接口应由数据库服务器提供,不必完全严格检查.
if (!RPC.checkReminds()) {
reminds = false;
}
}
return reminds;
}
/**
* 每一个HTTP请求都要经过该预处理.
*/
public static void preProcess(HttpRequest request) {
if (checkReminds()) {
// 一个并发的队列
RequestQueue.queue.add(request);
} else {
// 如果已经没有商品了,则直接驳回请求即可.
forbidden();
}
}
}
-
ArrayBlockingQueue是初始容量固定的阻塞队列,我们可以用来作为数据库模块成功竞拍的队列,比如有10个商品,那么我们就设定一个10大小的数组队列。
-
ConcurrentLinkedQueue使用的是CAS原语无锁队列实现,是一个异步队列,入队的速度很快,出队进行了加锁,性能稍慢。
-
LinkedBlockingQueue也是阻塞的队列,入队和出队都用了加锁,当队空的时候线程会暂时阻塞。
package seckill;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentLinkedQueue;
import org.apache.http.HttpRequest;
public class RequestQueue {
public static ConcurrentLinkedQueue<HttpRequest> queue = new ConcurrentLinkedQueue<HttpRequest>();
}
2.用户请求模块
package seckill;
import org.apache.http.HttpRequest;
public class Processor {
/**
* 发送秒杀事务到数据库队列.
*/
public static void kill(BidInfo info) {
DB.bids.add(info);
}
public static void process() {
BidInfo info = new BidInfo(RequestQueue.queue.poll());
if (info != null) {
kill(info);
}
}
}
class BidInfo {
BidInfo(HttpRequest request) {
// Do something.
}
}
3.数据库模块
数据库主要是使用一个ArrayBlockingQueue来暂存有可能成功的用户请求。
package seckill;
import java.util.concurrent.ArrayBlockingQueue;
/**
* DB应该是数据库的唯一接口.
*/
public class DB {
public static int count = 10;
public static ArrayBlockingQueue<BidInfo> bids = new ArrayBlockingQueue<BidInfo>(10);
public static boolean checkReminds() {
// TODO
return true;
}
// 单线程操作
public static void bid() {
BidInfo info = bids.poll();
while (count-- > 0) {
// insert into table Bids values(item_id, user_id, bid_date, other)
// select count(id) from Bids where item_id = ?
// 如果数据库商品数量大约总数,则标志秒杀已完成,设置标志位reminds = false.
info = bids.poll();
}
}
}
4.4 数据库设计
4.4.1 基本概念
-
优点:简单,容易扩展
-
缺点:各库压力不均(新号段更活跃)
-
优点:简单,数据均衡,负载均匀
-
缺点:迁移麻烦(2库扩3库数据要迁移)
-
优点:灵活性强,业务与路由算法解耦
-
缺点:每次访问数据库前多一次查询
4.4.2 设计思路
-
如何保证数据可用性;
-
如何提高数据库读性能(大部分应用读多写少,读会先成为瓶颈);
-
如何保证一致性;
-
如何提高扩展性;
1.如何保证数据的可用性?
-
如何保证站点的可用性?复制站点,冗余站点
-
如何保证服务的可用性?复制服务,冗余服务
-
如何保证数据的可用性?复制数据,冗余数据
2.如何保证数据库“读”高可用?
3.如何保证数据库“写”高可用?
-
两个写库使用不同的初始值,相同的步长来增加id:1写库的id为0,2,4,6...;2写库的id为1,3,5,7...;
-
不使用数据的id,业务层自己生成唯一的id,保证数据不冲突;
-
读写没有延时;
-
读写高可用;
-
不能通过加从库的方式扩展读性能;
-
资源利用率为50%,一台冗余主没有提供服务;
4.如何扩展读性能
-
写库不建立索引;
-
线上读库建立线上访问索引,例如uid;
-
线下读库建立线下访问索引,例如time;
-
从库越多,同步越慢;
-
同步越慢,数据不一致窗口越大(不一致后面说,还是先说读性能的提高);
5.如何保证一致性?
-
淘汰cache;
-
写数据库;
-
读cache,如果cache hit则返回;
-
如果cache miss,则读从库;
-
读从库后,将数据放回cache;
-
淘汰cache;
-
写数据库;
-
在经过“主从同步延时窗口时间”后,再次发起一个异步淘汰cache的请求;
如何提高数据库的扩展性?
-
第一步,将一个主库提升;
-
第二步,修改配置,2库变4库(原来MOD2,现在配置修改后MOD4),扩容完成;
-
将旧的双主同步解除;
-
增加新的双主(双主是保证可用性的,shadow-master平时不提供服务);
-
删除多余的数据(余0的主,可以将余2的数据删除掉);
5 大并发带来的挑战#
5.1 请求接口的合理设计
5.2 高并发的挑战:一定要“快”
20*500/0.1 = 100000 (10万QPS)
20*500/0.25 = 40000 (4万QPS)
20*500
个可用连接进程都在满负荷工作中,却仍然有1万个新来请求,没有连接进程可用,系统陷入到异常状态也是预期之内。
5.3 重启与过载保护##
6 作弊的手段:进攻与防守
6.1 同一个账号,一次性发出多个请求
6.2 多个账号,一次性发送多个请求
6.3 多个账号,不同IP发送不同请求
7 高并发下的数据安全
7.1 超发的原因
7.2 悲观锁思路##
7.3 FIFO队列思路
7.4 乐观锁思路
8 总结
作者:猿码架构
https://urlify.cn/veeyQv
最近整理一份面试资料《Java技术栈学习手册》,覆盖了Java技术、面试题精选、Spring全家桶、Nginx、SSM、微服务、数据库、数据结构、架构等等。
获取方式:点“ 在看,关注公众号 Java后端 并回复 777 领取,更多内容陆续奉上。
推
荐
阅
读
1. 介绍一款 API 敏捷开发工具
2. 这 4 种 ThreadLocal 你都知道吗?
3. Windows 给力!可以扔掉 Linux 虚拟机了!
4. 25 张图搞懂「文件系统」
5. 干掉烦人的 Null !
喜欢文章,点个
在看
本文分享自微信公众号 - Java后端(web_resource)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。