秒杀项目05-页面优化技术
1. 页面缓存+URL缓存+对象缓存
1.1 页面缓存
页面缓存就是将请求访问的页面放到redis里保存,这种缓存技术一般用于不会经常变动信息,并且访问次数较多的页面,这样就不用每次都动态加载
商品列表页 页面缓存: 1 取缓存 2 手动渲染 3 结果输出
- 修改GoodsController中的/goods/to_list/请求返回的内容,使其返回html页面的内容。
@RequestMapping(value = "/to_list", produces = "text/html")
@ResponseBody
public String toList(HttpServletRequest request, HttpServletResponse response, Model model, MiaoshaUser user) {
//取缓存
String html = redisService.get(GoodsKey.getGoodsList,"", String.class);
if (!StringUtils.isEmpty(html)) {
return html;
}
//查询商品列表
List<GoodsVo> goodsList = goodsService.listGoodsVo();
model.addAttribute("goodsList", goodsList);
//return "goods_list";
WebContext ctx = new WebContext(request, response, request.getServletContext(), request.getLocale(), model.asMap());
//手动渲染
html = thymeleafViewResolver.getTemplateEngine().process("goods_list", ctx);
if (StringUtils.isNotBlank(html)) {
redisService.set(GoodsKey.getGoodsList, "", html);
}
return html;
}
- 编写GoodsKey(存储redis中的key和expireTime)
public class GoodsKey extends BasePrefix{
private GoodsKey(int expireSeconds, String prefix) {
super(expireSeconds, prefix);
}
public static GoodsKey getGoodsList = new GoodsKey(60, "gl");
}
1.2 URL缓存
这里的URL缓存相当于页面缓存,只是针对详情页/goods/to_detail/{goodsId}
不同的详情页 显示不同缓存页面+渲染 实质一样
1.3 对象缓存(更细粒度的缓存)
对象缓存就是将对象放到缓存中
MiaoshaUserservice.java
/**
* 将从数据库中获取对象,优化为从缓存中获取对象数据
* 先从缓存中取用户数据,取不到,再从数据库中取,并且将数据放到缓存一份
* @param id
* @return
*/
public MiaoshaUser getById(long id) {
//取缓存
MiaoshaUser user = redisService.get(MiaoshaUserKey.getById, "" + id, MiaoshaUser.class);
if (user != null) {
return user;
}
//取数据库
user = miaoshaUserMapper.selectById(id);
if (user != null) {
redisService.set(MiaoshaUserKey.getById, "" + id, user);
}
return user;
}
/**
* 更新用户密码: 涉及到对象缓存 -- 若更新对象缓存的相关数据 要处理缓存
* 同步数据库和redis缓存的信息,否则会造成数据不一致的情况
*/
public boolean updatePassword(String token, long id, String formPass) {
//取user
MiaoshaUser user = getById(id);
if (user == null) {
throw new GlobalException(CodeMsg.MOBILE_NOT_EXIST);
}
//更新数据库
MiaoshaUser toBeUpdate = new MiaoshaUser();
toBeUpdate.setId(id);
toBeUpdate.setPassword(MD5Util.formPassToDBPass(formPass, user.getSalt()));
miaoshaUserMapper.update(toBeUpdate);
//处理缓存,防止数据库中与redis缓存中的数据不一致,所有涉及到该对象的缓存都要处理
redisService.delete(MiaoshaUserKey.getById, "" + id);
user.setPassword(toBeUpdate.getPassword());
redisService.set(MiaoshaUserKey.token, token, user);
return true;
}
2. 页面静态化,前后端分离
1. 常用技术AngularJS、Vue.js
2. 优点: 利用浏览器的缓存
3. 商品详情页面静态化(将页面与动态的内容分离)
修改Controller层的方法
GoodsController.java
@RequestMapping(value = "/detail/{goodsId}")
@ResponseBody
public Result<GoodsDetailVo> detail(Model model, MiaoshaUser user, @PathVariable("goodsId") long goodsId) {
GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
long startAt = goods.getStartDate().getTime();
long endAt = goods.getEndDate().getTime();
long now = System.currentTimeMillis();
int miaoshaStatus = 0;
int remainSeconds = 0;
if (now < startAt) {
// 秒杀还没开始 倒计时
miaoshaStatus = 0;
remainSeconds = (int)(startAt - now) / 1000;
} else if(now > endAt) {
//秒杀已经结束
miaoshaStatus = 2;
remainSeconds = -1;
} else {
//秒杀进行中
miaoshaStatus = 1;
remainSeconds = 0;
}
GoodsDetailVo vo = new GoodsDetailVo();
vo.setGoods(goods);
vo.setUser(user);
vo.setRemainSeconds(remainSeconds);
vo.setMiaoshaStatus(miaoshaStatus);
return Result.success(vo);
}
添加Vo对象
GoodsDetailVo.java
@Data
public class GoodsDetailVo {
private int miaoshaStatus = 0;
private int remainSeconds = 0;
private GoodsVo goods;
private MiaoshaUser user;
}
修改common.js添加时间格式化函数和获取url参数的函数
//展示loading
function g_showLoading(){
var idx = layer.msg('处理中...', {icon: 16,shade: [0.5, '#f5f5f5'],scrollbar: false,offset: '0px', time:100000}) ;
return idx;
}
//salt
var g_passsword_salt="hmxP@ssw0rd"
// 获取url参数
function g_getQueryString(name) {
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
var r = window.location.search.substr(1).match(reg);
if(r != null) return unescape(r[2]);
return null;
};
//设定时间格式化函数,使用new Date().format("yyyyMMddhhmmss");
Date.prototype.format = function (format) {
var args = {
"M+": this.getMonth() + 1,
"d+": this.getDate(),
"h+": this.getHours(),
"m+": this.getMinutes(),
"s+": this.getSeconds(),
};
if (/(y+)/.test(format))
format = format.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
for (var i in args) {
var n = args[i];
if (new RegExp("(" + i + ")").test(format))
format = format.replace(RegExp.$1, RegExp.$1.length === 1 ? n : ("00" + n).substr(("" + n).length));
}
return format;
};
将商品详情页面放到/resources/static目录下
并修改其内容,使其不再依赖Thymeleaf模板引擎
goods_detail.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>
<!-- bootstrap -->
<link rel="stylesheet" type="text/css" href="/bootstrap/css/bootstrap.min.css" />
<script type="text/javascript" src="/bootstrap/js/bootstrap.min.js}"></script>
<!-- jquery-validator -->
<script type="text/javascript" src="/jquery-validation/jquery.validate.min.js"></script>
<script type="text/javascript" src="/jquery-validation/localization/messages_zh.min.js"></script>
<!-- layer -->
<script type="text/javascript" src="/layer/layer.js"></script>
<!-- md5.js -->
<script type="text/javascript" src="/js/md5.min.js"></script>
<!-- common.js -->
<script type="text/javascript" src="/js/common.js"></script>
</head>
<body>
<div class="panel panel-default">
<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>
<tr>
<td>商品图片</td>
<td colspan="3"><img id="goodsImg" width="200" height="200" /></td>
</tr>
<tr>
<td>秒杀开始时间</td>
<td id="startTime"></td>
<td>
<input type="hidden" id="remainSeconds" />
<span id="miaoshaTip"></span>
</td>
<td>
<form id="miaoshaForm" method="post" action="/miaosha/do_miaosha">
<button class="btn btn-primary btn-block" type="submit" id="buyButton">立即秒杀</button>
<input type="hidden" name="goodsId" id="goodsId"/>
</form>
</td>
</tr>
<tr>
<td>商品原价</td>
<td colspan="3" id="goodsPrice"></td>
</tr>
<tr>
<td>秒杀价</td>
<td colspan="3" id="miaoshaPrice"></td>
</tr>
<tr>
<td>库存数量</td>
<td colspan="3" id="stockCount"></td>
</tr>
</table>
</div>
</body>
<script>
$(function(){
//countDown();
getDetail();
});
function getDetail() {
var goodsId = g_getQueryString("goodsId");
$.ajax({
url: "/goods/detail/" + goodsId,
type: "GET",
success:function(data){
if (data.code === 0) {
render(data.data);
} else {
layer.msg(data.msg);
}
},
error:function() {
layer.msg("客户端请求有误");
}
})
}
function render(detail) {
var miaoshaStatus = detail.miaoshaStatus;
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);
$("#miaoshaPrice").text(goods.miaoshaPrice);
$("#stockCount").text(goods.stockCount);
countDown();
}
function countDown(){
//debugger;
var remainSeconds = $("#remainSeconds").val();
var timeout;
//debugger;
if(remainSeconds > 0){//秒杀还没开始,倒计时
$("#buyButton").attr("disabled", true);
$("#miaoshaTip").html("秒杀倒计时: " + remainSeconds + "秒")
timeout = setTimeout(function(){
$("#countDown").text(remainSeconds - 1);
$("#remainSeconds").val(remainSeconds - 1);
countDown();
},1000);
}else if(remainSeconds == 0){//秒杀进行中
$("#buyButton").attr("disabled", false);
if(timeout){
clearTimeout(timeout);
}
$("#miaoshaTip").html("秒杀进行中");
}else{//秒杀已经结束
$("#buyButton").attr("disabled", true);
$("#miaoshaTip").html("秒杀已经结束");
}
}
</script>
</html>
修改goods_list页面的部分内容
4. 订单详情页面静态化
新增CodeMsg
//订单模块 5004XX
ORDER_NOT_EXIST(500400, "订单不存在"),
修改Controller层
MiaoshaController.java
@RequestMapping("/miaosha")
@Controller
public class MiaoshaController {
@Autowired
private GoodsService goodsService;
@Autowired
private OrderService orderService;
@Autowired
private MiaoshaService miaoshaService;
/**
* QPS:909.3
* 异常%: 4.38%
* 5000 * 10
*/
/**
* GET POST有什么区别?
* GET幂等 <a href="/delete?id=1212">
*/
@RequestMapping(value = "/do_miaosha", method= RequestMethod.POST)
@ResponseBody
public Result<OrderInfo> doMiaosha(Model model, MiaoshaUser user, @RequestParam("goodsId")long goodsId) {
model.addAttribute("user", user);
if (user == null) {
return Result.fail(CodeMsg.SESSION_ERROR);
}
// 判断库存
GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
int stock = goods.getStockCount();
if (stock <= 0) {
return Result.fail(CodeMsg.MIAO_SHA_OVER);
}
// 判断是否已经秒杀到了
MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
if (order != null) {
return Result.fail(CodeMsg.REPEAT_MIAOSHA);
}
// 减库存 下订单 写入秒杀订单
OrderInfo orderInfo = miaoshaService.miaosha(user, goods);
return Result.success(orderInfo);
}
}
添加vo对象
OrderDetailVo.java
@Data
public class OrderDetailVo {
private GoodsVo goods;
private OrderInfo order;
}
将订单详情页面放到/resources/static目录下
order_detail.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>
<!-- bootstrap -->
<link rel="stylesheet" type="text/css" href="/bootstrap/css/bootstrap.min.css" />
<script type="text/javascript" src="/bootstrap/js/bootstrap.min.js"></script>
<!-- jquery-validator -->
<script type="text/javascript" src="/jquery-validation/jquery.validate.min.js"></script>
<script type="text/javascript" src="/jquery-validation/localization/messages_zh.min.js"></script>
<!-- layer -->
<script type="text/javascript" src="/layer/layer.js"></script>
<!-- md5.js -->
<script type="text/javascript" src="/js/md5.min.js"></script>
<!-- common.js -->
<script type="text/javascript" src="/js/common.js"></script>
</head>
<body>
<div class="panel panel-default">
<div class="panel-heading">秒杀订单详情</div>
<table class="table" id="goodslist">
<tr>
<td>商品名称</td>
<td id="goodsName" colspan="3"></td>
</tr>
<tr>
<td>商品图片</td>
<td colspan="2"><img id="goodsImg" wid="200" height="200" /></td>
</tr>
<tr>
<td>订单价格</td>
<td colspan="2" id="goodsPrice"></td>
</tr>
<tr>
<td>下单时间</td>
<td id="createDate" colspan="2"></td>
</tr>
<tr>
<td>订单状态</td>
<td id="orderStatus">
</td>
<td>
<button class="btn btn-primary btn-block" type="submit" id="payButton">立即支付</button>
</td>
</tr>
<tr>
<td>收货人</td>
<td colspan="2">XXX 18812341234</td>
</tr>
<tr>
<td>收货地址</td>
<td colspan="2">北京市昌平区回龙观龙博一区</td>
</tr>
</table>
</div>
</body>
</html>
<script>
$(function(){
getOrderDetail();
});
function getOrderDetail() {
var orderId = g_getQueryString("orderId")
$.ajax({
url: "/order/detail/",
type: "GET",
data: {
orderId:orderId
},
success:function(data){
debugger;
if (data.code === 0) {
render(data.data);
} else {
layer.msg(data.msg);
}
},
error:function() {
layer.msg("客户端请求有误");
}
})
}
function render(detail) {
var goods = detail.goods;
var order = detail.order;
$("#goodsName").text(goods.goodsName);
$("#goodsImg").attr("src", goods.goodsImg);
$("#orderPrice").text(order.goodsPrice);
$("#createDate").text(new Date(order.createDate).format("yyyy-MM-dd hh:mm:ss"));
var status = "";
if(order.status === 0){
status = "未支付"
}else if(order.status === 1){
status = "待发货";
}
$("#orderStatus").text(status);
}
</script>
修改商品详情页的部分内容
good_detail.html
//秒杀
function doMiaosha() {
$.ajax({
url: "/miaosha/do_miaosha",
type: "POST",
data: {
goodsId:$("#goodsId").val(),
},
success:function(data){
if (data.code === 0) {
window.location.href="/order_detail.html?orderId="+data.data.id;
} else {
layer.msg(data.msg);
}
},
error:function() {
layer.msg("客户端请求有误");
}
})
}
添加静态资源配置
# static
# SPRING RESOURCES HANDLING (ResourceProperties)
# 是否启用默认资源处理
spring.web.resources.add-mappings=true
# 资源处理程序服务的资源的缓存周期。如果未指定持续时间后缀,则将使用秒。可以被 'spring.web.resources.cache.cachecontrol' 属性覆盖。
spring.web.resources.cache.period=3600
# 是否启用资源链中的缓存。
spring.web.resources.chain.cache=true
# 是否启用 Spring 资源处理链。默认情况下,除非至少启用了一种策略,否则禁用。
spring.web.resources.chain.enabled=true
# 是否启用已压缩资源的解析(gzip、brotli)。检查具有“.gz”或“.br”文件扩展名的资源名称。
spring.web.resources.chain.compressed=false
# 静态资源的位置。默认为类路径:classpath:/static/
spring.web.resources.static-locations=classpath:/static/
浏览器缓存
解决一个bug
修改OrderService中的创建订单方法
一些问题
解决超卖问题
当高并发访问秒杀请求时,会出现超卖问题,我们可以通过在数据库减库存操作时,在SQL语句上加上stockCount>0的判断
一个人秒杀多次的问题
- 先给miaosha_order表的userid和goodsid添加唯一索引
当进入秒杀业务的人数>库存数时,要判断当前用户是否减库存成功,成功则生成订单,失败抛出异常
MiaoshaService.java
@Transactional
public OrderInfo miaosha(MiaoshaUser user, GoodsVo goods) {
// 减库存 下订单 写入秒杀订单
int row = goodsService.reduceStock(goods);
if (row > 0) {
// order_info miaosha_order
return orderService.createOrder(user, goods);
}
//row == 0 说明减库存失败,订单也不应该创建,报错回滚
throw new GlobalException(CodeMsg.MIAO_SHA_OVER);
}
优化
将miaoshaoOrder信息作为对象缓存缓存到redis中
添加OrderKey
OrderKey.java
public class OrderKey extends BasePrefix {
public OrderKey(String prefix) {
super(prefix);
}
public static OrderKey getMiaoshaOrderByUidGid = new OrderKey("moug");
}
修改OrderService
OrderService.java
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private RedisService redisService;
public MiaoshaOrder getMiaoshaOrderByUserIdGoodsId(long userId, long goodsId) {
//return orderMapper.getMiaoshaOrderByUserIdGoodsId(userId, goodsId);
return redisService.get(OrderKey.getMiaoshaOrderByUidGid, ""+userId+"_"+goodsId, MiaoshaOrder.class);
}
@Transactional
public OrderInfo createOrder(MiaoshaUser user, GoodsVo goods) {
OrderInfo orderInfo = new OrderInfo();
orderInfo.setCreateDate(new Date());
orderInfo.setDeliveryAddrId(0L);
orderInfo.setGoodsCount(1);
orderInfo.setGoodsId(goods.getId());
orderInfo.setGoodsName(goods.getGoodsName());
orderInfo.setGoodsPrice(goods.getMiaoshaPrice());
orderInfo.setOrderChannel(1);
orderInfo.setStatus(1);
orderInfo.setStatus(0);
orderInfo.setUserId(user.getId());
//insert方法会把自增的主键设置到对象属性中
orderMapper.insert(orderInfo);
MiaoshaOrder miaoshaOrder = new MiaoshaOrder();
miaoshaOrder.setGoodsId(goods.getId());
miaoshaOrder.setOrderId(orderInfo.getId());
miaoshaOrder.setUserId(user.getId());
orderMapper.insertMiaoshaOrder(miaoshaOrder);
redisService.set(OrderKey.getMiaoshaOrderByUidGid, ""+user.getId()+"_"+goods.getId(), miaoshaOrder);
return orderInfo;
}
public OrderInfo getOrderById(long orderId) {
return orderMapper.selectById(orderId);
}
}
重新压测
先启动MyUtil类的主方法,让5000个用户的token被保存在redis中.
将jar包放到centos中并运行,然后开始压测,并查看生成的订单信息。![在这里插入图片描述](https://img-blog.csdnimg.cn/f34870862629448397edaa890e7968d6.png)