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 商品列表页页面缓存实现
推荐页面优化博文:
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.xzxz -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 servernetstat -nap|grep 5672 查看端口
rabbitmqctl stop 停止
6.3 数据库分库分表
Mycat数据库分库分表中间件
(图片引用自: 学会数据库读写分离、分表分库——用Mycat,这一篇就够了! )
7 图形验证码及恶意防刷
8 服务端优化(Tomcat/Ngnix/LVS/Keepalived)
8.1 Tomcat
8.2 Nginx
8.3 LVS四层负载均衡
LVS是Linux Virtual Server的简写,意即Linux虚拟服务器,是一个虚拟的服务器集群系统,可以在UNIX/LINUX平台下实现负载均衡集群功能。
8.4 Keepalive负载均衡与高可用
检测死连接的机制。