版權聲明:本文爲博主原創文章,未經博主允許不得轉載。 https://blog.csdn.net/oSunXu/article/details/78356560
緩存雪崩:因爲緩存失效(key生存時間到期)導致所有請求都去查找數據庫,導致數據庫CPU和內存負載過高導致宕機。
緩存雪崩原因及解決方案:
使用緩存主要解決數據同步,並減少對數據庫訪問次數。因此,通常解決方案往往是使用互斥鎖,讓一個線程訪問數據庫,並將數據更新到緩存中,其他線程訪問緩存中數據。如果是基於jvm鎖機制的話,只能解決單機問題,也就是隻讓本機一個線程訪問緩存,但是分佈式條件下是不能使用的。所以,要基於緩存的分佈式鎖來實現。
以redis爲例解釋下實現分佈式鎖的原理:
獲取鎖:
所有線程操作一個共同的key比如:lock,如果redis中不存在key爲lock的值,那麼當前線程獲取鎖併爲lock設置一個隨機值。
如果lock已經存在了,說明已經有線程獲取鎖,該線程不能再獲取了。
釋放鎖:
獲取鎖的線程操作執行完畢後,清除lock的值,這樣鎖就釋放了。所以,對鎖的操作就是通過對同一個key值的添加和刪除操作。
代碼:
-
@Service
-
public class RedisLock implements Lock {
-
@Autowired
-
private JedisConnectionFactory factory;
-
private static final String LOCK="lock";
-
private ThreadLocal<String> local=new ThreadLocal<String>();
-
//獲取鎖
-
@Override
-
public boolean tryLock() {
-
//獲取Jedis的原始數據連接
-
Jedis jedis = (Jedis)factory.getConnection().getNativeConnection();
-
String uuid = UUID.randomUUID().toString();
-
/** 獲取鎖:設置一個隨機值,超期時間1s
-
String key, String value, String nxxx, String expx, int time)
-
nxxx: NX:key不存在時設值 XX:key存在時設值
-
expx: EX|PX, expire time units: EX = seconds; PX = milliseconds
-
*/
-
String ret = jedis.set(LOCK, uuid, "NX", "PX", 1000);
-
if(!StringUtils.isEmpty(ret)&&ret.equals("OK")){
-
local.set(uuid);
-
return true;
-
}
-
return false;
-
}
-
/**
-
* 解鎖
-
*/
-
@Override
-
public void unlock() {
-
String script=null;
-
try {
-
script=FileCopyUtils.copyToString(new FileReader(ResourceUtils.getFile("classpath:cn/rjx/spring/cache/unlock.c")));
-
} catch (IOException e) {
-
e.printStackTrace();
-
}
-
Jedis jedis = (Jedis)factory.getConnection().getNativeConnection();
-
List<String> keys=new ArrayList<String>();
-
keys.add(LOCK);
-
List<String> args=new ArrayList<String>();
-
args.add(local.get());
-
//如果redis中的 lock值和當前線程的uuid值相等,刪除Key值
-
jedis.eval(script, keys, args);
-
}
-
}
刪除鍵值是執行的腳本unlock.c:
-
if redis.call("get",KEYS[1])==ARGV[1] then
-
return redis.call("del",KEYS[1])
-
else
-
return 0
-
end
操作緩存的具體流程:
1.當線程查詢某一值時先查看緩存是否存在該值。
2.如果存在直接返回主緩存中的數據。
3.1如果不存在,只有一個線程獲取鎖並去數據庫讀取數據,讀取後更新主緩存和備份緩存。
3.2 其他線程取備份緩存中的數據。.
代碼實現:初始時,主緩存和備份緩存爲空,此時可能會有線程獲取的值爲空,但是並不影響用戶體驗,用戶可以再刷新一次。在要求比較高的場景裏面,可以考慮先把數據寫入緩存中,可以搭配定時刷新緩存的機制。
-
public List<Integer> queryCountByLeiMu() {
-
List<Integer> cacheResult = cacheService.cacheResult("101", "leimu");
-
if(cacheResult!=null){
-
logger.info("================get cache=======================");
-
return cacheResult;
-
}
-
if(lock.tryLock()){
-
logger.info("================get db=======================");
-
List<Integer> list=empDao.queryCountByLeiMu();
-
cacheService.cachePut("101", list, "leimu");//主緩存
-
cacheService.cachePut("beifen101", list, "beifenleimu");//備份緩存
-
lock.unlock();
-
return list;
-
}else{
-
logger.info("================get BEIFEN=======================");
-
//備份中拿
-
return cacheService.cacheResult("beifen101", "beifenleimu");
-
}
-
}
數據同步問題:主緩存中key的過期時間比較短,這樣保證儘可能獲取新數據。
bean.xml中緩存失效時間設置:
-
<!-- 開啓緩存註解掃描 -->
-
<cache:annotation-driven />
-
<bean id="cacheManager" class="org.springframework.data.redis.cache.RedisCacheManager">
-
<constructor-arg index="0" ref="redisTemplate"></constructor-arg>
-
<property name="expires">
-
<map>
-
<entry key="leimu" value="5"></entry>
-
<entry key="beifenleimu" value="100"></entry>
-
</map>
-
</property>
-
</bean>
測試方法模擬高併發情景:
-
@Autowired
-
LeiMuService leiMuService;
-
private static final int threadNum=13;
-
//倒計數器(發令槍) 用於製造線程併發執行
-
private static CountDownLatch cdl=new CountDownLatch(threadNum);
-
/**
-
* 模擬高併發條件下,數據庫查詢耗時比較長
-
* @throws InterruptedException
-
*/
-
@Test
-
public void test04() throws InterruptedException{
-
for(int i=0;i<threadNum;i++){
-
new Thread(new UserRequest()).start();
-
cdl.countDown();//threadNum每次減1,到零時同時執行cdl.await();後邊代碼
-
}
-
//主線程掛起,等子線程執行完以後
-
Thread.currentThread().join();
-
}
-
private class UserRequest implements Runnable{
-
@Override
-
public void run() {
-
//所有子線程在這裏等待,當所有線程實例化後,同時停止等待
-
try {
-
cdl.await();
-
} catch (InterruptedException e) {
-
e.printStackTrace();
-
}
-
//N個子線程同時調用獲取類目
-
List<Integer> leimu = leiMuService.queryCountByLeiMu();
-
logger.info(Thread.currentThread().getName()+"==========================================>"+leimu.size());
-
}
-
}
-
}
缺點:1.非阻塞,短時間不能保證數據一致性
2.鎖失效時間難把握,一般爲單線程處理時長的兩到三倍
3.可能出現鎖失效情況
4******不能在redis集羣環境中使用(集羣中可用redLock)
建議使用基於zookeeper的分佈式鎖實現方式!!.