咖啡汪日誌——實際開發中如何避免快取穿透和快取雪崩(程式碼示例實際展示)
本汪作為一名資深的哈士奇
每天除了閒逛,拆家,就是啃部落格了
作為不是在戲精,就是在戲精的路上的二哈
今天就來給大家說說在實際工作中,如何預防快取穿透
一、 開篇有益
1、什麼是快取穿透?
通常快取系統,都是按照key去進行快取查詢,如果不存在對應的value,就應該去資料庫查詢。一些惡意的請求會故意大量查詢不存在的key(例如使用“-1”,“#”,或者UUID生成100萬個Key進行查詢),就會對資料庫造成很大的壓力。我們把這種情況稱之為快取穿透。
2.什麼是快取雪崩?
快取雪崩(快取失效)的兩個原因,從廣義上來講:
第一種,快取系統本身不可用,導致大量請求直接回源到資料庫
第二種,應用層面大量的 Key 在同一時間過期,導致大量的資料回源
3、快取穿透有什麼具體的防護方法?
(1)採用布隆過濾器,將所有可能存在的資料存到一個bitMap中,不存在的資料就會進行攔截。
(2)對查詢結果為空的情況也進行快取,快取時間設定短一點,不超過5分鐘。
4、如何有效避免快取雪崩?(失效時間擾動)
確保大量 Key , 不在同一時間過期:
簡單方案:差異化快取過期時間,不讓大量 Key 在同一時間過期。比如,在初始化快取的時候,設定快取的過期時間為 應設過期時間30min + 30秒以內的隨機延遲(擾動值)。(待支付訂單的有效時間均為30min,網際網路企業定律) ,這樣,這些 Key 不會在 30min 這個時刻過期,而是分散在 30min ~ 30min+30second 之間過期。
二、大家隨本汪,一起來看看實際工作中的程式碼實現
1、布隆過濾器,資料預載入,預防快取穿透。
(1)在基礎controller引入了bloomfilter
/**
* @author Yuezejian
* @date 2020年 08月22日 16:04:01
*/
public class AbstractController {
protected final Logger logger = LoggerFactory.getLogger(getClass());
/**
* 通用的基礎過濾器,如有使用,請在此註明
* 1.BloomFilter 內資料需要預熱(固定時間內有效token的過濾,正在使用此過濾器)
* The current three parties request the current system and need to obtain the token,
* which is using the filter {@Modified by yuezejian }
*/
protected static final BloomFilter<String> filter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), 1000,0.01);
}
(2)專案啟動進行資料載入
import com.slzhkj.smartworksite.model.dto.RequestDto;
import com.slzhkj.smartworksite.model.mapper.RequestMapper;
import com.slzhkj.smartworksite.server.controller.AbstractController;
import com.slzhkj.smartworksite.server.controller.ResponseController;
import com.slzhkj.smartworksite.server.controller.TokenAndSignController;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.Ordered;
import org.springframework.core.env.Environment;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* This loader is used for data preheating of redis and bloom filter.
* The current preheating data mainly includes:
* The first, Three parties request obtain time effective token {@link TokenAndSignController },
* When the bloom filter and redis are loaded,
* they need to be updated when I start the system
* The second,Three party request for data access,
* we judge the token {@link ResponseController} if it's enable
*
* @author Yuezejian
* @date 2020年 11月 10日 14:00:18
*/
@Component
public class RedisAndBloomFilterRecordRunner extends AbstractController implements ApplicationRunner, Ordered {
@Autowired
RequestMapper requestMapper;
@Autowired
RedisTemplate redisTemplate;
@Autowired
private Environment env;
@Override
public void run(ApplicationArguments args) throws Exception {
ValueOperations<String,String> tokenOpera = redisTemplate.opsForValue();
List<RequestDto> requestDtos = requestMapper.selectTokenRecord();
AtomicInteger count = new AtomicInteger(0);
requestDtos.stream()
.parallel()
.forEach( dto -> {
filter.put(dto.getToken());
filter.put(dto.getAppId());
if (filter.mightContain(dto.getToken())) {
count.getAndAdd(1);
}
String key = dto.getAppId();
//TODO:- Math.abs((System.currentTimeMillis() - dto.getUpdateTime().getTime())/60000 )
tokenOpera.set(key,dto.getToken(),env.getProperty("token.enable.time",Long.class), TimeUnit.MINUTES);
} );
logger.info("==========total is "+ count +", The data preloading of redis and bloom filter is completed!===========");
}
/**
* Makes the current class load with a higher priority
* @return
*/
@Override
public int getOrder() {
return 2;
}
}
(3)在合適的時機,將新資料加入bloomfilter
private void updateRedisAndBloomFilter(String appId, ValueOperations<String, String> tokenOpera,
String token, int res) {
if ( res > 0 ) {
//TODO: update redis ,insert BloomFilter
String key = appId;
tokenOpera.set(key,token,env.getProperty("token.enable.time",Long.class), TimeUnit.MINUTES);
filter.put(token);
filter.put(appId);
logger.info("appId為:{} 的使用者,更新了token: {}, 已存入Redis和基類布隆過濾器", appId, token);
} else {
logger.error("appId為:{} 的使用者,更新了token: {}, Database update success, but Redis or BloomFilter update fail!",appId,token);
throw new IllegalStateException("Database update success, but Redis or BloomFilter update fail!");
}
}
2、設定無效資料型別,預防快取穿透。每當發生快取穿透時,即快取和資料庫都沒有該條資料,在資料庫返回 null 後,也應該在快取中放置相應得 無效型別返回。
在Coupon 類中,加一個獲取ID 為“-1”的無效coupon 方法
/**
* <h2>返回一個無效的 Coupon 物件</h2>
* */
public static Coupon invalidCoupon() {
Coupon coupon = new Coupon();
coupon.setId(-1);
return coupon;
}
/** redis 客戶端,redis 的 key 肯定是 String 型別,而 StringRedisTemplate 是 value 也都是 String 的一個簡化 */
@Autowired
private StringRedisTemplate redisTemplate;
/**
* save List<Coupon> which are null to Cache
* 目的: 避免快取穿透
* @param userId user id
* @param status coupon status
*/
@Override
public void saveEmptyCouponListToCache(Long userId, List<Integer> status) {
log.info("Save empty list to cache for user: {}, status: {}",userId, JSON.toJSONString(status));
Map<String, String> invalidCouponMap = new HashMap<>();
invalidCouponMap.put("-1",JSON.toJSONString(Coupon.invalidCoupon()));
//使用 SessionCallback 把資料命令放入到 Redis 的 pipeline
//redis 的 pipeline 可以讓我們一次性執行多個命令,統一返回結果,而不用每一個命令去返回;
//redis 本身是單程式單執行緒的,你傳送一個命令,他給你一個返回,然後你才可以發生下一個命令給他。
// 單執行緒指的是網路請求模組使用了一個執行緒(所以不需考慮併發安全性),即一個執行緒處理所有網路請求,其他模組仍用了多個執行緒
//我們都知道Redis有兩種持久化的方式,一種是RDB,一種是AOF。
//拿RDB舉例,執行bgsave,就意味著 fork 出一個子程式在後臺進行備份。
//這也就為什麼執行完bgsave命令之後,還能對該Redis例項繼續其他的操作。
SessionCallback<Object> sessionCallback =
new SessionCallback<Object>() {
@Override
public Object execute(RedisOperations operations)
throws DataAccessException {
status.forEach( s -> {
//TODO: 把使用者 ID 和 優惠券使用 status 進行拼接,作為 redisKey
String redisKey = status2RedisKey(s,userId);
operations.opsForHash()
.putAll(redisKey,invalidCouponMap);
});
return null;
}
};
log.info("Pipeline exe result: {}",
JSON.toJSONString(sessionCallback));
}
/**
* Get Redis Key According to status
* @param status
* @param userId
* @return
*/
private String status2RedisKey(Integer status, Long userId) {
String redisKey = null;
CouponStatus couponStatus = CouponStatus.of(status);
switch (couponStatus) {
case USABLE:
redisKey = String.format("%s%s", Constant.RedisPrefix.USER_COUPON_USABLE, userId);
break;
case USED:
redisKey = String.format("%s%s",Constant.RedisPrefix.USER_COUPON_USED,userId);
break;
case EXPIRED:
redisKey = String.format("%s%s",Constant.RedisPrefix.USER_COUPON_EXPIRED,userId);
}
return redisKey;
}
當查詢快取沒有時,設定快取失效資料,返回空;有時,直接反序列化然後返回(存的時候,是coupon 的 JSON.toJSONString(coupon))
import org.apache.commons.collections4.CollectionUtils;
@Override
public List<Coupon> getCacheCoupons(Long userId, Integer status) {
log.info("Get Coupons From Cache: {}, {}", userId, status);
String redisKey = status2RedisKey(status,userId);
List<String> couponStrs = redisTemplate
.opsForHash().values(redisKey)
.stream().map( o -> Objects.toString(o,null))
.collect(Collectors.toList());
if (CollectionUtils.isEmpty(couponStrs)) {
saveEmptyCouponListToCache(userId,
Collections.singletonList(status));
return Collections.emptyList();
}
return couponStrs.stream().map(
cs -> JSON.parseObject(cs,Coupon.class))
s.collect(Collectors.toList());
}
3、時間擾動,預防快取雪崩
以秒為單位時,getRandomExpirationTime(1,2) 會返回1~2小時之間的隨機時間
redisTemplate.expire(redisKey, getRandomExpirationTime(1,2) , TimeUnit.SECONDS);
/**
* get one Random Expiration Time
* 快取雪崩:key 在同一時間失效
* @param min 最小小時數
* @param max 最大小時數
* @return 返回 【min, max】之間的隨機秒數
*/
private long getRandomExpirationTime(Integer min, Integer max) {
return RandomUtils.nextLong(min * 60 * 60, max * 60 * 60);
}
/**
* insert coupon to Cache
* @param userId
* @param coupons
* @return
*/
private Integer addCouponToCacheForUsable(Long userId, List<Coupon> coupons) {
// 如果 status 是 USABLE, 代表是新增的優惠券
// 只會影響到一個 cache : USER_COUPON_USABLE
log.debug("Add Coupon To Cache For Usable");
Map<String, String> needCacheObject = new HashMap<>();
coupons.forEach( coupon -> {
needCacheObject.put(coupon.getId().toString(),JSON.toJSONString(coupon));
});
String redisKey = status2RedisKey(
CouponStatus.USABLE.getCode(), userId
);
//TODO: redis 中的 Hash key 不能重複,needCacheObject 對應HashMap ,
// coupon id 不可能重複,所以直接 putAll 不會有問題
redisTemplate.opsForHash().putAll(redisKey, needCacheObject);
log.info("Add {} Coupons TO Cache: {} , {}", needCacheObject.size(), userId, redisKey);
//TODO: set Expiration Time, 1h - 2h ,random time
redisTemplate.expire(redisKey, getRandomExpirationTime(1,2) , TimeUnit.SECONDS);
return needCacheObject.size();
}
相關文章
- 程式碼解決快取穿透和快取雪崩問題快取穿透
- 快取穿透 快取雪崩快取穿透
- 快取穿透、快取擊穿、快取雪崩快取穿透
- 快取穿透、快取雪崩、快取擊穿快取穿透
- Redis快取穿透和雪崩Redis快取穿透
- Redis快取擊穿、快取穿透、快取雪崩Redis快取穿透
- [Redis]快取穿透/快取擊穿/快取雪崩Redis快取穿透
- 快取穿透、快取雪崩和快取擊穿是什麼?快取穿透
- 快取穿透、快取擊穿、快取雪崩、快取預熱快取穿透
- 快取問題(一) 快取穿透、快取雪崩、快取併發 核心概念快取穿透
- 快取穿透、快取擊穿、快取雪崩區別快取穿透
- 快取問題(四) 快取穿透、快取雪崩、快取併發 解決案例快取穿透
- Java高併發快取架構,快取雪崩、快取穿透之謎Java快取架構穿透
- 淺談快取寫法(一):快取的雪崩和穿透快取穿透
- Redis快取穿透,擊穿和雪崩Redis快取穿透
- 面試官:快取穿透、快取雪崩和快取擊穿是什麼?面試快取穿透
- Redis 快取雪崩,快取擊穿和快取穿透技術方案總結Redis快取穿透
- Redis快取穿透、快取雪崩、redis併發問題分析Redis快取穿透
- Redis詳解(十二)------ 快取穿透、快取擊穿、快取雪崩Redis快取穿透
- 什麼是redis快取雪崩、快取穿透、快取擊穿Redis快取穿透
- 快取穿透,快取擊穿,快取雪崩解決方案分析快取穿透
- Redis——快取穿透、快取擊穿、快取雪崩、分散式鎖Redis快取穿透分散式
- Redis快取穿透、快取雪崩、快取擊穿好好說說Redis快取穿透
- Redis快取穿透與雪崩Redis快取穿透
- 【面試普通人VS高手系列】說說快取雪崩和快取穿透的理解,以及如何避免?面試快取穿透
- 如何設計快取系統:快取穿透,快取擊穿,快取雪崩解決方案分析快取穿透
- 快取穿透、快取擊穿、快取雪崩概念及解決方案快取穿透
- 快取穿透、快取擊穿、快取雪崩區別和解決方案快取穿透
- 【Redis】快取穿透,快取擊穿,快取雪崩及解決方案Redis快取穿透
- REDIS快取穿透,快取擊穿,快取雪崩原因+解決方案Redis快取穿透
- Redis的快取穿透、快取雪崩、快取擊穿的區別Redis快取穿透
- 面試總結 —— Redis “快取穿透”、“快取擊穿”、“快取雪崩”面試Redis快取穿透
- Redis 快取穿透、快取雪崩原理及解決方案Redis快取穿透
- 什麼是redis的快取雪崩與快取穿透Redis快取穿透
- Redis 快取擊穿(失效)、快取穿透、快取雪崩怎麼解決?Redis快取穿透
- 快取穿透、快取擊穿、快取雪崩的場景以及解決方法快取穿透
- 十分鐘徹底掌握快取擊穿、快取穿透、快取雪崩快取穿透
- REDIS 快取的穿透,雪崩和熱點keyRedis快取穿透