我们发现,目前系统最大的瓶颈就在数据库访问。因此,系统优化的方案核心在于减少数据库的访问,而缓存就是一个好方法。

一、页面缓存

以商品列表为例,Controller方法改造如下

@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参数从缓存中获取不同的页面数据

以查看商品详情的方法为例,Controller方法改造如下

@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;
	}

三、对象缓存

对象缓存控制粒度比页面缓存细,但要注意对象变更时缓存值的处理

SeckillUserService方法修改如下:

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(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(SeckillUserKey.token, token, user);
		
		return true;
	}

四、页面静态化(前后端分离)

简单的说,就是html页面 + ajax。通过把静态html页面缓存到客户端浏览器,通过ajax动态获取数据,从而减少服务端访问。

现在比较流行的方案是使用AngularJs或者VueJs。本节为了演示方便直接使用html+jquery模拟。

新增vo用于传输数据

package com.wings.seckill.vo;

import com.wings.seckill.domain.SeckillUser;

public class GoodsDetailVo {
	private int seckillStatus = 0;
	private int remainSeconds = 0;
	private GoodsVo goods;
	private SeckillUser user;

	public int getSeckillStatus() {
		return seckillStatus;
	}

	public void setSeckillStatus(int seckillStatus) {
		this.seckillStatus = seckillStatus;
	}

	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 SeckillUser getUser() {
		return user;
	}

	public void setUser(SeckillUser user) {
		this.user = user;
	}
}

获取商品详情的Controller方法之前是负责服务端动态获取数据并渲染页面,现在改为只负责返回动态数据,页面缓存到客户端浏览器。

@RequestMapping(value = "/detail/{goodsId}")
	@ResponseBody
	public Result<GoodsDetailVo> detail(HttpServletRequest request, HttpServletResponse response, Model model,
			SeckillUser 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 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;
		}
		GoodsDetailVo vo = new GoodsDetailVo();
		vo.setGoods(goods);
		vo.setUser(user);
		vo.setRemainSeconds(remainSeconds);
		vo.setSeckillStatus(seckillStatus);
		return Result.success(vo);
	}

商品列表中跳转方式更改如下:

<td><a th:href="'/goods_detail.htm?goodsId='+${goods.id}">详情</a></td>

新增商品详情静态页面goods_detail.htm(注意是.htm,不是.html!)

java高并发场景超卖问题 java 实现高并发秒杀_java高并发场景超卖问题

<!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="seckillTip"></span>
        </td>
        <td>
        <!--  
        	<form id="seckillForm" method="post" action="/seckill/do_seckill">
        		<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="doSeckill()">立即秒杀</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="seckillPrice"></td>  
     </tr>
     <tr>  
        <td>库存数量</td>  
        <td colspan="3"  id="stockCount"></td>  
     </tr>
  </table>
</div>
</body>
<script>

function doSeckill(){
	$.ajax({
		url:"/seckill/do_seckill",
		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 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();
}

$(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);
	   $("#seckillTip").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);
		}
		$("#seckillTip").html("秒杀进行中");
	}else{//秒杀已经结束
		$("#buyButton").attr("disabled", true);
		$("#seckillTip").html("秒杀已经结束");
	}
}

</script>
</html>

common.js新增一下方法:

// 获取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;
};

java高并发场景超卖问题 java 实现高并发秒杀_java高并发场景超卖问题_02

可以看到,此刻的页面是从缓存中获取的!

然而,出现304还不够,因为这意味着客户端还是要发一次请求给浏览器以确认静态页面是否有变化。

我们希望客户端直接根据缓存时间判断是否需要重新请求静态页面,在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.gzipped=true
spring.resources.chain.html-application-cache=true
spring.resources.static-locations=classpath:/static/

java高并发场景超卖问题 java 实现高并发秒杀_缓存_03

BTW:在谷歌浏览器上未能看到此现象。。。。。。

 

