欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

【SpringBoot商城秒杀系统项目实战17】页面优化技术(页面缓存+URL缓存+对象缓存)

程序员文章站 2022-06-20 08:36:44
...

页面优化技术

  1. 页面缓存+URL缓存+对象缓存
    由于并发瓶颈在数据库,想办法如何减少对数据库的访问,所以加若干缓存来提高,通过各种粒度的缓存,最大粒度页面缓存到最小粒度的对象级缓存。
  2. 页面静态化,前后端分离
    都是纯的html,通过js或者ajax来请求服务器,如果做了静态化,浏览器可以把html缓存在客户端。
  3. 静态资源优化
    JS/CSS压缩,减少流量。(压缩版的js,去掉多余的空格字符。区别于阅读版)
    JS/CSS组合,减少连接数。(将多个JS和CSS的组合到一个请求里面去,一下子从服务端全部下载下来)
  4. 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);    //绑定	
	}

思考

为什么不能先处理缓存,再更新数据库呢?
为什么你的缓存更新策略是先更新数据库后删除缓存,讲讲其他的情况有什么问题?

问题:怎么保持缓存与数据库一致?

要解答这个问题,我们首先来看不一致的几种情况。我将不一致分为三种情况

数据库有数据,缓存没有数据;
数据库有数据,缓存也有数据,数据不相等;
数据库没有数据,缓存有数据。

策略:

  1. 首先尝试从缓存读取,读到数据则直接返回;如果读不到,就读数据库,并将数据会写到缓存,并返回。

  2. 需要更新数据时,先更新数据库,然后把缓存里对应的数据失效掉(删掉)。

读的逻辑大家都很容易理解,谈谈更新。如果不采取我提到的这种更新方法,你还能想到什么更新方法呢?大概会是:先删除缓存,然后再更新数据库。这么做引发的问题是,如果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
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。