在業務初始階段,流量很少的情況下,通過直接運算元據是可行的操作,但是隨著業務量的增長,使用者的存取量也隨之增加,在該階段自然需要使用一些手段(快取)來減輕資料庫的壓力;所謂遇事不決,那就加一層。
在當前技術棧中,redis當屬快取的第一梯隊了,但是隨著快取的引入,業務架構和問題也隨之而來。
快取好處:
快取成本:
記憶體淘汰:
redis自動進行,當redis記憶體達到咱們設定的max-memery的時候,會自動觸發淘汰機制,淘汰掉一些不重要的資料(可以自己設定策略方式)
寶塔redis設定圖:
超時剔除:當我們給redis設定了過期時間ttl之後,redis會將超時的資料進行刪除,方便咱們繼續使用快取
主動更新:我們可以手動呼叫方法把快取刪掉,通常用於解決快取和資料庫不一致問題
業務場景:
刪除快取還是更新快取?
如何保證快取與資料庫的操作的同時成功或失敗?
先操作快取還是先運算元據庫?
結論:先運算元據庫,在操作快取
第一種(淘汰):
假設執行緒1先來,他先把快取刪了,此時執行緒2過來,他查詢快取資料並不存在,此時他寫入快取,當他寫入快取後,執行緒1再執行更新動作時,實際上寫入的就是舊的資料,新的資料被舊資料覆蓋了。
第二種:也會出現一個時差的問題,但是需要滿足以下條件
兩個讀寫執行緒同時存取
快取剛好失效(查詢未命中)
線上程一寫入快取的時間內,執行緒二要完成資料庫的更新和刪除快取
以上擇優原則先運算元據後刪除快取的
該場景實現流程:以下分析結合部分程式碼(聚焦於redis的實現);
完整後端程式碼可在Github中獲取:https://github.com/xbhog/hm-dianping
開發流程:
【查詢店鋪快取流程】
從redis中查詢店鋪資訊
查詢資料庫
結果為空:店鋪資訊不存在
設定店鋪快取
public Result queryById(Long id) {
//從redis查詢商鋪資訊
String shopInfo = stringRedisTemplate.opsForValue().get(SHOP_CACHE_KEY + id);
//命中快取,返回店鋪資訊
if(StrUtil.isNotBlank(shopInfo)){
Shop shop = JSONUtil.toBean(shopInfo, Shop.class);
return Result.ok(shop);
}
//未命中快取
Shop shop = getById(id);
if(Objects.isNull(shop)){
return Result.fail("店鋪不存在");
}
//物件轉字串
stringRedisTemplate.opsForValue().set(SHOP_CACHE_KEY+id,JSONUtil.toJsonStr(shop),30L, TimeUnit.MINUTES);
return Result.ok(shop);
}
在設定店鋪快取的時候,設定了失效時間(保證快取的利用率)---滿足高一致性需求:主動更新,並以超時剔除作為兜底方案;
然後在後臺修改店鋪資訊的時候,先修改資料庫,然後刪除快取;
@Override
@Transactional
public Result updateShopById(Shop shop) {
Long id = shop.getId();
if(ObjectUtil.isNull(id)){
return Result.fail("====>店鋪ID不能為空");
}
log.info("====》開始更新資料庫");
//更新資料庫
updateById(shop);
stringRedisTemplate.delete(SHOP_CACHE_KEY + id);
return Result.ok();
}
這裡有一個點,在方法上設定事務,當資料庫更新成功,刪除快取(相當於更新快取);因為這裡刪除快取後,下次存取店鋪資訊的時候,查詢資料庫會重新建立快取。
雖然上述刪除快取的不管在前還是後面流程異常,都不會影響快取的使用。但是不是雙方一致,而是有所取捨(舍的快取);
保證資料庫和快取都一致的方式:
重試:****無論是先操作快取,還是先運算元據庫,但凡後者執行失敗了,我們就可以發起重試,儘可能地去做「補償」。
完整後端程式碼可在Github中獲取:https://github.com/xbhog/hm-dianping
RocketMQ叢集的搭建和使用:https://www.cnblogs.com/xbhog/p/17003037.html
在上面店鋪資訊修改的時候,我們更新了資料庫後刪除redis快取,為了避免第二步的執行失敗,我們將redis的操作放到訊息佇列中,由消費者來操作快取。
參照:
至於寫佇列失敗和訊息佇列的維護成本問題:
設定pom.xml和application.yaml
<rocketmq-spring-boot-starter-version>2.0.3</rocketmq-spring-boot-starter-version>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.9.3</version>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>${rocketmq-spring-boot-starter-version}</version>
</dependency>
rocketmq:
name-server: xxx.xxx.xxx.174:9876;xxx.xxx.xxx.246:9876
producer:
group: shopDataGroup
在更新店鋪的操作中引入MQ,非同步傳送資訊:
@Override
@Transactional
public Result updateShopById(Shop shop) {
Long id = shop.getId();
if(ObjectUtil.isNull(id)){
return Result.fail("====>店鋪ID不能為空");
}
log.info("====》開始更新資料庫");
//更新資料庫
updateById(shop);
String shopRedisKey = SHOP_CACHE_KEY + id;
Message message = new Message(TOPIC_SHOP,"shopRe",shopRedisKey.getBytes());
//非同步傳送MQ
try {
rocketMQTemplate.getProducer().send(message);
} catch (Exception e) {
log.info("=========>傳送非同步訊息失敗:{}",e.getMessage());
}
//stringRedisTemplate.delete(SHOP_CACHE_KEY + id);
//int i = 1/0; 驗證異常流程後,
return Result.ok();
}
設定消費者監聽器:
package com.hmdp.mq;
/**
* @author xbhog
* @describe:
* @date 2022/12/21
*/
@Slf4j
@Component
@RocketMQMessageListener(topic = TOPIC_SHOP,consumerGroup = "shopRe",
messageModel = MessageModel.CLUSTERING)
public class RocketMqNessageListener implements RocketMQListener<MessageExt> {
@Resource
private StringRedisTemplate stringRedisTemplate;
@SneakyThrows
@Override
public void onMessage(MessageExt message) {
log.info("========>非同步消費開始");
String body = null;
body = new String(message.getBody(), "UTF-8");
stringRedisTemplate.delete(body);
int reconsumeTimes = message.getReconsumeTimes();
log.info("======>重試次數{}",reconsumeTimes);
if(reconsumeTimes > 3){
log.info("消費失敗:{}",body);
return;
}
throw new RuntimeException("模擬異常丟擲");
}
}
檢視重試結果:
====》開始更新資料庫
36:29.174 DEBUG 69636 --- [nio-8081-exec-2] com.hmdp.mapper.ShopMapper.updateById : ==> Preparing: UPDATE tb_shop SET name=?, type_id=?, area=?, address=?, avg_price=?, sold=?, comments=?, score=?, open_hours=? WHERE id=?
36:29.192 DEBUG 69636 --- [nio-8081-exec-2] com.hmdp.mapper.ShopMapper.updateById : ==> Parameters: 102茶餐廳(String), 1(Long), 大關(String), 金華路錦昌文華苑29號(String), 80(Long), 4215(Integer), 3035(Integer), 37(Integer), 10:00-22:00(String), 1(Long)
36:29.301 DEBUG 69636 --- [nio-8081-exec-2] com.hmdp.mapper.ShopMapper.updateById : <== Updates: 1
36:29.744 INFO 69636 --- [Thread_shopRe_1] com.hmdp.mq.RocketMqNessageListener : ========>非同步消費開始
36:30.011 INFO 69636 --- [Thread_shopRe_1] com.hmdp.mq.RocketMqNessageListener : ======>重試次數0
36:30.014 WARN 69636 --- [Thread_shopRe_1] a.r.s.s.DefaultRocketMQListenerContainer : consume message failed. messageExt:.......
java.lang.RuntimeException: 模擬異常丟擲
.......
36:42.636 INFO 69636 --- [Thread_shopRe_2] com.hmdp.mq.RocketMqNessageListener : ========>非同步消費開始
36:42.689 INFO 69636 --- [Thread_shopRe_2] com.hmdp.mq.RocketMqNessageListener : ======>重試次數1
36:42.689 WARN 69636 --- [Thread_shopRe_2] a.r.s.s.DefaultRocketMQListenerContainer : consume message failed. messageExt:.......
java.lang.RuntimeException: 模擬異常丟擲
.......
37:12.764 INFO 69636 --- [Thread_shopRe_3] com.hmdp.mq.RocketMqNessageListener : ========>非同步消費開始
37:12.820 INFO 69636 --- [Thread_shopRe_3] com.hmdp.mq.RocketMqNessageListener : ======>重試次數2
37:12.821 WARN 69636 --- [Thread_shopRe_3] a.r.s.s.DefaultRocketMQListenerContainer : consume message failed. messageExt:MessageExt .......
java.lang.RuntimeException: 模擬異常丟擲
.......
38:12.896 INFO 69636 --- [Thread_shopRe_4] com.hmdp.mq.RocketMqNessageListener : ========>非同步消費開始
38:12.960 INFO 69636 --- [Thread_shopRe_4] com.hmdp.mq.RocketMqNessageListener : ======>重試次數3
38:12.960 WARN 69636 --- [Thread_shopRe_4] a.r.s.s.DefaultRocketMQListenerContainer : consume message failed. messageExt:MessageExt .......
java.lang.RuntimeException: 模擬異常丟擲
.......
40:13.045 INFO 69636 --- [Thread_shopRe_5] com.hmdp.mq.RocketMqNessageListener : ========>非同步消費開始
40:13.110 INFO 69636 --- [Thread_shopRe_5] com.hmdp.mq.RocketMqNessageListener : ======>重試次數4
40:13.110 INFO 69636 --- [Thread_shopRe_5] com.hmdp.mq.RocketMqNessageListener : 消費失敗:cache:shop:1