1. 程式人生 > >Redis】SpringBoot整合Redis分散式鎖以及Redis快取

Redis】SpringBoot整合Redis分散式鎖以及Redis快取

整合Redis 首先在pom.xml中加入需要的redis依賴和快取依賴

<!-- 引入redis依賴 --> <dependency>     <groupId>org.springframework.boot</groupId>     <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- 快取的依賴--> <dependency>     <groupId>org.springframework.boot</groupId>     <artifactId>spring-boot-start-cache</artifactId> </dependency> 1 2 3 4 5 6 7 8 9 10 第二個spring-boot-start-cache的依賴,是使用快取註解需要的,我在專案中沒有引入。  因為我在websocket中已經引入了。  查詢依賴關係 ctrl+shift+alt+u 快捷鍵(也可以在pom.xml檔案上右鍵->Maven->Show Dependencies…)查詢maven包依賴引入關係,ctrl+f搜尋包

SpringBoot的yml配置檔案下增加redis的配置:

spring:   redis:     host: 127.0.0.1     port: 6379     password: chenhaoxiang 1 2 3 4 5 輸入你自己Redis伺服器的地址,埠和密碼,沒有密碼的就不要password了。

實現Redis分散式鎖 在類中直接使用如下程式碼即可注入Redis的操作類

@Autowired private StringRedisTemplate stringRedisTemplate;//可以寫很多型別的值 1 2 簡單的操作 更多的Redis內容請看: http://redis.cn/

set

//設定key-value和過期時間 stringRedisTemplate.opsForValue().set("key","value",7200, TimeUnit.SECONDS);//key,value,過期時間,時間單位 s 1 2 使用儲存的時候,最後要設定一個過期時間,就算是幾年,你也要設定一個過期時間。否則會一直佔用儲存空間的

delete

stringRedisTemplate.opsForValue().getOperations().delete("key");//刪除key對應的鍵值對 1 get

stringRedisTemplate.opsForValue().get("key");//獲取對應key的value 1 分散式鎖 接下來就是講分散式鎖了。  假設在一個活動中,商品的特價出售,限時秒殺場景。比如雙11的。  通常的做法,有樂觀鎖和悲觀鎖  介紹樂觀鎖和悲觀鎖是什麼我就不介紹了。  其實這裡的Redis分散式鎖也算是一種樂觀鎖。也就是即使資源被鎖了,後來的使用者不會被阻塞,而是返回異常/資訊給你,告訴你操作(在這裡是搶購)不成功。

實現起來很簡單。看下面的類:

package cn.chenhaoxiang.service;

import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils;

