1. 程式人生 > >SpringBoot集成Redis分布式鎖以及Redis緩存

SpringBoot集成Redis分布式鎖以及Redis緩存

ofo 當前 csdn bean system ext pid 註解 fin

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緩存