SpringBoot秒殺系統實戰17-頁面優化技術(頁面緩存+URL緩存+對象緩存)...

文章目錄

頁面優化技術

1、 頁面緩存+URL緩存+對象緩存

由於併發瓶頸在數據庫,想辦法如何減少對數據庫的訪問,所以加若干緩存來提高,通過各種粒度的緩存,最大粒度頁面緩存到最小粒度的對象級緩存。

2、頁面靜態化,前後端分離

都是純的html,通過js或者ajax來請求服務器,如果做了靜態化,瀏覽器可以把html緩存在客戶端。

3、靜態資源優化

JS/CSS壓縮,減少流量。(壓縮版的js,去掉多餘的空格字符。區別於閱讀版)
JS/CSS組合,減少連接數。(將多個JS和CSS的組合到一個請求裏面去,一下子從服務端全部下載下來)

4、CDN優化

內容分發網絡,就近訪問。

緩存特徵:

命中率

當某個請求能夠通過訪問緩存而得到響應時,稱爲緩存命中。
緩存命中率越高,緩存的利用率也就越高。

最大空間

緩存通常位於內存中,內存的空間通常比磁盤空間小的多,因此緩存的最大空間不可能非常大。
當緩存存放的數據量超過最大空間時,就需要淘汰部分數據來存放新到達的數據。

淘汰策略

FIFO(First In First Out):先進先出策略,在實時性的場景下,需要經常訪問最新的數據,那麼就可以使用 FIFO,使得最先進入的數據(最晚的數據)被淘汰。

LRU(Least Recently Used):最近最久未使用策略,優先淘汰最久未使用的數據,也就是上次被訪問時間距離現在最久的數據。該策略可以保證內存中的數據都是熱點數據,也就是經常被訪問的數據,從而保證緩存命中率。

LFU(Least Frequently Used):最不經常使用策略,優先淘汰一段時間內使用次數最少的數據。

一般,頁面緩存和URL緩存時間比較短,適合場景:變化不大的頁面。如果分頁,不會全部緩存,一般緩存前一兩頁。

首先介紹:頁面緩存+URL緩存+對象緩存

頁面緩存

  1. 取緩存 (緩存裏面存的是html)
  2. 手動渲染模板
  3. 結果輸出(直接輸出html代碼)

GoodsController裏面的toListCache方法改造一下

/**
* 做頁面緩存的list頁面,防止同一時間訪問量巨大到達數據庫,如果緩存時間過長,數據及時性就不高。
 */
@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 代碼:

shaUserDao 代碼:

	@Mapper
	public interface MiaoshaUserDao {
	@Select("select * from miaosha_user where id=#{id}")  //這裏#{id}通過後面參數來爲其賦值
	public MiaoshaUser getById(@Param("id")long id);    //綁定
	
	//綁定在對象上面了----@Param("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
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯繫作者獲得授權並註明出處。

緩存問題

緩存穿透

指的是對某個一定不存在的數據進行請求,該請求將會穿透緩存到達數據庫。

解決方案:

  1. 對這些不存在的數據緩存一個空數據;
  2. 對這類請求進行過濾。

緩存雪崩

指的是由於數據沒有被加載到緩存中,或者緩存數據在同一時間大面積失效(過期),又或者緩存服務器宕機,導致大量的請求都到達數據庫。

在有緩存的系統中,系統非常依賴於緩存,緩存分擔了很大一部分的數據請求。當發生緩存雪崩時,數據庫無法處理這麼大的請求,導致數據庫崩潰。

解決方案:

  1. 爲了防止緩存在同一時間大面積過期導致的緩存雪崩,可以通過觀察用戶行爲,合理設置緩存過期時間來實現;
  2. 爲了防止緩存服務器宕機出現的緩存雪崩,可以使用分佈式緩存,分佈式緩存中每一個節點只緩存部分的數據,當某個節點宕機時可以保證其它節點的緩存仍然可用。
  3. 也可以進行緩存預熱,避免在系統剛啓動不久由於還未將大量數據進行緩存而導致緩存雪崩。

緩存一致性

緩存一致性要求數據更新的同時緩存數據也能夠實時更新。

解決方案:

  1. 在數據更新的同時立即去更新緩存;
  2. 在讀緩存之前先判斷緩存是否是最新的,如果不是最新的先進行更新。
  3. 要保證緩存一致性需要付出很大的代價,緩存數據最好是那些對一致性要求不高的數據,允許緩存數據存在一些髒數據。

緩存 “無底洞” 現象

指的是爲了滿足業務要求添加了大量緩存節點,但是性能不但沒有好轉反而下降了的現象。

產生原因:緩存系統通常採用 hash 函數將 key 映射到對應的緩存節點,隨着緩存節點數目的增加,鍵值分佈到更多的節點上,導致客戶端一次批量操作會涉及多次網絡操作,這意味着批量操作的耗時會隨着節點數目的增加而不斷增大。此外,網絡連接數變多,對節點的性能也有一定影響。

解決方案:

  1. 優化批量數據操作命令;
  2. 減少網絡通信次數;
  3. 降低接入成本,使用長連接 / 連接池,NIO 等。

轉發至:https://github.com/CyC2018/CS-Notes/blob/master/notes/緩存.md

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章