秒杀及订单详情接口静态化

package com.wings.seckill.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import com.wings.seckill.domain.OrderInfo;
import com.wings.seckill.domain.SeckillOrder;
import com.wings.seckill.domain.SeckillUser;
import com.wings.seckill.redis.RedisService;
import com.wings.seckill.result.CodeMsg;
import com.wings.seckill.result.Result;
import com.wings.seckill.service.GoodsService;
import com.wings.seckill.service.OrderService;
import com.wings.seckill.service.SeckillService;
import com.wings.seckill.service.SeckillUserService;
import com.wings.seckill.vo.GoodsVo;

@Controller
@RequestMapping("/seckill")
public class SeckillController {

	@Autowired
	SeckillUserService userService;

	@Autowired
	RedisService redisService;

	@Autowired
	GoodsService goodsService;

	@Autowired
	OrderService orderService;

	@Autowired
	SeckillService seckillService;

	@RequestMapping(value = "/do_seckill", method = RequestMethod.POST)
	@ResponseBody
	public Result<OrderInfo> list(Model model, SeckillUser user, @RequestParam("goodsId") long goodsId) {
		
		if (user == null) {
			return Result.error(CodeMsg.SESSION_ERROR);
		}

		// 判断库存
		GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
		int stock = goods.getStockCount();
		if (stock <= 0) {
			return Result.error(CodeMsg.SECKill_OVER);
		}

		// 判断是否已经秒杀到了
		SeckillOrder order = orderService.getSeckillOrderByUserIdGoodsId(user.getId(), goodsId);
		if (order != null) {
			return Result.error(CodeMsg.REPEATE_SECKILL);
		}
		// 减库存 下订单 写入秒杀订单
		OrderInfo orderInfo = seckillService.seckill(user, goods);
		return Result.success(orderInfo);
	}
}
package com.wings.seckill.service;

import java.util.Date;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.wings.seckill.dao.OrderDao;
import com.wings.seckill.domain.OrderInfo;
import com.wings.seckill.domain.SeckillOrder;
import com.wings.seckill.domain.SeckillUser;
import com.wings.seckill.redis.OrderKey;
import com.wings.seckill.redis.RedisService;
import com.wings.seckill.vo.GoodsVo;

@Service
public class OrderService {

	@Autowired
	OrderDao orderDao;

	@Autowired
	RedisService redisService;

	public SeckillOrder getSeckillOrderByUserIdGoodsId(long userId, long goodsId) {
		SeckillOrder seckillOrder = redisService.get(OrderKey.getSeckillOrderByUidGid, "" + userId + "_" + goodsId,
				SeckillOrder.class);
		if (seckillOrder != null) {
			return seckillOrder;
		}
		return orderDao.getSeckillOrderByUserIdGoodsId(userId, goodsId);
	}

	@Transactional
	public OrderInfo createOrder(SeckillUser 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.getSeckillPrice());
		orderInfo.setOrderChannel(1);
		orderInfo.setStatus(0);
		orderInfo.setUserId(user.getId());
		long orderId = orderDao.insert(orderInfo);

		SeckillOrder seckillOrder = new SeckillOrder();
		seckillOrder.setGoodsId(goods.getId());
		seckillOrder.setOrderId(orderId);
		seckillOrder.setUserId(user.getId());
		orderDao.insertSeckillOrder(seckillOrder);

		redisService.set(OrderKey.getSeckillOrderByUidGid, "" + user.getId() + "_" + goods.getId(), seckillOrder);
		return orderInfo;
	}

	public OrderInfo getOrderById(long orderId) {
		return orderDao.getOrderById(orderId);
	}

}

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 减库存方法更改如下:

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

2.建立唯一索引

java高并发场景超卖问题 java 实现高并发秒杀_redis_04

六、静态资源优化

1.JS/CSS压缩,减少流量

2.多个JS/CSS组合,减少连接数

3.CDN就近访问