【SpringBoot商城秒杀系统项目实战17】页面优化技术(页面缓存+URL缓存+对象缓存)
页面优化技术
-
页面缓存+URL缓存+对象缓存
由于并发瓶颈在数据库,想办法如何减少对数据库的访问,所以加若干缓存来提高,通过各种粒度的缓存,最大粒度页面缓存到最小粒度的对象级缓存。 -
页面静态化,前后端分离
都是纯的html,通过js或者ajax来请求服务器,如果做了静态化,浏览器可以把html缓存在客户端。 -
静态资源优化
JS/CSS压缩,减少流量。(压缩版的js,去掉多余的空格字符。区别于阅读版)
JS/CSS组合,减少连接数。(将多个JS和CSS的组合到一个请求里面去,一下子从服务端全部下载下来) -
CDN优化
内容分发网络,就近访问。
一般,页面缓存和URL缓存时间比较短,适合场景:变化不大的页面。如果分页,不会全部缓存,一般缓存前一两页。
首先介绍:页面缓存+URL缓存+对象缓存
页面缓存
1.取缓存 (缓存里面存的是html)
2.手动渲染模板
3.结果输出(直接输出html代码)
GoodsController里面的toListCache方法改造一下
/**
* 做页面缓存的list页面,防止同一时间访问量巨大到达数据库,如果缓存时间过长,数据及时性就不高。
*/
//5-17
@RequestMapping(value="/to_list",produces="text/html")
@ResponseBody
public String toListCache(Model model,MiaoshaUser user,HttpServletRequest request,
HttpServletResponse response) {
// 1.取缓存
// public <T> T get(KeyPrefix prefix,String key,Class<T> data)
String html = redisService.get(GoodsKey.getGoodsList, "", String.class);
if (!StringUtils.isEmpty(html)) {
return html;
}
model.addAttribute("user", user);
//1.查询商品列表
List<GoodsVo> goodsList= goodsService.getGoodsVoList();
model.addAttribute("goodsList", goodsList);
//2.手动渲染 使用模板引擎 templateName:模板名称 String templateName="goods_list";
SpringWebContext context=new SpringWebContext(request,response,request.getServletContext(),
request.getLocale(),model.asMap(),applicationContext);
html=thymeleafViewResolver.getTemplateEngine().process("goods_list", context);
//保存至缓存
if(!StringUtils.isEmpty(html)) {
redisService.set(GoodsKey.getGoodsList, "", html);//key---GoodsKey:gl---缓存goodslist这个页面
}
return html;
//return "goods_list";//返回页面login
}
当访问goods_list页面的时候,如果从缓存中取到就返回这个html,(这里方法的返回格式已经设置为text/html,这样就是返回html的源代码),如果取不到,利用ThymeleafViewResolver的getTemplateEngine().process和我们获取到的数据,渲染模板,并且在返回到前端之前保存至缓存里面,然后之后再来获取的时候,只要缓存里面存的goods_list页面的html还没有过期,那么直接返回给前端即可。
一般这个页面缓存时间,也不会很长,防止数据的时效性很低。但是可以防止短时间大并发访问。
GoodsKey :作为页面缓存的缓存Key的前缀,缓存有效时间,一般设置为1分钟
public class GoodsKey extends BasePrefix{
//考虑页面缓存有效期比较短
public GoodsKey(int expireSeconds,String prefix) {
super(expireSeconds,prefix);
}
//goods_list页面 1分钟
public static GoodsKey getGoodsList=new GoodsKey(60,"gl");
//goods_detail页面 1分钟
public static GoodsKey getGoodsDetail=new GoodsKey(60,"gd");
//秒杀的商品的数量stock,0不失效
public static GoodsKey getMiaoshaGoodsStock=new GoodsKey(0,"gs");
}
URL缓存
这里的url缓存相当于页面缓存,针对不同的详情页显示不同缓存页面,对不同的url进行缓存(redisService.set(GoodsKey.getGoodsDetail, “”+goodsId, html),与页面缓存实质一样。
/**
* 做了页面缓存的to_detail商品详情页。
* 做了页面缓存 URL缓存 ""+goodsId 不同的url进行缓存redisService.set(GoodsKey.getGoodsDetail, ""+goodsId, html);
* @param model
* @param user
* @param goodsId
* @return
*/
@RequestMapping(value="/to_detail_html/{goodsId}") //produces="text/html"
@ResponseBody
public String toDetailCachehtml(Model model,MiaoshaUser user,
HttpServletRequest request,HttpServletResponse response,@PathVariable("goodsId")long goodsId) {//id一般用snowflake算法
// 1.取缓存
// public <T> T get(KeyPrefix prefix,String key,Class<T> data)
String html = redisService.get(GoodsKey.getGoodsDetail, ""+goodsId, String.class);//不同商品页面不同的详情
if (!StringUtils.isEmpty(html)) {
return html;
}
//缓存中没有,则将业务数据取出,放到缓存中去。
model.addAttribute("user", user);
GoodsVo goods=goodsService.getGoodsVoByGoodsId(goodsId);
model.addAttribute("goods", goods);
//既然是秒杀,还要传入秒杀开始时间,结束时间等信息
long start=goods.getStartDate().getTime();
long end=goods.getEndDate().getTime();
long now=System.currentTimeMillis();
//秒杀状态量
int status=0;
//开始时间倒计时
int remailSeconds=0;
//查看当前秒杀状态
if(now<start) {//秒杀还未开始,--->倒计时
status=0;
remailSeconds=(int) ((start-now)/1000); //毫秒转为秒
}else if(now>end){ //秒杀已经结束
status=2;
remailSeconds=-1; //毫秒转为秒
}else {//秒杀正在进行
status=1;
remailSeconds=0; //毫秒转为秒
}
model.addAttribute("status", status);
model.addAttribute("remailSeconds", remailSeconds);
// 2.手动渲染 使用模板引擎 templateName:模板名称 String templateName="goods_detail";
SpringWebContext context = new SpringWebContext(request, response, request.getServletContext(),
request.getLocale(), model.asMap(), applicationContext);
html = thymeleafViewResolver.getTemplateEngine().process("goods_detail", context);
// 将渲染好的html保存至缓存
if (!StringUtils.isEmpty(html)) {
redisService.set(GoodsKey.getGoodsDetail, ""+goodsId, html);
}
return html;//html是已经渲染好的html文件
//return "goods_detail";//返回页面login
}
对象缓存
相比页面缓存是更细粒度缓存。在实际项目中, 不会大规模使用页面缓存,对象缓存就是当用到用户数据的时候,可以从缓存中取出。比如:更新用户密码,根据token来获取用户缓存对象。
MiaoshaUserService里面增加getById方法,先去取缓存,如果缓存中拿不到,那么就去取数据库,然后再设置到缓存中去
/**
* 根据id取得对象,先去缓存中取
* @param id
* @return
*/
public MiaoshaUser getById(long id) {
//1.取缓存 ---先根据id来取得缓存
MiaoshaUser user=redisService.get(MiaoshaUserKey.getById, ""+id, MiaoshaUser.class);
//能再缓存中拿到
if(user!=null) {
return user;
}
//2.缓存中拿不到,那么就去取数据库
user=miaoshaUserDao.getById(id);
//3.设置缓存
if(user!=null) {
redisService.set(MiaoshaUserKey.getById, ""+id, user);
}
return user;
}
MiaoshaUserKey,这里我们认为对象缓存一般没有有效期,永久有效
public class MiaoshaUserKey extends BasePrefix{
public static final int TOKEN_EXPIRE=3600*24*2;//3600S*24*2 =2天
public MiaoshaUserKey(int expireSeconds,String prefix) {
super(expireSeconds,prefix);
}
public static MiaoshaUserKey token=new MiaoshaUserKey(TOKEN_EXPIRE,"tk");
//对象缓存一般没有有效期,永久有效
public static MiaoshaUserKey getById=new MiaoshaUserKey(0,"id");
}
更新用户密码:更新数据库与缓存,一定保证数据一致性,修改token关联的对象以及id关联的对象,先更新数据库后删除缓存,不能直接删除token,删除之后就不能登录了,再将token以及对应的用户信息一起再写回缓存里面去。
/**
* 注意数据修改时候,保持缓存与数据库的一致性
* 需要传入token
* @param id
* @return
*/
public boolean updatePassword(String token,long id,String passNew) {
//1.取user对象,查看是否存在
MiaoshaUser user=getById(id);
if(user==null) {
throw new GlobalException(CodeMsg.MOBILE_NOTEXIST);
}
//2.更新密码
MiaoshaUser toupdateuser=new MiaoshaUser();
toupdateuser.setId(id);
toupdateuser.setPwd(MD5Util.inputPassToDbPass(passNew, user.getSalt()));
miaoshaUserDao.update(toupdateuser);
//3.更新数据库与缓存,一定保证数据一致性,修改token关联的对象以及id关联的对象
redisService.delete(MiaoshaUserKey.getById, ""+id);
//不能直接删除token,删除之后就不能登录了
user.setPwd(toupdateuser.getPwd());
redisService.set(MiaoshaUserKey.token, token,user);
return true;
}
RedisService里面的delete方法
public boolean delete(KeyPrefix prefix,String key){
Jedis jedis=null;
try {
jedis=jedisPool.getResource();
String realKey=prefix.getPrefix()+key;
long ret=jedis.del(realKey);
return ret>0;//删除成功,返回大于0
//return jedis.decr(realKey);
}finally {
returnToPool(jedis);
}
}
MiaoshaUserDao 代码:
@Mapper
public interface MiaoshaUserDao {
@Select("select * from miaosha_user where id=#{id}") //这里#{id}通过后面参数来为其赋值
public MiaoshaUser getById(@Param("id")long id); //绑定
//绑定在对象上面了[email protected]("id")long id,@Param("pwd")long pwd 效果一致
@Update("update miaosha_user set pwd=#{pwd} where id=#{id}")
public void update(MiaoshaUser toupdateuser);
//public boolean update(@Param("id")long id); //绑定
}
思考
为什么不能先处理缓存,再更新数据库呢?
为什么你的缓存更新策略是先更新数据库后删除缓存,讲讲其他的情况有什么问题?
问题:怎么保持缓存与数据库一致?
要解答这个问题,我们首先来看不一致的几种情况。我将不一致分为三种情况
数据库有数据,缓存没有数据;
数据库有数据,缓存也有数据,数据不相等;
数据库没有数据,缓存有数据。
策略:
-
首先尝试从缓存读取,读到数据则直接返回;如果读不到,就读数据库,并将数据会写到缓存,并返回。
-
需要更新数据时,先更新数据库,然后把缓存里对应的数据失效掉(删掉)。
读的逻辑大家都很容易理解,谈谈更新。如果不采取我提到的这种更新方法,你还能想到什么更新方法呢?大概会是:先删除缓存,然后再更新数据库。这么做引发的问题是,如果A,B两个线程同时要更新数据,并且A,B已经都做完了删除缓存这一步,接下来,A先更新了数据库,C线程读取数据,由于缓存没有,则查数据库,并把A更新的数据,写入了缓存,最后B更新数据库。那么缓存和数据库的值就不一致了。
另外有人会问,如果采用你提到的方法,为什么最后是把缓存的数据删掉,而不是把更新的数据写到缓存里。这么做引发的问题是,如果A,B两个线程同时做数据更新,A先更新了数据库,B后更新数据库,则此时数据库里存的是B的数据。而更新缓存的时候,是B先更新了缓存,而A后更新了缓存,则缓存里是A的数据。这样缓存和数据库的数据也不一致。
我想出的解决方案大概有以下几种:
1. 对删除缓存进行重试,数据的一致性要求越高,我越是重试得快。
2. 定期全量更新,简单地说,就是我定期把后再全量加载。
3. 给所有的缓存一个失效期。
第三种方案可以说是一个大杀器,任何不一致,都可以靠失效期解决,失效期越短,数据一致性越高。但是失效期越短,查数据库就会越频繁。因此失效期应该根据业务来定。
作者:小小少年Boy
链接:https://www.jianshu.com/p/8950c52ce53b
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。