优化
1、页面缓存+URL缓存+对象缓存(减少数据库的访问,前两者适合变化不大、缓存时间短)
页面缓存(缓解访压力,有效期不长,避免数据发生变化)
1)先从redis中去查询相关数据,查不到就到数据库中查
2)手动渲染模板存入redis中
3)结果输出
实现
@RequestMapping(value = "/to_list", produces = "text/html")
@ResponseBody
public String toList(HttpServletRequest request, HttpServletResponse response, Model model,
SeckillUser seckillUser) {
// 取缓存
String html = redisService.get(GoodsKey.getGoodsList, "", String.class);
if (!StringUtils.isEmpty(html)) {
return html;
}
List<GoodsVo> goodsList = goodsService.listGoodsVo();
model.addAttribute("goodsList", goodsList);
// 手动渲染
SpringWebContext ctx = new SpringWebContext(request, response, request.getServletContext(), request.getLocale(),
model.asMap(), applicationContext);
html = thymeleafViewResolver.getTemplateEngine().process("goods_list", ctx);
if (!StringUtils.isEmpty(html)) {
redisService.set(GoodsKey.getGoodsList, "", html);
}
return html;
}
URL缓存(与页面缓存差不多,根据不同url显示不同的界面,不同的商品详情不同goodsid区分实现)
实现商品详情
@RequestMapping(value = "/to_detail/{goodsId}", produces = "text/html")
@ResponseBody
public String detail(HttpServletRequest request, HttpServletResponse response, Model model, SeckillUser seckillUser,
@PathVariable("goodsId") long goodsId) {
// 取缓存
String html = redisService.get(GoodsKey.getGoodsDetail, "" + goodsId, String.class);
if (!StringUtils.isEmpty(html)) {
return html;
}
model.addAttribute("user", seckillUser);
GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
model.addAttribute("goods", goods);
long startAt = goods.getStartDate().getTime();
long endAt = goods.getEndDate().getTime();
long now = System.currentTimeMillis();
int seckillStatus = 0;
int remainSeconds = 0;
if (now < startAt) {// 秒杀还没开始,倒计时
seckillStatus = 0;
remainSeconds = (int) ((startAt - now) / 1000);
} else if (now > endAt) {// 秒杀已经结束
seckillStatus = 2;
remainSeconds = -1;
} else {// 秒杀进行中
seckillStatus = 1;
remainSeconds = 0;
}
model.addAttribute("seckillStatus", seckillStatus);
model.addAttribute("remainSeconds", remainSeconds);
// 手动渲染
SpringWebContext ctx = new SpringWebContext(request, response, request.getServletContext(), request.getLocale(),
model.asMap(), applicationContext);
html = thymeleafViewResolver.getTemplateEngine().process("goods_detail", ctx);
if (!StringUtils.isEmpty(html)) {
redisService.set(GoodsKey.getGoodsDetail, "" + goodsId, html);
}
return html;
}
对象缓存(缓存时间长,粒度最小,更新数据库与缓存处理的顺序不能变,不然数据不一定同步)
MiaoshaUserService:
public MiaoshaUser getById(long id){
//取缓存
MiaoshaUser user = redisService.get(MiaoshaUserKey.getById, "" + id, miaoshaUser.class);
if(user != null){
return user;
}
//取数据库
user = miaoshaUserDao.getById(id);
if(user != null){
redisService.set(MiaoshaUserKey.getById, "" + id, user);
}
return user;
}
public boolean updatePassword(long id, String token, String formPass){
SeckillUser user = getById(id);
if(user == null){
throw new GlobalException(CodeMsg.MOBILE_NOT_EXIST);
}
SeckillUser toBeUpdated = new SeckillUser();
toBeUpdated.setId(id);
toBeUpdated.setPassword(Md5Util.formPass2DbPass(formPass, user.getSalt()));
seckillUserDao.update(toBeUpdated);
redisService.delete(SeckillUserKey.getById, "" + id);
user.setPassword(toBeUpdated.getPassword());
redisService.set(MiaoshaUserKey.token, token, user);
return true;
}
2、页面静态化(数据缓存到用户浏览器)
实现方法html + ajax,把html页面缓存到浏览器,通过ajax获取数据,减少服务端访问来提高性能。
商品详情页的静态化:
GoodsDetailVo:(传送数据)
public class GoodsDetailVo {
private int miaoshaStatus = 0;
private int remainSeconds = 0;
private GoodsVo goods ;
private MiaoshaUser user;
public int getMiaoshaStatus() {
return miaoshaStatus;
}
public void setMiaoshaStatus(int miaoshaStatus) {
this.miaoshaStatus = miaoshaStatus;
}
public int getRemainSeconds() {
return remainSeconds;
}
public void setRemainSeconds(int remainSeconds) {
this.remainSeconds = remainSeconds;
}
public GoodsVo getGoods() {
return goods;
}
public void setGoods(GoodsVo goods) {
this.goods = goods;
}
public MiaoshaUser getUser() {
return user;
}
public void setUser(MiaoshaUser user) {
this.user = user;
}
}
GoodsController中goods/to_detailAPI之前是负责动态获取数据并渲染页面,现在优化成负责返回动态数据
@RequestMapping(value="/detail/{goodsId}")
@ResponseBody
public Result<GoodsDetailVo> detail(HttpServletRequest request, HttpServletResponse response, 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);
}
resources/static下创建goods_detail.htm
<!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>-->
<button class="btn btn-primary btn-block" type="button" id="buyButton"onclick="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 doMiaosha(){
$.ajax({
url:"/miaosha/do_miaosha",
type:"POST",
data:{
goodsId:$("#goodsId").val(),
},
success:function(data){
if(data.code == 0){
window.location.href="/order_detail.htm?orderId="+data.data.id;
}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();
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>
</html>
修改goods_list.html中超链接的参数:
<td><a th:href="'/goods_detail.htm?goodsId='+${goods.id}">详情</a></td>
修改common.js(作用于获取数据)
//展示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="1a2b3c4d"
// 获取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;
};
在application.properties添加以下配置(客户端根据缓存时间判断是否需要重新请求静态页面)
#static
spring.resources.add-mappings=true
spring.resources.cache.period=3600
spring.resources.chain.cache=true
spring.resources.chain.enabled=true
spring.resources.chain.html-application-cache=true
spring.resources.static-locations=classpath:/static/
秒杀静态化
MiaoshaController
//GET和POST不同,GET是幂等的,从服务端获取数据,对数据没有影响,POST是向服务端提交数据,服务端数据可能发生变化
@RequestMapping(value="/do_miaosha", method= RequestMethod.POST)
@ResponseBody
public Result<OrderInfo> miaosha(@ApiParam("将数据返回给前端")Model model,@ApiParam("封装的用户的信息") MiaoshaUser user,
@ApiParam("秒杀商品的id")@RequestParam("goodsId")long goodsId) {
model.addAttribute("user", user);
if(user == null) {
return Result.error(CodeMsg.SESSION_ERROR);
}
//判断库存
GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);//10个商品,req1 req2
int stock = goods.getStockCount();
if(stock <= 0) {
return Result.error(CodeMsg.MIAO_SHA_OVER);
}
//判断是否已经秒杀到了
MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
if(order != null) {
return Result.error(CodeMsg.REPEATE_MIAOSHA);
}
//减库存 下订单 写入秒杀订单
OrderInfo orderInfo = miaoshaService.miaosha(user, goods);
return Result.success(orderInfo);
}
}
OrderService
@Service
public class OrderService {
@Autowired
OrderDao orderDao;
@Autowired
RedisSevice redisSevice;
public MiaoshaOrder getMiaoshaOrderByUserIdGoodsId(long userId, long goodsId) {
return redisSevice.get(OrderKey.getMiaoshaOrderByUidGid,""+userId+" "+goodsId,MiaoshaOrder.class);
}
public OrderInfo getOrderById(long orderId) {
return orderDao.getOrderById(orderId);
}
@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(0);
orderInfo.setUserId(user.getId());
long orderId = orderDao.insert(orderInfo);
MiaoshaOrder miaoshaOrder = new MiaoshaOrder();
miaoshaOrder.setGoodsId(goods.getId());
miaoshaOrder.setOrderId(orderId);
miaoshaOrder.setUserId(user.getId());
orderDao.insertMiaoshaOrder(miaoshaOrder);
redisSevice.set(OrderKey.getMiaoshaOrderByUidGid,""+user.getId()+" "+goods.getId(),miaoshaOrder);
return orderInfo;
}
}
订单详情接口静态化
OrderController:
@RequestMapping("/detail")
@ResponseBody
public Result<OrderDetailVo> info(Model model, MiaoshaUser user, @RequestParam("orderId") long orderId) {
if(user == null) {
return Result.error(CodeMsg.SESSION_ERROR);
}
OrderInfo order = orderService.getOrderById(orderId);
if(order == null) {
return Result.error(CodeMsg.ORDER_NOT_EXIST);
}
long goodsId = order.getGoodsId();
GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
OrderDetailVo vo = new OrderDetailVo();
vo.setOrder(order);
vo.setGoods(goods);
return Result.success(vo);
}
OrderDetailVo:
public class OrderDetailVo {
private GoodsVo goods;
private OrderInfo order;
public GoodsVo getGoods() {
return goods;
}
public void setGoods(GoodsVo goods) {
this.goods = goods;
}
public OrderInfo getOrder() {
return order;
}
public void setOrder(OrderInfo order) {
this.order = order;
}
}
resources/static下创建order_detail.htm
<!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 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>
修复库存漏洞
通过数据库的方式
1、SQL语句限制
在GoodsDao中优化了sql语句,限制了库存数
@Update("update miaosha_goods set stock_count = stock_count - 1 where goods_id = #{goodsId} and stock_count>0")
public int reduceStock(MiaoshaGoods g);
2、建立唯一索引
对秒杀订单表建立唯一索引
静态资源优化
1.JS/CSS压缩,减少流量
2.多个JS/CSS组合,减少连接数
3.CDN就近访问(实时的将用户请求导向离用户最近的服务节点上,提高用户访问响应速度)