一、緩存驚群
1、場(chǎng)景描述
用戶數(shù)據(jù)寫入和查詢,緩存設(shè)計(jì),寫入的時(shí)候,庫(kù)+緩存,雙寫,緩存默認(rèn) 2 天多隨機(jī)時(shí)間
的過(guò)期,讀的時(shí)候,讀延期,頻繁讀,緩存會(huì)不停的延期,沒(méi)有人查呢,過(guò)期,避免占用緩
存的空間,緩存沒(méi)查到,從數(shù)據(jù)庫(kù)里提出來(lái),放到緩存里去
每次寫入緩存的時(shí)候,為什么一定要設(shè)置 2 天+隨機(jī)幾個(gè)小時(shí)的時(shí)間呢?
答案,緩存驚群的問(wèn)題,緩存一批數(shù)據(jù),突然之間一起都沒(méi)了,過(guò)期時(shí)間設(shè)置的都是一樣的,
緩存集群都驚了,數(shù)據(jù)庫(kù)也驚了,大量緩存同時(shí)過(guò)期->驚群 ->瞬時(shí)大量請(qǐng)求走到 mysql 去
造成壓力
大量的緩存數(shù)據(jù),過(guò)期時(shí)間都是隨機(jī),不要集中在某個(gè)時(shí)間點(diǎn)一起過(guò)期
驚群,技術(shù)里典型的術(shù)語(yǔ),突然在某個(gè)時(shí)間點(diǎn),出了一個(gè)故障,一大片范圍線程/進(jìn)程/機(jī)器,
都同時(shí)被驚動(dòng)了,驚群效應(yīng)
2、解決方案
每次寫入緩存的時(shí)候,設(shè)置 2 天+隨機(jī)幾個(gè)小時(shí)的時(shí)間,使得緩存數(shù)據(jù)不會(huì)一起失效
代碼實(shí)現(xiàn)
redisCache.set(RedisKeyConstants.USER_INFO_PREFIX + cookbookUserDO.getId(),
JsonUtil.object2Json(cookbookUserDTO), CacheSupport.generateCacheExpireSecond());
/**
* 生成緩存過(guò)期時(shí)間
* 2天加上隨機(jī)幾小時(shí)
*
* @return
*/
static Integer generateCacheExpireSecond() {
return TWO_DAYS_SECONDS + RandomUtil.genRandomInt(0, 10) * 60 * 60;
}
二、緩存穿透
1、場(chǎng)景描述
緩存穿透的一個(gè)問(wèn)題,穿透->讀取緩存沒(méi)讀到->從 db 里讀->也沒(méi)讀到->bug->高并發(fā)的讀一個(gè)數(shù)據(jù),緩存和 db 都沒(méi)有->每次高并發(fā)讀取,緩存都會(huì)被穿透過(guò)去,每次都要讀一下db,導(dǎo)致高并發(fā)空請(qǐng)求都針對(duì) db 再走,壓力
2、解決方案
先從緩存中獲取數(shù)據(jù),如果緩存中不存在,則從數(shù)據(jù)庫(kù)中查出來(lái),如果數(shù)據(jù)庫(kù)中查出來(lái)的數(shù)據(jù)為空,則賦值為"{}",并且設(shè)置失效時(shí)間。
@Override
public CookbookUserDTO getUserInfo(CookbookUserQueryRequest request) {
Long userId = request.getUserId();
CookbookUserDTO user = getUserFromCache(userId);
if(user != null) {
return user;
}
return getUserInfoFromDB(userId);
}
private CookbookUserDTO getUserFromCache(Long userId) {
String userInfoKey = RedisKeyConstants.USER_INFO_PREFIX + userId;
String userInfoJsonString = redisCache.get(userInfoKey);
log.info("從緩存中獲取作者信息,userId:{},value:{}", userId, userInfoJsonString);
if (StringUtils.hasLength(userInfoJsonString)){
// 防止緩存穿透
if (Objects.equals(CacheSupport.EMPTY_CACHE, userInfoJsonString)) {
return new CookbookUserDTO();
}
redisCache.expire(RedisKeyConstants.USER_INFO_PREFIX + userId,
CacheSupport.generateCacheExpireSecond());
CookbookUserDTO dto = JsonUtil.json2Object(userInfoJsonString, CookbookUserDTO.class);
return dto;
}
return null;
}
private CookbookUserDTO getUserInfoFromDB(Long userId) {
// 有兩個(gè)選擇,load from db + write redis,加兩把鎖,user_lock,user_update_lock
// 基于redisson,加多鎖,multi lock
// 共用一把鎖,multi lock加鎖,不同的鎖應(yīng)對(duì)的是不同的并發(fā)場(chǎng)景
// String userLockKey = RedisKeyConstants.USER_LOCK_PREFIX + userId;
// 有大量的線程突然讀一個(gè)冷門的用戶數(shù)據(jù),都囤積在這里,在上面大家都沒(méi)讀到
// 都在這個(gè)地方在排隊(duì)等待獲取鎖,然后去嘗試load db + write redis
// 非常嚴(yán)重的鎖競(jìng)爭(zhēng)的問(wèn)題,線程,串行化,一個(gè)一個(gè)的排隊(duì),一個(gè)人先拿鎖,load一次db,寫緩存
// 下一個(gè)人拿到鎖了,通過(guò)double check,直接讀緩存,下一個(gè)人,短時(shí)間內(nèi)突然有一個(gè)嚴(yán)重串行化,雖然每次讀緩存,時(shí)間不多
// 其實(shí)只要有第一個(gè)人,能夠拿到鎖,進(jìn)去,laod db + wreite redis,redis里就已經(jīng)有數(shù)據(jù)了
// 后續(xù)的線程就不需要通過(guò)鎖排隊(duì),串行化,一個(gè)一個(gè)load redis里的數(shù)據(jù)
// 只要有一個(gè)人能夠成功,其他的人,其實(shí)可以突然之間全部轉(zhuǎn)換為上面的操作,無(wú)鎖的情況下,大量的一起并發(fā)的讀redis就可以了
String userLockKey = RedisKeyConstants.USER_UPDATE_LOCK_PREFIX + userId;
boolean lock = false;
try {
lock = redisLock.tryLock(userLockKey, USER_UPDATE_LOCK_TIMEOUT);
} catch(InterruptedException e) {
CookbookUserDTO user = getUserFromCache(userId);
if(user != null) {
return user;
}
log.error(e.getMessage(), e);
throw new BaseBizException("查詢失敗");
}
if (!lock) {
CookbookUserDTO user = getUserFromCache(userId);
if(user != null) {
return user;
}
log.info("緩存數(shù)據(jù)為空,從數(shù)據(jù)庫(kù)查詢作者信息時(shí)獲取鎖失敗,userId:{}", userId);
throw new BaseBizException("查詢失敗");
}
try {
CookbookUserDTO user = getUserFromCache(userId);
if(user != null) {
return user;
}
log.info("緩存數(shù)據(jù)為空,從數(shù)據(jù)庫(kù)中獲取數(shù)據(jù),userId:{}", userId);
String userInfoKey = RedisKeyConstants.USER_INFO_PREFIX + userId;
// 在這里先讀到了db里的用戶信息的舊數(shù)據(jù)
// 這個(gè)線程剛剛讀到,還沒(méi)有來(lái)得及把舊數(shù)據(jù)寫入緩存里去
CookbookUserDO cookbookUserDO = cookbookUserDAO.getById(userId);
if (Objects.isNull(cookbookUserDO)) {
redisCache.set(userInfoKey, CacheSupport.EMPTY_CACHE, CacheSupport.generateCachePenetrationExpireSecond());
return null;
}
CookbookUserDTO dto = cookbookUserConverter.convertCookbookUserDTO(cookbookUserDO);
// 此時(shí)這個(gè)線程,在上面的那個(gè)線程都已經(jīng)把新數(shù)據(jù)寫入緩存里去了,緩存里已經(jīng)是最新數(shù)據(jù)了
// 把舊數(shù)據(jù)庫(kù),寫入了緩存做了一個(gè)覆蓋操作,典型的,數(shù)據(jù)庫(kù)+緩存雙寫的時(shí)候,寫和讀,并發(fā)的時(shí)候
// db里是新數(shù)據(jù),緩存里是舊數(shù)據(jù),舊數(shù)據(jù)是覆蓋了新數(shù)據(jù)的
// db和緩存,數(shù)據(jù)是不一致的
redisCache.set(userInfoKey, JsonUtil.object2Json(dto), CacheSupport.generateCacheExpireSecond());
return dto;
} finally {
redisLock.unlock(userLockKey);
}
}
三、緩存一致性
1、場(chǎng)景描述
如果數(shù)據(jù)庫(kù)中的某條數(shù)據(jù),放入緩存之后,又立馬被更新了,那么該如何更新緩存呢?不更新緩存行不行?
答:當(dāng)然不行,如果不更新緩存,在很長(zhǎng)的一段時(shí)間內(nèi)(決定于緩存的過(guò)期時(shí)間),用戶請(qǐng)求從緩存中獲取到的都可能是舊值,而非數(shù)據(jù)庫(kù)的最新值。這不是有數(shù)據(jù)不一致的問(wèn)題?
2、解決方案
