源码download:java秒杀系统 (resourcecode.cn)
主题:在大并发,大流量的情况下如何提升吞吐量或者说QPS?
而秒杀活动恰恰就是属于大并发的情形,因此下面简单来谈谈大并发下秒杀方案的优化。
项目采用技术:SpringBoot + MyBatis + MySql + RabbitMq + Redis
RabbitMq安装参考:rabbitmq安装_Garry1115的博客-CSDN博客
文章首先说明优化思路方案和步骤,然后阐述代码具体实现,最后采用JMeter进行简单压测。
1.页面缓存 + URL缓存 + 对象级缓存
2.页面静态化(浏览器缓存),前后端分离+ajax
3.静态资源优化(js/css压缩,减少流量),多个js/css组合,减少连接数
4.CDN优化
而作为一个程序猿我们都知道并发最大的瓶颈基本就是数据库,因此最好就是减少数据库的访问次数即加缓存。
浏览器端缓存(页面静态化)--> CDN --> nginx缓存 --> 后端服务缓存(页面缓存、对象级缓存等) --> 数据库
而作为一名后端工程师的优化则主要是针对接口优化,这也是我们的重头戏;
1.redis预减库存减少 数据库访问
2.内存标记减少redis访问
3.请求先入队缓冲,异步下单 ,增强用户体验
核心思路:减少数据库访问(数据库瓶颈)
1.系统初始化,把商品库存数量加载到redis
2.收到请求,redis预减库存,库存不足,直接返回,否则进入3
3.请求入队,立即返回 排队中
4.请求出队,生成订单,减少库存
5.客户端轮询,是否秒杀成功
1.秒杀接口地址隐藏
2.数学公式验证码
3.接口限流防刷
思路:秒杀开始之前,先去请求接口获取秒杀地址
1.接口改造 ,带上PathVariable参数
2.添加生成地址的接口
3.秒杀收到请求,先验证PathVariable
目的:防机器人,分散请求
思路:点击秒杀之前,先输入验证码,分散用户的请求
1.添加生成验证码接口
2.在获取秒杀路径的时候,验证验证码
1.利用redis缓存:比如限制用户1min中内只允许访问多少次
2.可以利用拦截器减少对业务代码的入侵
1.数据库加唯一索引:防止用户重复购买
2.更新库存sql增加库存数量判断:防止库存变成负数
(update table set count=count-1 where count>0)
a.判断用户登录信息是否异常
b.验证秒杀路径
c.获取秒杀商品内存标记并判断是否已秒杀完
d.redis获取用户订单,判断该用户是否是重复秒杀
e.如果是正常秒杀,对于未秒杀完的商品进行redis减库存操作
f.如果redis库存已<0,标记内存商品已秒杀完
g.秒杀正常则加入队列,异步处理订单入库,返回排队中
5.前端轮询订单结果(是否秒杀成功)
用户登录界面
描述商品列表页:
秒杀商品详情页(增加验证码):
增加访问限制(防刷限流):
重复秒杀处理:
换一个秒杀商品重新秒杀:
秒杀成功进入订单详情页:
数据库查看库存正常减1
项目结构如下:
秒杀接口部分核心代码如下:
获取验证码:
@AccessLimit(seconds = 5, maxCount = 5, needLogin = true)
@RequestMapping(value = "/path", method = RequestMethod.GET)
@ResponseBody
public Result getMiaoshaPath(HttpServletRequest request, MiaoshaUser user,
@RequestParam("goodsId") long goodsId,
@RequestParam(value = "verifyCode", defaultValue = "0") int verifyCode
) {
if (user == null) {
return Result.error(CodeMsg.SESSION_ERROR);
}
boolean check = miaoshaService.checkVerifyCode(user, goodsId, verifyCode);
if (!check) {
return Result.error(CodeMsg.REQUEST_ILLEGAL);
}
String path = miaoshaService.createMiaoshaPath(user, goodsId);
return Result.success(path);
}
获取秒杀路径:
@RequestMapping(value = "/verifyCode", method = RequestMethod.GET)
@ResponseBody
public Result getMiaoshaVerifyCod(HttpServletResponse response, MiaoshaUser user,
@RequestParam("goodsId") long goodsId) {
if (user == null) {
return Result.error(CodeMsg.SESSION_ERROR);
}
try {
BufferedImage image = miaoshaService.createVerifyCode(user, goodsId);
OutputStream out = response.getOutputStream();
ImageIO.write(image, "JPEG", out);
out.flush();
out.close();
return null;
} catch (Exception e) {
e.printStackTrace();
return Result.error(CodeMsg.MIAOSHA_FAIL);
}
}
执行秒杀:
@RequestMapping(value = "/{path}/do_miaosha", method = RequestMethod.POST)
@ResponseBody
public Result miaosha(Model model, MiaoshaUser user,
@RequestParam("goodsId") long goodsId,
@PathVariable("path") String path) {
model.addAttribute("user", user);
if (user == null) {
return Result.error(CodeMsg.SESSION_ERROR);
}
//验证path
boolean check = miaoshaService.checkPath(user, goodsId, path);
if (!check) {
return Result.error(CodeMsg.REQUEST_ILLEGAL);
}
//内存标记,减少redis访问
if (localOverMap.size() > 0) {
boolean over = localOverMap.get(goodsId);
if (over) {
return Result.error(CodeMsg.MIAO_SHA_OVER);
}
}
//判断是否已经秒杀到了
MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
if (order != null) {
return Result.error(CodeMsg.REPEATE_MIAOSHA);
}
//预减库存
long stock = redisService.decr(GoodsKey.getMiaoshaGoodsStock, "" + goodsId);//10
if (stock < 0) {
localOverMap.put(goodsId, true);
return Result.error(CodeMsg.MIAO_SHA_OVER);
}
//mq入队
MiaoshaMessage mm = new MiaoshaMessage();
mm.setUser(user);
mm.setGoodsId(goodsId);
sender.sendMiaoshaMessage(mm);
return Result.success(0);//排队中
JMeter简单使用参考:JMeter压测入门简单使用_Garry1115的博客-CSDN博客
操作系统:centos 虚拟机双核
测试参数:生成5000个用户(token信息),设置5000个线程数循环10次,即运行50000次,然后查看聚合报告中的吞吐量。
虚拟机测试结果吞吐量为2000左右,取决于机器配置
注:该接口是在未加入验证码和动态获取秒杀路径的前提下测试的
项目完整代码下载地址:java秒杀系统 (resourcecode.cn)