redis緩存在高併發和安全壓力下的一些問題解決方案---緩存穿透、緩存擊穿、緩存雪崩;redis分佈式鎖:set px nx、Redisson實現java代碼層控制分佈式鎖

1、緩存擊穿

  • 是某一個熱點key在高併發訪問的情況下,突然失效,導致大量的併發打進mysql數據庫的情況
  • 解決:在正常的訪問情況下,如果緩存失效,需保護mysql,在重啓緩存的過程,
    使用redis數據庫的分佈式鎖,解決mysql的訪問壓力問題。

2、緩存穿透

  • 是利用redis和mysql的機制(redis緩存一旦不存在,就訪問mysql),直接繞過緩存訪問mysql,而製造的db請求壓力
    一般在代碼中防止該現象的發生
  • 解決: 爲了防止緩存穿透將,null或者空字符串值設置給redis

3、緩存雪崩

  • 緩存時採用了相同的過期時間,導致緩存在某一時刻同時失效,導致的db崩潰
  • 解決:設置不同的緩存失效時間

4、解決緩存擊穿的分佈式鎖有兩種

1. redis自帶一個分佈式鎖,set px nx

  • 這種分佈式鎖作用在redis上
  • redis上的命令:set key:lock 1 px time(ms) nx
    在這裏插入圖片描述

2. redisson框架,一個redis的帶有juc的lock功能的客戶端的實現(既有jedis的功能,又有juc的鎖功能)

  • 這種分佈式鎖作用在多個Jedis上
    在這裏插入圖片描述

3.使用redis分佈式鎖的實現

3.1、使用本方法處理高併發下的人會遇到的問題
1、 高併發下,若是拿鎖的線程因某些原因,導致自己的鎖未經過自己釋放而過期,而某一時刻該線程醒來時,會來釋放鎖,而此時redis上的鎖已經是別的線程上的鎖了,怎麼辦?

解決此問題有兩種方案:

    1. 不主動釋放鎖,讓其自動釋放。(但要求高響應的系統都不會這樣實現)
    1. 仍然主動釋放鎖,但是設置鎖的key對於的唯一Value ,需解鎖時,先判斷是否是自己的鎖,再釋放鎖,是則釋放,否則直接跳過。
2、若是剛好在比對鎖的key對於的唯一Value的時候,該線程的鎖過期,由別的線程進入並拿到鎖,此時釋放鎖,而此時redis上的鎖已經是別的線程上的鎖了,該怎麼辦?
  • 可以用lua腳本,在查詢到key的同時刪除該key,防止高併發下的該意外的發生。
    (lua:別讓我看到你,看到我就把你幹掉)
    在這裏插入圖片描述
3.2、redis分佈式鎖代碼體現,以下代碼對上述問題的解決方案都有體現。

本代碼是一個業務邏輯中的服務層的一個方法。