/**  * Created with IntelliJ IDEA.  * User: 陳浩翔.  * Date: 2018/1/26.  * Time: 下午 10:05.  * Explain:Redis分散式鎖  */ @Component @Slf4j public class RedisLock {     @Autowired     private StringRedisTemplate stringRedisTemplate;

    /**      * 加鎖      * @param key productId - 商品的唯一標誌      * @param value  當前時間+超時時間 也就是時間戳      * @return      */     public boolean lock(String key,String value){         if(stringRedisTemplate.opsForValue().setIfAbsent(key,value)){//對應setnx命令             //可以成功設定,也就是key不存在             return true;         }

        //判斷鎖超時 - 防止原來的操作異常,沒有執行解鎖操作  防止死鎖         String currentValue = stringRedisTemplate.opsForValue().get(key);         //如果鎖過期         if(!StringUtils.isEmpty(currentValue) && Long.parseLong(currentValue) < System.currentTimeMillis()){//currentValue不為空且小於當前時間             //獲取上一個鎖的時間value             String oldValue =stringRedisTemplate.opsForValue().getAndSet(key,value);//對應getset,如果key存在

            //假設兩個執行緒同時進來這裡,因為key被佔用了,而且鎖過期了。獲取的值currentValue=A(get取的舊的值肯定是一樣的),兩個執行緒的value都是B,key都是K.鎖時間已經過期了。             //而這裡面的getAndSet一次只會一個執行,也就是一個執行之後,上一個的value已經變成了B。只有一個執行緒獲取的上一個值會是A,另一個執行緒拿到的值是B。             if(!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue) ){                 //oldValue不為空且oldValue等於currentValue,也就是校驗是不是上個對應的商品時間戳,也是防止併發                 return true;             }         }         return false;     }

    /**      * 解鎖      * @param key      * @param value      */     public void unlock(String key,String value){         try {             String currentValue = stringRedisTemplate.opsForValue().get(key);             if(!StringUtils.isEmpty(currentValue) && currentValue.equals(value) ){                 stringRedisTemplate.opsForValue().getOperations().delete(key);//刪除key             }         } catch (Exception e) {             log.error("[Redis分散式鎖] 解鎖出現異常了,{}",e);         }     }

} 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 這個是Redis加鎖和解鎖的工具類  裡面使用的主要是兩個命令,SETNX和GETSET。  SETNX命令 將key設定值為value,如果key不存在,這種情況下等同SET命令。 當key存在時,什麼也不做  GETSET命令 先查詢出原來的值,值不存在就返回nil。然後再設定值  對應的Java方法在程式碼中提示了。  注意一點的是,Redis是單執行緒的!所以在執行GETSET和SETNX不會存在併發的情況。

下面來看我們使用該類加鎖解鎖的類:

package cn.chenhaoxiang.service.impl;

import cn.chenhaoxiang.exception.SellException; import cn.chenhaoxiang.service.RedisLock; import cn.chenhaoxiang.service.SeckillService; import cn.chenhaoxiang.utils.KeyUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service;

import java.util.HashMap; import java.util.Map;

/**  * Created with IntelliJ IDEA.  * User: 陳浩翔.  * Date: 2018/1/26.  * Time: 下午 9:30.  * Explain:  */ @Service public class SeckillServiceImpl implements SeckillService{

    @Autowired     private RedisLock redisLock;

    private static final int TIMEOUT = 10*1000;//超時時間 10s

    /**      * 活動,特價,限量100000份      */     static Map<String,Integer> products;//模擬商品資訊表     static Map<String,Integer> stock;//模擬庫存表     static Map<String,String> orders;//模擬下單成功使用者表     static {         /**          * 模擬多個表,商品資訊表,庫存表,秒殺成功訂單表           */         products = new HashMap<>();         stock = new HashMap<>();         orders = new HashMap<>();         products.put("123456",100000);         stock.put("123456",100000);     }

    private String queryMap(String productId){//模擬查詢資料庫         return "國慶活動,皮蛋特教,限量"                 +products.get(productId)                 +"份,還剩:"+stock.get(productId)                 +"份,該商品成功下單使用者數:"                 +orders.size()+"人";     }

    @Override     public String querySecKillProductInfo(String productId) {         return this.queryMap(productId);     }

    //解決方法二,基於Redis的分散式鎖 http://redis.cn/commands/setnx.html  http://redis.cn/commands/getset.html     //SETNX命令  將key設定值為value,如果key不存在,這種情況下等同SET命令。 當key存在時,什麼也不做     // GETSET命令  先查詢出原來的值,值不存在就返回nil。然後再設定值     //支援分散式,可以更細粒度的控制     //多臺機器上多個執行緒對一個數據進行操作的互斥。     //Redis是單執行緒的!!!     @Override     public void orderProductMocckDiffUser(String productId) {//解決方法一:synchronized鎖方法是可以解決的,但是請求會變慢,請求變慢是正常的。主要是沒做到細粒度控制。比如有很多商品的秒殺,但是這個把所有商品的秒殺都鎖住了。而且這個只適合單機的情況,不適合叢集

        //加鎖         long time = System.currentTimeMillis() + TIMEOUT;         if(!redisLock.lock(productId,String.valueOf(time))){             throw new SellException(101,"很抱歉,人太多了,換個姿勢再試試~~");         }

        //1.查詢該商品庫存,為0則活動結束         int stockNum = stock.get(productId);         if(stockNum==0){             throw new SellException(100,"活動結束");         }else {             //2.下單             orders.put(KeyUtil.getUniqueKey(),productId);             //3.減庫存             stockNum =stockNum-1;//不做處理的話,高併發下會出現超賣的情況,下單數,大於減庫存的情況。雖然這裡減了,但由於併發,減的庫存還沒存到map中去。新的併發拿到的是原來的庫存             try{                 Thread.sleep(100);//模擬減庫存的處理時間             }catch (InterruptedException e){                 e.printStackTrace();             }             stock.put(productId,stockNum);         }

        //解鎖         redisLock.unlock(productId,String.valueOf(time));

    } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 在上面是用Map來模擬查詢資料庫的操作了,sleep是為了模擬一些io操作的時間  你可以用apache ab工具進行高併發模擬。

Redis快取 接下來就講下快取了  首先當然是匯入Maven依賴咯  接下來就是在springboot啟動類上加上註解:

@EnableCaching //快取支援  配置Redis快取需要的 1 因為我們上面已經在配置檔案配置好了 redis的地址,賬號。就不需要再配置了。  下面你就可以使用註解快取了

在Controller層的使用 //Redis快取註解  Cacheable第一次訪問會訪問到方內的內容,方法會返回一個物件,返回物件的時候,會把這個物件儲存。下一次訪問的時候,不會進去這個方法,直接從redis快取中拿 @Cacheable(cacheNames = "product",key = "123") public ResultVO list(){ ... } 1 2 3 4 5 在這裡,product其實就相當於一個名稱空間。key的話,在更新快取,刪除快取的時候用到的。  注意,方法返回的物件加了快取註解的,一定要實現序列化!

然後,我們可以在增刪改的地方加上刪除快取,或者更新快取的註解。

@CacheEvict(cacheNames = "product",key = "123") //訪問這個方法之後刪除對應的快取  對應之前的Redis快取註解的配置 。key如果不填,預設是空,對應的值應該就是方法的引數的值了.對應BuyerProductController-list方法的快取 //    @CachePut(cacheNames = "product",key = "123") //對應之前的Redis快取註解的配置     //@CachePut 每次還是會執行方法中的內容,每次執行完成後會把返回的內容放到Redis中去.     // 這種註解和原來對應的返回物件需要是相同的才行,這裡返回的是ModelAndView。可以到service層註解或者dao層註解CachePut     public ModelAndView save(@Valid ProductForm productForm,                              BindingResult bindingResult,                              Map<String,Object> map){  ... } 1 2 3 4 5 6 7 8 9 但是假如我們不想使用CacheEvict刪除快取呢,只希望更新快取呢,但是這裡的返回值是ModelAndView,和前面的ResultVO不一樣,而且無法序列化ModelAndView。所以在這裡寫註解,肯定只能是刪除快取的註解CacheEvict  其實我們可以去service層寫快取註解的,或者是Dao層,這樣,返回物件是受我們控制的了。

在service層使用快取 在整個類上註解

@CacheConfig(cacheNames = "product") //配置整個類的快取cacheNames,相當於作用域 1 這樣,這個類下的方法就不用再寫cacheNames了 。

@Cacheable(key = "123") //註解快取 public ProductInfo findOne(String productInfoId) {     return productInfoDao.findOne(productInfoId); } 1 2 3 4 @CachePut(key = "123") //和上面findOne的返回物件對應 public ProductInfo save(ProductInfo productInfo) {     return productInfoDao.save(productInfo); } 1 2 3 4 快取註解的另外一些值 key我們是可以動態設定的

@Cacheable(cacheNames = "product",key = "#sellerId")//sellerId為方法中的引數名,這樣,key就是動態配置了 public ResultVO list(String sellerId){ ... } 1 2 3 4 可以根據引數來進行判斷,是否快取

@Cacheable(cacheNames = "product",key = "#sellerId",condition = "#sellerId.length() > 3") public ResultVO list(String sellerId){ ... } 1 2 3 4 這樣只有條件成立才會直接返回快取,結果不成立是不快取的,即使有快取,也會執行方法

還可以根據返回結果來判斷是不是快取這個結果

@Cacheable(cacheNames = "product",key = "#sellerId",unless = "#result.getCode() != 0") public ResultVO list(String sellerId){ ... } 1 2 3 4 依據結果來判斷是否快取 unless = “#result.getCode() != 0”,#result其實就是ResultVO,也就是返回的物件  unless(除什麼之外,如果不 的意思) 如果=0就快取,需要寫成!=0。理解起來就是,除了不等於0的情況之外,才快取,也就是等於0才快取。  其實就是,你想要什麼條件下快取,你寫在這裡面,把條件反過來寫就行了

你如果測試快取的話,你可以在方法內打一個斷點進行測試。沒有執行那個方法就獲取到資料了,證明快取生效了。

最後,注意,返回的快取物件一定要實現序列化!!!

專案地址: GITHUB原始碼下載地址:【點我進行訪問】 本文章由[諳憶]編寫, 所有權利保留。  歡迎轉載,分享是進步的源泉。

轉載請註明出處:http://chenhaoxiang.cn/2018/01/27/0104/ 本文源自【諳憶的部落格】 ---------------------  作者:諳憶  來源:CSDN  原文:https://blog.csdn.net/qq_26525215/article/details/79182687  版權宣告:本文為博主原創文章,轉載請附上博文連結!