Java高并发秒杀系统(二)

4 秒杀压测-Jmeter压力测试

4.1 Jmeter快速入门

并发数为多少时,网站的QPS或TPS(每秒)是?

压测商品列表接口/goods/to_list

打开JMeter,测试计划,添加,Threads(Users),线程组

测试计划,添加,配置元件,HTTP请求默认值

线程组,添加,取样器,HTTP请求

线程组,添加,监听器,聚合报告

top命令监控服务器CPU

通过压测可以发现MySQL是最大的瓶颈

4.2 自定义变量模拟多用户

压测获取用户的接口/user/info

4.3 命令行压测

4.4 Redis压测工具redis-benchmark

5 页面级高并发秒杀优化(Redis缓存+静态化分离)

并发的瓶颈在数据库,加缓存减少对数据库的访问

Jsp和Thymeleaf是动态化页面,页面静态化,即采用纯HTML编写,用Js和Ajax去请求服务器

5.1 商品列表页页面缓存实现

推荐页面优化博文:

1. 从浏览器渲染原理谈页面优化

2. 浏览器是如何渲染页面的?

访问页面时先在缓存里取,没有浏览器再进行渲染。

设置页面缓存过期时间

public class GoodsKey extends BasePrefix{

	private GoodsKey(int expireSeconds, String prefix) {
		super(expireSeconds, prefix);
	}
	public static GoodsKey getGoodsList = new GoodsKey(60, "gl");
	public static GoodsKey getGoodsDetail = new GoodsKey(60, "gd");
	public static GoodsKey getSeckillGoodsStock= new GoodsKey(0, "gs");
}

数据60s内做出改变,页面不会及时显示。适用于页面不常更改的,如显示商品列表。

在线压缩html/css/js文件成一行,去掉文中的一些注释和空格以及空行,减小文件体积,节省带宽,提高响应速度。

5.1.1 取缓存

GoodsController.java

 //取缓存
String html = redisService.get(GoodsKey.getGoodsDetail, ""+goodsId, String.class);
if(!StringUtils.isEmpty(html)) {
     return html;
}

5.1.2 手动渲染模板

GoodsController.java

//手动渲染
html = thymeleafViewResolver.getTemplateEngine().process("goods_detail", ctx);
if(!StringUtils.isEmpty(html)) {
     //再写入缓存中
     redisService.set(GoodsKey.getGoodsDetail, ""+goodsId, html);
}
return html;

在Redis缓存中通过Gooskey查看是否能获取到页面数据。

5.2 热点数据对象缓存

对Java对象进行缓存

SeckillUserService.java

public SeckillUser getById(long id) {
     //取缓存
     SeckillUser user = redisService.get(SeckillUserKey.getById, ""+id, SeckillUser.class);
     if(user != null) {
          return user;
     }
     //取数据库
     user = SeckillUserDao.getById(id);
     if(user != null) {
          //写入缓存
          redisService.set(SeckillUserKey.getById, ""+id, user);
     }
     return user;
}
public boolean updatePassword(String token, long id, String formPass) {
      //取user
      SeckillUser user = getById(id);
      if(user == null) {
          throw new GlobalException(CodeMsg.MOBILE_NOT_EXIST);
      }
      //更新数据库,直接更新user,产生的SQL增多,访问数据库的次数增多,所以选择新new一个对象
      SeckillUser toBeUpdate = new SeckillUser();
      toBeUpdate.setId(id);//只更新要修改的字段
      toBeUpdate.setPassword(MD5Util.formPassToDBPass(formPass, user.getSalt()));
      SeckillUserDao.update(toBeUpdate);
      //处理缓存
      redisService.delete(SeckillUserKey.getById, ""+id);
      user.setPassword(toBeUpdate.getPassword());
      //更新token
      redisService.set(SeckillUserKey.token, token, user);
      return true;
}

注意,不能先删除缓存再更新数据库

试想,两个并发操作,一个是更新操作,另一个是查询操作,更新操作删除缓存后,查询操作没有命中缓存,先把老数据读出来后放到缓存中,然后更新操作更新了数据库。于是,在缓存中的数据还是老的数据,导致缓存中的数据是脏的,而且还一直这样脏下去了。

参考:高性能网站设计之缓存更新的套路_代码技巧的博客-CSDN博客

通过测压,查看优化后,各接口的QPS(TPS)。引用服务时,在service要引用别的service,而不是别的dao,因为service可能会涉及缓存。

