在游戲江門服務(wù)器開發(fā)中,為了更快速的獲取游戲玩家的數(shù)據(jù),一般都會把數(shù)據(jù)存儲在Redis之中,做為一級緩存。數(shù)據(jù)加載的過程是一般是這樣的:
先從Redis中獲取數(shù)據(jù)庫。如果有數(shù)據(jù)直接返回,不用再查詢數(shù)據(jù)庫。
如果Redis沒有數(shù)據(jù),再查詢數(shù)據(jù)庫,將查詢到的數(shù)據(jù)先緩存到Redis一份,再返回給調(diào)用者。
如果數(shù)據(jù)庫也沒有數(shù)據(jù),直接返回null。
我在剛開始使用Redis做游戲江門服務(wù)器緩存的時候,就是這樣做的。但是隨著工作經(jīng)驗(yàn)的積累和解決的Bug越來越多,如果代碼實(shí)現(xiàn)的不夠慎密,還是會出現(xiàn)各種問題的,而且現(xiàn)在面試官也喜歡問關(guān)于緩存的問題。做為做緩存,一般要解決的就是兩個大問題。
緩存穿透
所謂的緩存穿透就是發(fā)生在第一步上面:先從緩存查詢,如果緩存不存在,再查詢數(shù)據(jù)庫。這個時候,如果某些數(shù)據(jù)是一份公共數(shù)據(jù),很多游戲玩家并發(fā)來查詢,都去redis查了一下,發(fā)現(xiàn)沒有,就都去數(shù)據(jù)庫查。這個時候,壓力就全部在數(shù)據(jù)庫上面了。如果數(shù)據(jù)庫扛著住還好,如果扛不住,就有可能導(dǎo)致數(shù)據(jù)庫超載。
解決緩存穿透的方法也很簡單,在收到第一次查詢redis時,如果redis查詢出來為空,這個時間對從數(shù)據(jù)庫查詢的操作加鎖,如果從數(shù)據(jù)庫查出來了,并緩存到了redis之中,這時別的查詢操作就可以從redis中獲取數(shù)據(jù)了。如果從數(shù)據(jù)庫查出來也是空的,這個時候,可以給redis提供一個默認(rèn)值,這樣,其它查詢出來的值就是這個默認(rèn)值 ,如果判斷是默認(rèn)值,表示不有數(shù)據(jù),返回null,這樣也不會再查數(shù)據(jù)庫了。
緩存雪崩(緩存擊穿)
這種現(xiàn)象也是出現(xiàn)在大并發(fā)的情景下。比如Redis緩存了很多玩家的活動數(shù)據(jù),但是這一大批玩家很長時間都沒有登錄了,而在redis中的活躍數(shù)據(jù)已過期,被redis自動刪除了。突然間,運(yùn)營又搞了一個老玩家拉回的活動,很多長時間不登錄的人又都回來登錄游戲了,這個時候,一大批用戶的活動數(shù)據(jù)需要會從數(shù)據(jù)庫拉取。極端情況導(dǎo)致數(shù)據(jù)庫超載。
解決這個問題的方法
緩存的過期時間可以稍微長一些,長時間真正流失的玩家可能也不會回來了。
對數(shù)據(jù)庫的操作需要添加限流,這個一般的數(shù)據(jù)庫連接池已經(jīng)做了,可以指定同一時間內(nèi),最多有多少連接操作數(shù)據(jù)庫。
提供一個防止緩存穿透的方法
package com.mygame.redis;
import java.time.Duration;
import java.util.function.Function;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
/**
*
* @ClassName: RedisCacheTemplate
* @Description: 這是一個redis緩存的模板。在redis做為緩存的時候,需要防止緩存的雪崩,穿透
* @author: wang guang shuai
* @date: 2020年1月9日 下午5:11:53
*/
@Service
public class RedisCacheTemplate {
private static final String DefaultRedisNullValue = "#-#";
@Autowired
private StringRedisTemplate redisTemplate;
/**
*
* <p>
* Description:第一次會從redis中獲取,如果redis中沒有此值,從db中獲取
* </p>
*
* @param redisKey
* @param param
* @param duration
* @param selectFromDB
* @return
* @author wang guang shuai
* @date 2020年1月9日 下午8:07:52
*
*/
public String getValue(String redisKey, String param, Duration duration, Function<String, String> selectFromDB) {
String value = redisTemplate.opsForValue().get(redisKey);
if (value == null) {
// 加鎖,防止緩存穿透和擊穿
synchronized (redisKey.intern()) {
// 二次檢測
value = redisTemplate.opsForValue().get(redisKey);
if (value == null) {// 如果等于空,從數(shù)據(jù)庫取
value = selectFromDB.apply(param);
if (value == null) {// 如果數(shù)據(jù)庫還是沒有,說明是真的沒有,添加空標(biāo)記
value = DefaultRedisNullValue;
}
// 將取到的值緩存到redis中。
if (duration != null) {
redisTemplate.opsForValue().set(redisKey, value, duration);
} else {
redisTemplate.opsForValue().set(redisKey, value);
}
}
}
}
if (value.equals(DefaultRedisNullValue)) {
return null;
}
return value;
}
}
以上文章來源于網(wǎng)絡(luò),如有侵權(quán)請聯(lián)系創(chuàng)一網(wǎng)的客服處理。謝謝!