秒杀项目学习第五章
主要内容
- 页面缓存+URL缓存+对象缓存
- 页面静态化,前后端分离
- 静态资源优化
- CDN优化
一、 页面缓存+URL缓存+对象缓存
页面缓存
- 取缓存
- 手动渲染模板
- 结果输出直接返回html页面
以获取商品列表页面为例
1.原先controller
2.现在controller
@RequestMapping(value="/to_list", produces="text/html")
@ResponseBody
public String list(HttpServletRequest request, HttpServletResponse response, Model model, MiaoshaUser user) {
model.addAttribute("user", user);
//取缓存见3
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.isEmpty(html)) {
redisService.set(GoodsKey.getGoodsList, "", html);
}
return html;
}
2.GoodsKey
一般页面缓存时间都比较短,时间长的话页面及时性就不会很好
3.Redis中存的数据
4.浏览器中的显示
URL缓存
以商品详情页面为例
1.以前的controller
@RequestMapping("/to_detail/{goodsId}")
public String detail(Model model,MiaoshaUser user,
@PathVariable("goodsId")long goodsId) {
model.addAttribute("user", user);
GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
model.addAttribute("goods", goods);
long startAt = goods.getStartDate().getTime();
long endAt = goods.getEndDate().getTime();
long now = System.currentTimeMillis();
int miaoshaStatus = 0;
int remainSeconds = 0;
int endSeconds = 0;
if(now < startAt ) {//秒杀还没开始,倒计时
miaoshaStatus = 0;
remainSeconds = (int)((startAt - now )/1000);
endSeconds = (int)((endAt - now )/1000);
}else if(now > endAt){//秒杀已经结束
miaoshaStatus = 2;
remainSeconds = -1;
}else {//秒杀进行中
miaoshaStatus = 1;
remainSeconds = 0;
endSeconds = (int)((endAt - now )/1000);
}
model.addAttribute("miaoshaStatus", miaoshaStatus);
model.addAttribute("remainSeconds", remainSeconds);
model.addAttribute("endSeconds", endSeconds);
return "goods_detail";
}
2.现在的controller
@RequestMapping(path = "/to_detail/{goodsid}",produces = "text/html")
@ResponseBody
public String toDetail(HttpServletRequest request,
HttpServletResponse response,
Model model,
MiaoshaUser user,
@PathVariable("goodsid")long goodsid) {
model.addAttribute("user", user);
//取缓存
String html = redisService.get(GoodsKey.getGoodsDetail,""+goodsid,String.class);
if(!StringUtils.isEmpty(html)){
return html;
}
GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsid);
model.addAttribute("goods", goods);
long startAt = goods.getStartDate().getTime();
long endAt = goods.getEndDate().getTime();
long now = System.currentTimeMillis();
int miaoshaStatus = 0;
int remainSeconds = 0;
int endSeconds = 0;
if(now < startAt ) {//秒杀还没开始,倒计时
miaoshaStatus = 0;
remainSeconds = (int)((startAt - now )/1000);
endSeconds = (int)((endAt - now )/1000);
}else if(now > endAt){//秒杀已经结束
miaoshaStatus = 2;
remainSeconds = -1;
}else {//秒杀进行中
miaoshaStatus = 1;
remainSeconds = 0;
endSeconds = (int)((endAt - now )/1000);
}
model.addAttribute("miaoshaStatus", miaoshaStatus);
model.addAttribute("remainSeconds", remainSeconds);
model.addAttribute("endSeconds", endSeconds);
//手动渲染
WebContext context = new WebContext(request,
response,
request.getServletContext(),
request.getLocale(),
model.asMap());
html = thymeleafViewResolver.getTemplateEngine().process("goods_detail", context);
if(!StringUtils.isEmpty(html)){
//System.out.println(html);
redisService.set(GoodsKey.getGoodsDetail,""+goodsid,html);
}
return html;
// return "goods_detail";
}
和以前controller的区别
1.取缓存
2.渲染模板直接返回html
3.Redis中数据
和页面缓存的区别
- 页面缓存不存在随着url的变化页面变化的情况
- URL缓存虽然模板一样但是渲染的数据不一样,存到redis中时的key是不同的
对象缓存
更细粒度的缓存以前已经用到了,在分布式Session中我们把token映射成User时就是对象级缓存
其他的类似用到数据库查询根据某值(token)取某值(User)的也可以这么改造
比如:MiaoshaUserService.getById方法
1.设置一个key
一般对象缓存都是永久有效
2.以前的getByid方法
2.修改后的getByid方法
注意:当更新已经缓存的对象时,一般步骤如下
- 取出要更新的对象
- 更新数据库
- 更新所有与此对象有关的缓存
对象缓存也说明了为啥Aservice使用Bservice中的功能时为啥不能直接用Bdao因为Bservice中可能存在缓存操作,调Bservice可以提高效率
二、页面静态化,前后端分离
- 常用技术AngularJS、Vue.js
- 优点:利用浏览器的缓存
商品详情页面静态化
1.GoodsController.detail方法
//页面静态化
@RequestMapping(path = "/to_detail/{goodsid}")
@ResponseBody
public Result<GoodsDetailVo> detail(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;
int endSeconds = 0;
if(now < startAt ) {//秒杀还没开始,倒计时
miaoshaStatus = 0;
remainSeconds = (int)((startAt - now )/1000);
endSeconds = (int)((endAt - now )/1000);
}else if(now > endAt){//秒杀已经结束
miaoshaStatus = 2;
remainSeconds = -1;
}else {//秒杀进行中
miaoshaStatus = 1;
remainSeconds = 0;
endSeconds = (int)((endAt - now )/1000);
}
//把以前放到model里的对象封装进GoodsDetailVo,见2
GoodsDetailVo vo = new GoodsDetailVo();
vo.setGoods(goods);
vo.setMiaoshaStatus(miaoshaStatus);
vo.setRemainSeconds(remainSeconds);
vo.setEndSeconds(endSeconds);
vo.setUser(user);
return Result.success(vo);
// return "goods_detail";
}
2.GoodsDetailVo
/**
* 向页面传数据
*/
public class GoodsDetailVo {
private int miaoshaStatus = 0;
private int remainSeconds = 0;
private int endSeconds = 0;
private GoodsVo goods;
private MiaoshaUser user;
3.修改goods_detail页面
- 把goods_detail.html页面改名为goods_detail.htm放到static目录下去掉Thymeleaf的所有东西
改名原因是application.properties中配置了如下参数
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
- 修改goods_list.html中的跳转地址,直接跳转到页面
- 修改good_detail.htm中的代码
- js代码,ajax请求json数据后填入html中
/*这是jQuery的写法
* 原本为:
*
* $(document).ready(function(){
// 开始写 jQuery 代码...
});
* 页面加载完之后才会执行
* */
$(function(){
getDetail();
//countDown();
});
function getDetail() {
//获取参数值
var goodsId = g_getQueryString("goodsId");
$.ajax({
url:"/goods/to_detail/"+goodsId,
type:"GET",
success:function(data){
//如果Result中code==0那么就是成功的
if(data.code==0){
//渲染页面的方法
render(data.data);
}else{
layer.msg(data.msg);
}
},
error:function (){
layer.msg("客户端请求错误333");
}
});
}
function render(vo) {
var miaoshaStatus = vo.miaoshaStatus;
var remainSeconds = vo.remainSeconds;
var endSeconds = vo.endSeconds;
var goods = vo.goods;
var user = vo.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);
$("#endSeconds").val(endSeconds);
$("#goodsId").val(goods.id);
$("#goodsPrice").text(goods.goodsPrice);
$("#miaoshaPrice").text(goods.miaoshaPrice);
$("#stockCount").text(goods.stockCount);
//处理id=miaoshaTip
countDown();
}
function countDown(){
var remainSeconds = $("#remainSeconds").val();
var endSeconds = $("#endSeconds").val();
var timeout;
var endtimeout;
if(remainSeconds > 0){//秒杀还没开始,倒计时
$("#buyButton").attr("disabled", true);
timeout = setTimeout(function(){
$("#miaoshaTip").html("剩余时间:"+(remainSeconds - 1));
$("#remainSeconds").val(remainSeconds - 1);
$("#endSeconds").val(endSeconds - 1);
countDown();
},1000);
}else if(remainSeconds == 0){//秒杀进行中
$("#buyButton").attr("disabled", false);
if(timeout){
clearTimeout(timeout);
}
if(endSeconds>0){
$("#miaoshaTip").html("秒杀进行中");
}else{
$("#remainSeconds").val(remainSeconds - 1);
}
endtimeout = setTimeout(function(){
$("#endSeconds").val(endSeconds - 1);
countDown();
},1000);
}else{//秒杀已经结束
if(endtimeout){
clearTimeout(endtimeout);
}
$("#buyButton").attr("disabled", true);
$("#miaoshaTip").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.htm?orderId="+data.data.id;
}else{
layer.msg(data.msg);
}
},
error:function () {
layer.msg("客户端请求有误")
}
});
}
</script>
订单详情页静态化
看上边js代码的 doMiaosha方法,我们需要修改url:"/miaosha/do_miaosha"的方法
1.原本MiaoshaController.miaosha方法
@RequestMapping(path = "/do_miaosha",method = RequestMethod.POST)
public String miaosha(Model model, MiaoshaUser user,
@RequestParam("goodsId")long goodsId) {
model.addAttribute("user", user);
if(user == null) {
return "/login";
}
//判断库存
GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
int stock = goods.getStockCount();
if(stock <= 0) {
model.addAttribute("errmsg", CodeMsg.MIAO_SHA_OVER.getMsg());
return "miaosha_fail";
}
//判断是否已经秒杀到了,不能重复秒杀
MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
if(order != null) {
model.addAttribute("errmsg", CodeMsg.REPEATE_MIAOSHA.getMsg());
return "miaosha_fail";
}
//减库存 下订单 写入秒杀订单
OrderInfo orderInfo = miaoshaService.miaosha(user, goods);
model.addAttribute("orderInfo", orderInfo);
model.addAttribute("goods", goods);
return "order_detail";
}
2.修改后的MiaoshaController.miaosha方法
也是返回json数据
@RequestMapping(value="/do_miaosha", method=RequestMethod.POST)
@ResponseBody
public Result<OrderInfo> miaosha(Model model,MiaoshaUser user,
@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);
}
3.然后根据上边提到的js中的方法判断是否跳转到订单详情页
4.同样的方法修改order_detail.html页面为order_detail.htm并放在static目录下
5.修改order_detail.htm的内容
取出Thymeleaf模板,使用ajax请求填充的数据。这里只帖出来ajax请求内容
<script>
$(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("客户端请求错误");
}
});
//渲染页面
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>
6.新增OrderController处理请求订单数据
@RequestMapping(path = "/detail",method = RequestMethod.GET)
@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);
}
7.OrderDetailVo 、orderService.getOrderById、orderDao.getOrderById
public class OrderDetailVo {
private GoodsVo goods;
private OrderInfo order;
添加Spring对静态文件的配置
#static
spring.resources.add-mappings=true
#对Resolver加缓存
spring.resources.chain.cache=true
spring.resources.chain.enabled=true
spring.resources.chain.html-application-cache=true
#指明静态文件路径
spring.resources.static-locations=classpath:/static/
使用一波
自我总结
- 页面静态化无非就是使用纯html页面+ajax请求json数据后再填充页面
- 若A页面跳转到B页面之前需要条件判断可以先在A页面中利用ajax请求判断后再跳转
- 如果不需要条件判断可以直接跳转到B的静态页面,让B自己用ajax请求数据
题外话GET和POST的区别
- get是幂等的,无论请求多少次得到的结果都一样
- post,用于对服务端数据更改
三、超卖问题的解决
库存减为负数
出现问题的原因
解决问题:
修改最后的更新sql,数据库更新时会加锁,让数据库保证数据正确性
一个用户秒杀到两件商品
出现问题的原因
开始是一个用户同时发出两个请求,通过controller中方法的如下检验
- 判断库存
- 判断是否已经秒杀到了
调用如下方法,造成一个用户秒杀到了两件商品
解决方法:
利用数据库的唯一索引特性,我们可以给miaosha_order表添加一个索引
这样在创建订单的时候就会发现没法插入第二个订单造成回滚,防止一个用户同时秒杀两个。但是其实我们一般不会让一个用户同时发出两个请求,比如秒杀前先填个验证码之类的可以请求错开时间。
对于超卖问题一般都是想法从数据库层面解决问题
为了压测提高性能把OrderService.getMiaoshaOrderByUserIdGoodsId修改为对象缓存
1.以前的getMiaoshaOrderByUserIdGoodsId方法
2.修改一下直接从缓存中取
OrderKey
3.为了能直接从缓存里取生成订单的时候得写入缓存
OrderService.createOrder
四、静态资源优化
- JS/CSS压缩,减少流量,如xx.min.js
- 多个JS/CSS组合,减少连接数,可以参考tengine
- CDN就近访问,多个节点缓存数据,找到离客户端最近的节点
总结
- 从浏览器开始使用页面静态化技术(前后端分离)缓存页面
- 浏览器发出请求时经过CDN处理
- 请求到达服务器时可以使用页面缓存、URL缓存、对象缓存
- 总之想尽办法减少对数据库的访问,但是也要考虑数据不一致的问题