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 版權宣告:本文為博主原創文章,轉載請附上博文連結!