5.3 商品详情静态化

将页面直接缓存到用户的浏览器上,节省网路流量。通过JavaScript或Ajax与后台交互,进行数据传输。

GoodController.java

public Result<GoodsDetailVo> detail(HttpServletRequest request,
HttpServletResponse response, Model model, 
SeckillUser user,@PathVariable("goodsId")long goodsId) {
     ...
     GoodsDetailVo vo = new GoodsDetailVo();
     vo.setGoods(goods);
     vo.setUser(user);
     vo.setRemainSeconds(remainSeconds);
     vo.setSeckillStatus(SeckillStatus);
     return Result.success(vo);
}

GoodsDetailVo.java

public class GoodsDetailVo {
	private int SeckillStatus = 0;
	private int remainSeconds = 0;
	private GoodsVo goods ;
	private SeckillUser user;
	...
}

goods_detail.htm

纯HTML渲染

<!DOCTYPE HTML>
<html >
<head>
    <title>商品详情</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <!-- jquery -->
    <script type="text/javascript" src="/js/jquery.min.js"></script>
    ...
    <style type="text/css">
       ...
    </style>
</head>
<body>

<div class="panel panel-default" style="height:100%;background-color:rgba(222,222,222,0.8)" >
  <div class="panel-heading">秒杀商品详情</div>
  <div class="panel-body">
  	<span id="userTip"> 您还没有登录,请登陆后再操作<br/></span>
  	<span>没有收货地址的提示。。。</span>
  </div>
  <table class="table" id="goodslist">
  	<tr>  
        <td>商品名称</td>  
        <td colspan="3" id="goodsName"></td> 
     </tr>  
     ...
  </table>
</div>
</body>

<script>
function getseckillPath(){
	var goodsId = $("#goodsId").val();
	g_showLoading();
	$.ajax({
		url:"/seckill/path",
		type:"GET",
		data:{
			goodsId:goodsId,
			verifyCode:$("#verifyCode").val()
		},
		...
	});
}
function getseckillResult(goodsId){
	g_showLoading();
	$.ajax({
		url:"/seckill/result",
		type:"GET",
		data:{
			goodsId:$("#goodsId").val(),
		},
		...
	});
}
...
function render(detail){
	var seckillStatus = detail.seckillStatus;
	var  remainSeconds = detail.remainSeconds;
	var goods = detail.goods;
	var user = detail.user;
	if(user){
		$("#userTip").hide();
	}
	$("#goodsName").text(goods.goodsName);
	$("#goodsImg").attr("src", goods.goodsImg);
	$("#startTime").text(new Date(goods.startDate).format("yyyy-MM-dd hh:mm:ss"));
	$("#remainSeconds").val(remainSeconds);
	$("#goodsId").val(goods.id);
	$("#goodsPrice").text(goods.goodsPrice);
	$("#seckillPrice").text(goods.seckillPrice);
	$("#stockCount").text(goods.stockCount);
	countDown();
}
...
</script>
</html>

5.4 解决卖超问题

一个用户只能秒杀一个商品,为了防止超卖,需要进行优化

优化1:优化SQL语句,防止库存变成负数

