redis分佈式鎖java實現解決緩存雪崩

 

版權聲明:本文爲博主原創文章,未經博主允許不得轉載。 https://blog.csdn.net/oSunXu/article/details/78356560

緩存雪崩:因爲緩存失效(key生存時間到期)導致所有請求都去查找數據庫,導致數據庫CPU和內存負載過高導致宕機。

緩存雪崩原因及解決方案:

使用緩存主要解決數據同步,並減少對數據庫訪問次數。因此,通常解決方案往往是使用互斥鎖,讓一個線程訪問數據庫,並將數據更新到緩存中,其他線程訪問緩存中數據。如果是基於jvm鎖機制的話,只能解決單機問題,也就是隻讓本機一個線程訪問緩存,但是分佈式條件下是不能使用的。所以,要基於緩存的分佈式鎖來實現。

 

以redis爲例解釋下實現分佈式鎖的原理:

獲取鎖:

所有線程操作一個共同的key比如:lock,如果redis中不存在key爲lock的值,那麼當前線程獲取鎖併爲lock設置一個隨機值。

如果lock已經存在了,說明已經有線程獲取鎖,該線程不能再獲取了。

釋放鎖:

獲取鎖的線程操作執行完畢後,清除lock的值,這樣鎖就釋放了。所以,對鎖的操作就是通過對同一個key值的添加和刪除操作。

代碼:

 


 
  1. @Service

  2. public class RedisLock implements Lock {

  3. @Autowired

  4. private JedisConnectionFactory factory;

  5.  
  6. private static final String LOCK="lock";

  7.  
  8. private ThreadLocal<String> local=new ThreadLocal<String>();

  9.  
  10. //獲取鎖

  11. @Override

  12. public boolean tryLock() {

  13. //獲取Jedis的原始數據連接

  14. Jedis jedis = (Jedis)factory.getConnection().getNativeConnection();

  15. String uuid = UUID.randomUUID().toString();

  16. /** 獲取鎖:設置一個隨機值,超期時間1s

  17. String key, String value, String nxxx, String expx, int time)

  18. nxxx: NX:key不存在時設值 XX:key存在時設值

  19. expx: EX|PX, expire time units: EX = seconds; PX = milliseconds

  20. */

  21. String ret = jedis.set(LOCK, uuid, "NX", "PX", 1000);

  22. if(!StringUtils.isEmpty(ret)&&ret.equals("OK")){

  23. local.set(uuid);

  24. return true;

  25. }

  26. return false;

  27. }

  28.  
  29.  
  30. /**

  31. * 解鎖

  32. */

  33. @Override

  34. public void unlock() {

  35. String script=null;

  36. try {

  37. script=FileCopyUtils.copyToString(new FileReader(ResourceUtils.getFile("classpath:cn/rjx/spring/cache/unlock.c")));

  38. } catch (IOException e) {

  39. e.printStackTrace();

  40. }

  41. Jedis jedis = (Jedis)factory.getConnection().getNativeConnection();

  42. List<String> keys=new ArrayList<String>();

  43. keys.add(LOCK);

  44. List<String> args=new ArrayList<String>();

  45. args.add(local.get());

  46. //如果redis中的 lock值和當前線程的uuid值相等,刪除Key值

  47. jedis.eval(script, keys, args);

  48. }

  49.  
  50. }


刪除鍵值是執行的腳本unlock.c:

 

 


 
  1. if redis.call("get",KEYS[1])==ARGV[1] then

  2. return redis.call("del",KEYS[1])

  3. else

  4. return 0

  5. end

 

 

 

操作緩存的具體流程:

1.當線程查詢某一值時先查看緩存是否存在該值。

2.如果存在直接返回主緩存中的數據。

3.1如果不存在,只有一個線程獲取鎖並去數據庫讀取數據,讀取後更新主緩存和備份緩存。

3.2 其他線程取備份緩存中的數據。.

 

代碼實現:初始時,主緩存和備份緩存爲空,此時可能會有線程獲取的值爲空,但是並不影響用戶體驗,用戶可以再刷新一次。在要求比較高的場景裏面,可以考慮先把數據寫入緩存中,可以搭配定時刷新緩存的機制。

 


 
  1. public List<Integer> queryCountByLeiMu() {

  2. List<Integer> cacheResult = cacheService.cacheResult("101", "leimu");

  3. if(cacheResult!=null){

  4. logger.info("================get cache=======================");

  5. return cacheResult;

  6. }

  7. if(lock.tryLock()){

  8. logger.info("================get db=======================");

  9. List<Integer> list=empDao.queryCountByLeiMu();

  10. cacheService.cachePut("101", list, "leimu");//主緩存

  11. cacheService.cachePut("beifen101", list, "beifenleimu");//備份緩存

  12. lock.unlock();

  13. return list;

  14. }else{

  15. logger.info("================get BEIFEN=======================");

  16. //備份中拿

  17. return cacheService.cacheResult("beifen101", "beifenleimu");

  18. }

  19.  
  20. }


數據同步問題:主緩存中key的過期時間比較短,這樣保證儘可能獲取新數據。

 

 

bean.xml中緩存失效時間設置:

 


 
  1. <!-- 開啓緩存註解掃描 -->

  2. <cache:annotation-driven />

  3.  
  4. <bean id="cacheManager" class="org.springframework.data.redis.cache.RedisCacheManager">

  5. <constructor-arg index="0" ref="redisTemplate"></constructor-arg>

  6. <property name="expires">

  7. <map>

  8. <entry key="leimu" value="5"></entry>

  9. <entry key="beifenleimu" value="100"></entry>

  10. </map>

  11. </property>

  12. </bean>

 

 


測試方法模擬高併發情景:

 


 
  1. @Autowired

  2. LeiMuService leiMuService;

  3. private static final int threadNum=13;

  4. //倒計數器(發令槍) 用於製造線程併發執行

  5. private static CountDownLatch cdl=new CountDownLatch(threadNum);

  6.  
  7. /**

  8. * 模擬高併發條件下,數據庫查詢耗時比較長

  9. * @throws InterruptedException

  10. */

  11. @Test

  12. public void test04() throws InterruptedException{

  13. for(int i=0;i<threadNum;i++){

  14. new Thread(new UserRequest()).start();

  15. cdl.countDown();//threadNum每次減1,到零時同時執行cdl.await();後邊代碼

  16. }

  17. //主線程掛起,等子線程執行完以後

  18. Thread.currentThread().join();

  19. }

  20.  
  21. private class UserRequest implements Runnable{

  22. @Override

  23. public void run() {

  24. //所有子線程在這裏等待,當所有線程實例化後,同時停止等待

  25. try {

  26. cdl.await();

  27. } catch (InterruptedException e) {

  28. e.printStackTrace();

  29. }

  30. //N個子線程同時調用獲取類目

  31. List<Integer> leimu = leiMuService.queryCountByLeiMu();

  32. logger.info(Thread.currentThread().getName()+"==========================================>"+leimu.size());

  33.  
  34. }

  35.  
  36. }

  37.  
  38. }

 

 

缺點:1.非阻塞,短時間不能保證數據一致性

2.鎖失效時間難把握,一般爲單線程處理時長的兩到三倍

3.可能出現鎖失效情況

4******不能在redis集羣環境中使用(集羣中可用redLock)

 

建議使用基於zookeeper的分佈式鎖實現方式!!.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章