public PmsSkuInfo getSkuBySkuId(String skuId,String ip) {
        System.out.println("ip爲"+ip+"的同學:"+Thread.currentThread().getName()+"進入的商品詳情的請求");
        //獲取jedis對象
        Jedis jedis = redisUtil.getJedis();
        //獲取skuinfo
        PmsSkuInfo pmsSkuInfo = new PmsSkuInfo();

        //構造key
        String skukey = "sku:"+skuId+":Info";
        //查緩存
        String skuJson = jedis.get(skukey);
        //判斷緩存是否存在
        if (StringUtils.isNotBlank(skuJson)){
            //緩存存在 提取出數據封裝在對象中
            pmsSkuInfo = JSON.parseObject(skuJson,PmsSkuInfo.class);
        }else{
            //緩存中不存在改數據 去mysql中查找

            //設置分佈式鎖 防止擊穿
           
            //設置redis上分佈式鎖的唯一值
            String lockValue = UUID.randomUUID().toString();
            //上鎖成功放回 OK
            String Ok = jedis.set("sku:"+skuId+":lock",lockValue,"nx","px",10*1000);
            if (StringUtils.isNotBlank(Ok) && Ok.equals("OK")){
                //設置鎖成功 有權在10s內操作mysql
                pmsSkuInfo = getSkuBySkuIdFromDb(skuId);
                if (pmsSkuInfo != null){

                    //先睡眠5s再讓其操作mysql  再解鎖
                    // 睡眠時爲了看到其他線程自旋效果 生產環境不會睡眠
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    //db存在數據 將db數據存入redis
                    jedis.set("sku:"+skuId+":Info", JSON.toJSONString(pmsSkuInfo));
                }else{
                    //db不存在數據 將null存入redis 防止穿透
                    jedis.set("sku:"+skuId+":Info",JSON.toJSONString(""));
                }

                //操作完mysql,將redis的分佈式鎖釋放
                System.out.println("ip爲"+ip+"的同學:"+Thread.currentThread().getName()+"使用完畢,將鎖歸還:"+"sku:" + skuId + ":lock");
                String currentLockValue = jedis.get("sku:"+skuId+":lock");
                if (StringUtils.isNotBlank(currentLockValue) && currentLockValue.equals(lockValue)){
                    //jedis.eval("lua");可與用lua腳本,在查詢到key的同時刪除該key,防止高併發下的意外的發生
                    jedis.del("sku:" + skuId + ":lock");// 用token確認刪除的是自己的sku的鎖
                }

            }else {
                //設置失敗 說明已經有別的用戶線程設置了鎖
                System.out.println("ip爲"+ip+"的同學:"+Thread.currentThread().getName()+"沒有拿到鎖,開始自旋");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //自旋 讓其睡眠幾秒後重寫調用本方法  睡眠是爲了看效果 具體生產環境不會設置睡眠
                //注意 不能不帶return ,若是不帶return 直接調用方法,會另外啓動一個線程
                //被新啓動的線程爲孤兒線程,別的線程無法訪問
                return getSkuBySkuId(skuId,ip);
            }
        }
        //關閉jedis
        jedis.close();
        return pmsSkuInfo;
    }
3.1.1、模擬併發,兩個頁面同時訪問,看後臺輸出,第一個先進入的線程會拿到鎖,鎖是唯一,別的線程拿不到鎖會不斷自旋,直到拿鎖線程釋放鎖爲止。
ip爲127.0.0.1的同學:DubboServerHandler-192.168.154.1:53630-thread-2進入的商品詳情的請求
ip爲127.0.0.1的同學:DubboServerHandler-192.168.154.1:53630-thread-2拿到分佈式鎖
ip爲127.0.0.1的同學:DubboServerHandler-192.168.154.1:53630-thread-3進入的商品詳情的請求
ip爲127.0.0.1的同學:DubboServerHandler-192.168.154.1:53630-thread-3沒有拿到鎖,開始自旋
ip爲127.0.0.1的同學:DubboServerHandler-192.168.154.1:53630-thread-3進入的商品詳情的請求
ip爲127.0.0.1的同學:DubboServerHandler-192.168.154.1:53630-thread-3沒有拿到鎖,開始自旋
ip爲127.0.0.1的同學:DubboServerHandler-192.168.154.1:53630-thread-2使用完畢,將鎖歸還:sku:115:lock
ip爲127.0.0.1的同學:DubboServerHandler-192.168.154.1:53630-thread-3進入的商品詳情的請求

4、使用redission的簡單實現

redisson封裝許多juc的併發工具類,可以調用內部工具類實現併發控制。

public String testRedisson(){
        Jedis jedis = redisUtil.getJedis();
        RLock lock = redissonClient.getLock("lock");// 聲明鎖
        lock.lock();//上鎖
        try {
            String v = jedis.get("k");
            if (StringUtils.isBlank(v)) {
                v = "1";
            }
            System.out.println("->" + v);
            jedis.set("k", (Integer.parseInt(v) + 1) + "");
        }finally {
            jedis.close();
            lock.unlock();// 解鎖
        }
        return "success";
    }

有興趣的小夥伴可以 一起交流哦!!!

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