@Update("update seckill_goods set stock_count = stock_count - 1 
where goods_id = #{goodsId} and stock_count > 0")
    public int reduceStock(SeckillGoods g);

优化2:对用户id和商品id添加一个唯一的索引,防止用户发出多个请求重复购买

5.5 静态资源优化

页面缓存,对象缓存,浏览器缓存,各种层面部署缓存,缓解数据库压力。

服务器集群上部署CDN节点

网站站点加速

6  服务级高并发秒杀优化(RabbitMQ+接口优化)

6.1 接口优化的思路

思路:减少数据库访问

  • Redis预减库存减少数据库访问
  • 内存标记减少Redis访问
  • 请求先入队缓存,异步下单,增强用户体验

1 系统初始化,把商品库存数量加载到Redis

2 收到请求,Redis预减库存,库存不足,直接返回,否则进入3

3 请求入队,立即返回排队中(入队很快,即存数据于内存)

类似12306点击买票,然后显示正在排队中,有可能失败的

4 请求出队,生成订单,减少库存

5 客户端轮询,是否秒杀成功

SeckillController.java

//系统初始化
public void afterPropertiesSet() throws Exception {
     List<GoodsVo> goodsList = goodsService.listGoodsVo();
     if(goodsList == null) {
            return;
     }
     for(GoodsVo goods : goodsList) {
     //商品列表加载到redis缓存
     redisService.set(GoodsKey.getSeckillGoodsStock, ""+goods.getId(), goods.getStockCount());
            localOverMap.put(goods.getId(), false);
     }
}

@RequestMapping(value="/{path}/do_seckill", method=RequestMethod.POST)
@ResponseBody
public Result<Integer> seckill(Model model, SeckillUser 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 = seckillService.checkPath(user, goodsId, path);
     if(!check){
            return Result.error(CodeMsg.REQUEST_ILLEGAL);
     }
     //内存标记,减少redis访问,goodsId初始为false
     boolean over = localOverMap.get(goodsId);
     if(over) {
            //goodsId初始为true就不必再访问redis
            return Result.error(CodeMsg.MIAO_SHA_OVER);
     }
     //预减库存
     long stock = redisService.decr(GoodsKey.getSeckillGoodsStock, ""+goodsId);//10
     if(stock < 0) {
            //减完库存goodsId设为true
            localOverMap.put(goodsId, true);
            return Result.error(CodeMsg.MIAO_SHA_OVER);
     }
     //判断是否已经秒杀到了
     SeckillOrder order = orderService.getSeckillOrderByUserIdGoodsId(user.getId(), goodsId);
     if(order != null) {
            return Result.error(CodeMsg.REPEATE_Seckill);
     }
     //入队,哪个用户秒杀哪个商品
     SeckillMessage mm = new SeckillMessage();
     mm.setUser(user);
     mm.setGoodsId(goodsId);//入队后,receive会受到消息,才去数据库减库存
     sender.sendSeckillMessage(mm);
     return Result.success(0);//排队中
}
MQReceiver.java
@RabbitListener(queues=MQConfig.MIAOSHA_QUEUE)
public void receive(String message) {
	log.info("receive message:"+message);
	MiaoshaMessage mm  = RedisService.stringToBean(message, MiaoshaMessage.class);
	MiaoshaUser user = mm.getUser();
	long goodsId = mm.getGoodsId();
			
	GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
    int stock = goods.getStockCount();
	    if(stock <= 0) {
	    	return;
	    }
	//判断是否已经秒杀到了
	MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
	if(order != null) {
	    return;
	}
	//减库存 下订单 写入秒杀订单
	miaoshaService.miaosha(user, goods);
}

6.2 集成RabbitMQ

官方文档

6.2.1 安装erlang

yum install  ncurses-devel
tar xf otp_src_20.1.tar.gz
cd otp_src_20.1
./configure --prefix=/usr/local/erlang20 --without-javac --with-ssl
 make -j 4
 make install(或者make && install)
 erl验证 cd /usr/local/erlang20

6.2.3 安装python

yum install python -y

6.2.4 安装simplejson

yum install xmlto -y
yum install python-simplejson -y

6.2.2 安装RabbitMQ

下载源码
Generic Unix -> rabbitmq-server-generic-unix-3.6.14.tar.xz

xz -d rabbitmq-server-generic-unix-3.6.14.tar.xz
tar xf rabbitmq-server-generic-unix-3.6.14.tar
mv rabbitmq_server-3.6.14 /usr/local/rabbitmq

修改环境变量:/etc/profile:
export PATH=$PATH:/usr/local/ruby/bin:/usr/local/erlang20/bin:/usr/local/rabbitmq/sbin
source /etc/profile
./rabbitmq-server启动rabbitMQ server

netstat -nap|grep 5672 查看端口
rabbitmqctl stop 停止

踩坑了咋办

6.3 数据库分库分表

数据库分库分表的应用场景及解决方案

Mycat数据库分库分表中间件

(图片引用自: 学会数据库读写分离、分表分库——用Mycat,这一篇就够了! 

7 图形验证码及恶意防刷

8 服务端优化(Tomcat/Ngnix/LVS/Keepalived)

8.1 Tomcat

8.2 Nginx

参考:超大流量网站的负载均衡

Nginx介绍

8.3 LVS四层负载均衡

LVS是Linux Virtual Server的简写,意即Linux虚拟服务器,是一个虚拟的服务器集群系统,可以在UNIX/LINUX平台下实现负载均衡集群功能。

8.4 Keepalive负载均衡与高可用

检测死连接的机制。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值