SpringBoot集成Redis分布式鎖以及Redis緩存
https://blog.csdn.net/qq_26525215/article/details/79182687
集成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/
本文源自【諳憶的博客】
SpringBoot集成Redis分布式鎖以及Redis緩存