緩存雪崩,緩存穿透,緩存擊穿出現(xiàn)的原因及解決方案?
緩存雪崩
出現(xiàn)過(guò)程
假設(shè)有如下一個(gè)系統(tǒng),高峰期請(qǐng)求為5000次/秒,4000次走了緩存,只有1000次落到了數(shù)據(jù)庫(kù)上,數(shù)據(jù)庫(kù)每秒1000的并發(fā)是一個(gè)正常的指標(biāo),完全可以正常工作,但如果緩存宕機(jī)了,或者緩存設(shè)置了相同的過(guò)期時(shí)間,導(dǎo)致緩存在同一時(shí)刻同時(shí)失效,每秒5000次的請(qǐng)求會(huì)全部落到數(shù)據(jù)庫(kù)上,數(shù)據(jù)庫(kù)立馬就死掉了,因?yàn)閿?shù)據(jù)庫(kù)一秒最多抗2000個(gè)請(qǐng)求,如果DBA重啟數(shù)據(jù)庫(kù),立馬又會(huì)被新的請(qǐng)求打死了,這就是緩存雪崩。
解決方法
- 事前:redis高可用,主從+哨兵,redis cluster,避免全盤(pán)崩潰
- 事中:本地ehcache緩存 + hystrix限流&降級(jí),避免MySQL被打死
- 事后:redis持久化RDB+AOF,快速恢復(fù)緩存數(shù)據(jù)
- 緩存的失效時(shí)間設(shè)置為隨機(jī)值,避免同時(shí)失效
緩存穿透
出現(xiàn)過(guò)程
假如客戶(hù)端每秒發(fā)送5000個(gè)請(qǐng)求,其中4000個(gè)為黑客的惡意攻擊,即在數(shù)據(jù)庫(kù)中也查不到。舉個(gè)例子,用戶(hù)id為正數(shù),黑客構(gòu)造的用戶(hù)id為負(fù)數(shù),如果黑客每秒一直發(fā)送這4000個(gè)請(qǐng)求,緩存就不起作用,數(shù)據(jù)庫(kù)也很快被打死。
解決方法
- 對(duì)請(qǐng)求參數(shù)進(jìn)行校驗(yàn),不合理直接返回
- 查詢(xún)不到的數(shù)據(jù)也放到緩存,value為空,如 set -999 ""
- 使用布隆過(guò)濾器,快速判斷key是否在數(shù)據(jù)庫(kù)中存在,不存在直接返回
緩存擊穿
出現(xiàn)過(guò)程
設(shè)置了過(guò)期時(shí)間的key,承載著高并發(fā),是一種熱點(diǎn)數(shù)據(jù)。從這個(gè)key過(guò)期到重新從MySQL加載數(shù)據(jù)放到緩存的一段時(shí)間,大量的請(qǐng)求有可能把數(shù)據(jù)庫(kù)打死。緩存雪崩是指大量緩存失效,緩存擊穿是指熱點(diǎn)數(shù)據(jù)的緩存失效
解決方法
- 設(shè)置key永遠(yuǎn)不過(guò)期,或者快過(guò)期時(shí),通過(guò)另一個(gè)異步線(xiàn)程重新設(shè)置key
- 當(dāng)從緩存拿到的數(shù)據(jù)為null,重新從數(shù)據(jù)庫(kù)加載數(shù)據(jù)的過(guò)程上鎖,下面寫(xiě)個(gè)分布式鎖實(shí)現(xiàn)的demo
Redis實(shí)現(xiàn)分布式鎖
我之前的文章寫(xiě)到了Redis實(shí)現(xiàn)分布式鎖的原理,這里就不再詳細(xì)概述了
在Redis中使用簡(jiǎn)單強(qiáng)大的Lua腳本
1.加鎖執(zhí)行命令
- SET resource_name random_value NX PX 30000
2.解鎖執(zhí)行腳本
- if redis.call("get", KEYS[1]) == ARGV[1] then
- return redis.call("del", KEYS[1])
- else
- return 0
- end
寫(xiě)一個(gè)分布式鎖工具類(lèi)
- public class LockUtil {
- private static final String OK = "OK";
- private static final Long LONG_ONE = 1L;
- private static final String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
- public static boolean tryLock(String key, String value, long expire) {
- Jedis jedis = RedisPool.getJedis();
- SetParams setParams = new SetParams();
- setParams.nx().px(expire);
- return OK.equals(jedis.set(key, value, setParams));
- }
- public static boolean releaseLock(String key, String value) {
- Jedis jedis = RedisPool.getJedis();
- return LONG_ONE.equals(jedis.eval(script, 1, key, value));
- }
- }
工具類(lèi)寫(xiě)起來(lái)還是挺簡(jiǎn)單的
示例代碼
- public String getData(String key) {
- String lockKey = "key";
- String lockValue = String.valueOf(System.currentTimeMillis());
- long expireTime = 1000L;
- String value = getFromRedis(key);
- if (value == null) {
- if (LockUtil.tryLock(lockKey, lockValue, expireTime)) {
- // 從數(shù)據(jù)庫(kù)取值并放到redis中
- LockUtil.releaseLock(lockKey, lockValue);
- } else {
- // sleep一段時(shí)間再?gòu)木彺嬷心?nbsp;
- Thread.sleep(100);
- getFromRedis(key);
- }
- }
- return value;
- }