一、商品列表页面缓存实现
1.页面缓存
要展示一个页面时,先从缓冲中取;如果取不到,再进行渲染,然后将渲染后的结果写入缓存。
对商品列表页进行优化,修改toList()
这个Controller。之前是直接返回html的文件名,让SpringBoot负责渲染。现在将代码改为:
@RequestMapping(value = "/to_list",produces = "text/html")
@ResponseBody
public String toList(Model model, MiaoshaUser user,
HttpServletRequest request,HttpServletResponse response){
model.addAttribute("user",user);
List<GoodsVo> goodsVos = goodsService.listGoodsVo();
model.addAttribute("goodsList",goodsVos);
//先从缓存中取
ValueOperations<String, String> opsForValue = redisTemplate.opsForValue();
String html = opsForValue.get(GoodsKey.getGoodsList.getPrefix());
if(!StringUtils.isEmpty(html)){
return html;
}else {
//取不到手动渲染,使用ThymeleafViewResolver
WebContext webContext = new WebContext(request, response, request.getServletContext(), request.getLocale(),
model.asMap());
html = thymeleafViewResolver.getTemplateEngine().process("goods_list", webContext);
if(!StringUtils.isEmpty(html)){
//写入缓存
opsForValue.set(GoodsKey.getGoodsList.getPrefix(),html,GoodsKey.getGoodsList.expireSeconds(), TimeUnit.SECONDS);
}
return html;
}
}
页面缓存的有效期通常是比较短的,只是为了防止短时间内大量访问。这里设置为60s。
看一看这里的页面缓存能带来多大的性能提升:
这是加入缓存之前的压测数据:
这是加入缓存之后的压测数据(5000个用户,循环访问10次):
怎么QPS还变低了
重新测试,QPS为3000,上涨50%。
2.URL缓存
与页面缓存相似,只不过是带有参数的。用来优化一下详情页面。
3.对象缓存
对象缓存是更细粒度的缓存。页面缓存适合不会经常变动信息,并且访问次数较多的页面。
其实之前就用到了对象缓存,比如在redis中存储token和用户信息,这就属于对象缓存。
(1)优化getById()方法
这是MiaoshaService中getById()的原方法:
public MiaoshaUser getById(long id) {
return miaoshaUserDao.getById(id);
}
非常的简单,只是从数据库中根据Id查询用户。现在我们加入对象缓存。
public MiaoshaUser getById(long id) {
//取缓存
ValueOperations<String, MiaoshaUser> opsForValue = redisTemplate.opsForValue();
MiaoshaUser user = opsForValue.get(MiaoshaUserKey.getById.getPrefix() + id);
if(user!=null){
return user;
}else {
MiaoshaUser userDaoById = miaoshaUserDao.getById(id);
if(userDaoById!=null){
opsForValue.set(MiaoshaUserKey.getById.getPrefix() + id,userDaoById);
}
return userDaoById;
}
}
如果要实现修改密码功能,那么步骤应该是:
- 先把数据存到数据库中
- 成功后,再让缓存失效
不能先操作缓存,因为如果先让缓存失效,那么在处理数据库的过程中,就有可能会被其他操作重新将旧的数据插入缓存。
二、商品详情页面静态化
直接将页面缓存到浏览器,相比于缓存到redis,效率更高。这里尝试将商品详情页静态化,动态的资源是通过接口发送的。
1.重写Controller
首先,修改Controller代码,将所有的model.addAttribute()
删掉,因为这是动态的。
然后定义一个对象,作为向页面传输的值:
@Data
public class GoodsDetailVo {
private int miaoshaStatus = 0;
private int remainSeconds = 0;
private GoodsVo goods;
private MiaoshaUser miaoshaUser;
}
然后改造Controller,不再返回页面,而是返回一个Result<GoodsDetailVo>
,前端通过Ajax获取数据再渲染到页面上。
@RequestMapping("/to_detail/{goodsId}")
@ResponseBody
public Result<GoodsDetailVo> detail(Model model, MiaoshaUser user,
@PathVariable("goodsId")long goodsId){
GoodsVo goods = goodsService.getByGoodsId(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 goodsDetailVo= new GoodsDetailVo();
goodsDetailVo.setGoods(goods);
goodsDetailVo.setMiaoshaStatus(miaoshaStatus);
goodsDetailVo.setRemainSeconds(remainSeconds);
goodsDetailVo.setMiaoshaUser(user);
return Result.success(goodsDetailVo);
}
2.页面静态化
对html文件做出修改,之前跳转是通过后端进行页面跳转,现在可以直接通过前端进行跳转再请求数据。
在商品列表页的跳转按钮,其href应该这样设置:
<td><a th:href="'/goods_detail.html?goodsId='+${goods.id}">详情</a></td>
此时goods_detail.html应该放在static的目录下,在goods_detail.html文件中获取参数使用的是正则表达式来匹配。
<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>
<button class="btn btn-primary btn-block" type="button" id="buyButton"οnclick="doMiaosha()">立即秒杀</button>
<input type="hidden" name="goodsId" id="goodsId" />
</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 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();
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 countDown(){
var remainSeconds = $("#remainSeconds").val();
var timeout;
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>
这就是新的页面代码,所有的动态数据都是通过Ajax异步请求来获取的。
三、订单详情页面静态化
1.跳转订单页面
秒杀功能页面的静态化和商品详情相似。当点击“立即秒杀”进入此方法:
function doMiaosha(){
$.ajax({
url:"/miaosha/do_miaosha",
type:"POST",
data:{
goodsId:$("#goodsId").val(),
},
success:function(data){
if(data.code == 200){
window.location.href="/order_detail.html?orderId="+data.data.id;
}else{
layer.msg(data.msg);
}
},
error:function(){
layer.msg("客户端请求有误");
}
});
}
2.MiaoshaController重写
@PostMapping("/do_miaosha")
@ResponseBody
public Result<OrderInfo> miaosha(Model model, MiaoshaUser user, @RequestParam("goodsId")long goodsId){
if(user==null){
return Result.error(CodeMsg.SESSION_ERROR);
}
//判断商品是否有库存
GoodsVo goodsVo = goodsService.getByGoodsId(goodsId);
int stock = goodsVo.getStockCount();
if(stock<=0){
return Result.error(CodeMsg.MIAOSHA_OVER);
}
//判断是否已经买过此商品(防止一人买多个)
MiaoshaOrder order = orderService.getOrderById(user.getId(),goodsId);
if(order!=null){
return Result.error(CodeMsg.REPEATE_MIAOSHA);
}
//开始秒杀:减库存、下订单、写入秒杀订单(事务)
OrderInfo orderInfo = miaoshaService.miaosha(user,goodsVo);
return Result.success(orderInfo);
}
3.订单详情页面
<body>
<div class="panel panel-default" >
<div class="panel-heading">秒杀订单详情</div>
<table class="table" id="goodslist">
<tr>
<td>商品名称</td>
<td colspan="3" id="goodsName"></td>
</tr>
<tr>
<td>商品图片</td>
<td colspan="2"><img id="goodsImg" width="200" height="200" /></td>
</tr>
<tr>
<td>订单价格</td>
<td colspan="2" id="orderPrice"></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 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);
}
$(function(){
getOrderDetail();
})
function getOrderDetail(){
var orderId = g_getQueryString("orderId");
$.ajax({
url:"/order/detail",
type:"GET",
data:{
orderId:orderId
},
success:function(data){
if(data.code == 0){
render(data.data);
}else{
layer.msg(data.msg);
}
},
error:function(){
layer.msg("客户端请求有误");
}
});
}
</script>
4.查看是否缓存成功
查看页面,观察到order_detail.html的Status Code是304.
304所表达的含义就是,客户端执行了GET请求,文件并没有变化。这时文件就可以使用本地缓存的html文件了。
但是这样客户端与服务端还是发生了一次交互,如果要客户端直接从浏览器取数据,则还需要一些配置:
#static
spring.resources.add-mappings=true
spring.resources.cache.period= 3600
spring.resources.chain.cache=true
spring.resources.chain.enabled=true
spring.resources.chain.compressed=true
spring.resources.chain.html-application-cache=true
spring.resources.static-locations=classpath:/static/
再次查看:
可以发现,这次的code成了200,并且后面的括号(from disk cache),说明这就是从缓存获取的页面。
5.OrderController
订单详情数据需要从Controller中请求,所以写一个Controller用来返回数据。
首先创建传输的数据OrderDetailVo:
@Data
public class OrderDetailVo {
private GoodsVo goods;
private OrderInfo order;
}
然后根据传入的OrderId将数据写入Vo并返回:
@GetMapping("/detail")
@ResponseBody
public Result<OrderDetailVo> orderInfo(MiaoshaUser user, @RequestParam("orderId") long orderId){
if(user==null){
return Result.error(CodeMsg.SESSION_ERROR);
}
OrderInfo orderInfo = orderService.getOrderInfoById(orderId);
if(orderInfo==null){
return Result.error(CodeMsg.ORDER_NOT_EXIST);
}
Long goodsId = orderInfo.getGoodsId();
GoodsVo goodsVo = goodsService.getByGoodsId(goodsId);
OrderDetailVo orderDetailVo = new OrderDetailVo();
orderDetailVo.setGoods(goodsVo);
orderDetailVo.setOrder(orderInfo);
return Result.success(orderDetailVo);
}
四、防止超卖
之前在压测时发现库存会变成负数,说明大量线程并发时会发生线程安全问题。
那么怎么解决呢?
首先在数据库层面,修改SQL语句。
update miaosha_goods set stock_count = stock_count - 1 where goods_id = #{goodsId} and stock_count > 0
这样就可以保证只有在库存数大于0的情况下才会减库存。
现在还有一个问题,那就是一旦一个人同时发送两个请求,同时进入了下订单的方法。那么判断逻辑根本检测不出来,这样一个人就能多次下单。
解决方法还是要通过数据库,可以利用数据库的唯一索引。将MiaoshaOrder表中的userid和orderId合成一个联合索引。
其实这里还有一个问题,老师并没有提到,那就是尽管数量不会为负数了,但是订单还是会超出数量。
五、静态资源优化
-
JS/CSS压缩,减少流量
-
多个JS/CSS组合,减少连接数
现在有很多模板可以自动实现这个功能,比如淘宝的Tengine,专门用来打包的Webpack。
- CDN 内容分发网络