Java秒杀实战 (五) 页面级高并发秒杀优化(Redis缓存+静态化分离)

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

一、页面缓存

以商品列表为例,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 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 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);
	}

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

详情 

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

Java秒杀实战 (五) 页面级高并发秒杀优化(Redis缓存+静态化分离)_第1张图片




    商品详情
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    



秒杀商品详情
您还没有登录,请登陆后再操作
没有收货地址的提示。。。
商品名称
商品图片
秒杀开始时间
商品原价
秒杀价
库存数量

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

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

然而,出现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秒杀实战 (五) 页面级高并发秒杀优化(Redis缓存+静态化分离)_第2张图片

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 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




    订单详情
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    


秒杀订单详情
商品名称
商品图片
订单价格
下单时间
订单状态
收货人 XXX 18812341234
收货地址 北京市昌平区回龙观龙博一区

五、修复超卖问题

通过数据库的方式

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.建立唯一索引

六、静态资源优化

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

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

3.CDN就近访问

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(Java秒杀实战 (五) 页面级高并发秒杀优化(Redis缓存+静态化分离))