Redis學習9——與springboot結合(事務,管道,Lua)

事務:不多解釋,保證數據的一致性。
管道:在需要大批量執行Redis命令的時候,這樣可以極大地提升Redis執行的速度。
Lua語言:在高併發的場景中,往往我們需要保證數據的一致性,利用Redis執行Lua的原子性來達到數據一致性的目的。

一、使用Redis事務

在Redis中使用事務,命令組合是watch… multi…exec,在spring中可以使用SessionCallback接口來實現。

watch:監控Redis的一些鍵;

multi:開始事務,開始事務後,該客戶端的命令不會馬上被執行,而是存放在一個隊列裏,也就是在這時我們執行一些返回數據的命令,結果都是返回null;

exe:執行事務,它在命令執行前會判斷被watch監控的Redis的鍵的數據是否發生過變化(即使賦予與之前相同的值也會被認爲是變化過),如果它認爲發生了變化,那麼Redis就會取消事務,否則就會執行事務。

Redis事務執行過程:
在這裏插入圖片描述

通過Spring使用Redis事務機制:

@RequestMapping("/multi")
@ResponseBody
public Map<String, Object> testMulti() {
    redisTemplate.opsForValue().set("key1", "value1");     
    List list = (List)redisTemplate.execute((RedisOperations operations) -> {
        // 設置要監控key1
        operations.watch("key1");
        // 開啓事務,在exec命令執行前,全部都只是進入隊列
        operations.multi();
        operations.opsForValue().set("key2", "value2");
        // operations.opsForValue().increment("key1", 1);// ①
		// 獲取值將爲null,因爲redis只是把命令放入隊列
        Object value2 = operations.opsForValue().get("key2");
        System.out.println("命令在隊列,所以value爲null【"+ value2 +"】");
        operations.opsForValue().set("key3", "value3");
        Object value3 = operations.opsForValue().get("key3");
        System.out.println("命令在隊列,所以value爲null【"+ value3 +"】");
        // 執行exec命令,將先判別key1是否在監控後被修改過,如果是則不執行事務,否則就執行事務
        return operations.exec();// ②
	});
		System.out.println(list);
		Map<String, Object> map = new HashMap<String, Object>();
		map.put("success", true);
		return map;
}

爲了揭示Redis事務的特性,我們對這段代碼做以下兩種測試。

•從打印結果看到,multi開啓後,命令並沒有被執行,而是放在隊列中,所以key2、key3返回的值都爲空(nil),

•把①處的註釋取消,讓代碼可以運行,因爲key1是一個字符串,所以這裏的代碼是對字符串加一,這顯然是不能運算的。所以可以看到服務器拋出了異常,但我們去Redis服務器查詢key2和key3,它們卻有了值。
注意,這是Redis事務和數據庫事務的不一樣,對於Redis事務是先讓命令進入隊列,所以一開始它並沒有檢測這個加一命令是否能夠成功,只有在exec命令執行的時候,才能發現錯誤,對於出錯的命令Redis只是報出錯誤,而錯誤後面的命令依舊被執行,所以key2和key3都存在數據,這就是Redis事務的特點,爲了克服這個問題,一般我們要在執行Redis事務前,嚴格地檢查數據,以避免這樣的情況發生。

二、使用Redis管道

管道:批量發送命令給redis,提升命令執行速度(在默認的情況下,Redis客戶端是一條條命令發送給Redis服務器的,網絡傳輸的速度會成爲性能的瓶頸)。

使用Redis管道測試性能

@RequestMapping("/pipeline")
@ResponseBody
public Map<String, Object> testPipeline() {
    Long start = System.currentTimeMillis();
    List list = (List)redisTemplate.executePipelined((RedisOperations operations) -> {
	for (int i=1; i<=100000; i++) {
            operations.opsForValue().set("pipeline_" + i, "value_" + i);
            String value = (String) operations.opsForValue().get("pipeline_" + i);
            if (i == 100000) {
                System.out.println("命令只是進入隊列,所以值爲空【" + value +"】");
            }
        }
        return null;
    });
    Long end = System.currentTimeMillis();
    System.out.println("耗時:" + (end - start) + "毫秒。");
    Map<String, Object> map = new HashMap<String, Object>();
    map.put("success", true);
    return map;
}

沿用SessionCallback接口執行寫入和讀出各10萬次Redis命令,測試結果:使用管理的速度可以提升10倍左右,非常適合大數據量的執行。

注意兩點:
內存空間的消耗,對於程序而言,它最終會返回一個List對象,如果過多的命令執行返回的結果都保存到這個List中,會造成內存消耗過大,在那些高併發的網站中就很容易造成JVM內存溢出的異常,這個時候應該考慮使用迭代的方法執行Redis命令。
•與事務一樣,使用管道的過程中,所有的命令也只是進入隊列而沒有執行,所以執行的命令返回值也爲空。

三、使用Redis發佈訂閱
現在有kafka這些的消息中間件來實現發佈訂閱,更方便,因爲Redis發佈訂閱基本沒有什麼應用場景,這裏不再做介紹。

四、使用Lua腳本

優點
Lua腳本在Redis中具備原子性,在高併發環境中,可以用來保證數據的一致性。
Lua腳本具備很強大的運算功能,在高併發環境中,比使用Redis自身提供的事務要更好一些。

在Redis中有兩種運行Lua的方法:

一種是直接發送Lua到Redis服務器去執行,

一種是先把Lua發送給Redis,Redis會對Lua腳本進行緩存,然後返回一個SHA1的32位編碼回來,之後只需要發送SHA1和相關參數給Redis便可以執行了。此方法主要因爲如果Lua腳本很長,通過網絡
傳遞腳本的速度跟不上Redis的執行速度,網絡就會成爲Redis執行的瓶頸。如果只是傳遞32位編碼和參數,那麼需要傳遞的消息就少了許多,這樣就可以極大地減少網絡傳輸的內容,從而提高系統的性能。

爲了支持Redis的Lua腳本,Spring提供了RedisScript接口,與此同時也有一個DefaultRedisScript實現類。

RedisScript接口定義:

package org.springframework.data.redis.core.script;
public interface RedisScript<T> {
     // 獲取腳本的Sha1
    String getSha1();

    // 獲取腳本返回值
    Class<T> getResultType();

    // 獲取腳本的字符串
    String getScriptAsString();
}

這裏Spring會將Lua腳本發送到Redis服務器進行緩存,而此時Redis服務器會返回一個32位的SHA1編碼,這時候通過getSha1方法就可以得到Redis返回的這個編碼了;
getResultType方法是獲取Lua腳本返回的Java類型;getScriptAsString是返回腳本的字符串,以便我們觀看腳本。

執行簡易Lua腳本:

@RequestMapping("/lua")
@ResponseBody
public Map<String, Object> testLua() {
    DefaultRedisScript<String> rs = new DefaultRedisScript<String>();
    // 設置腳本
    rs.setScriptText("return 'Hello Redis'");
    // 定義返回類型。注意:如果沒有這個定義,Spring不會返回結果
    rs.setResultType(String.class);
    RedisSerializer<String> stringSerializer
      = redisTemplate.getStringSerializer();
    // 執行Lua腳本
    String str = (String) redisTemplate.execute(
        rs, stringSerializer, stringSerializer, null);
    Map<String, Object> map = new HashMap<String, Object>();
    map.put("str", str);
    return map;
}

這裏的代碼,首先Lua只是定義了一個簡單的字符串,然後就返回了,而返回類型則定義爲字符串。這裏必須定義返回類型,否則對於Spring不會把腳本執行的結果返回。

接着獲取了由RedisTemplate自動創建的字符串序列化器,而後使用RedisTemplate的execute方法執行了腳本。在RedisTemplate中,execute方法執行腳本的方法有兩種,其定義如下:

public <T> T execute(RedisScript<T> script, List<K> keys, Object... args) 

public <T> T execute(RedisScript<T> script, RedisSerializer<?> argsSerializer, 
        RedisSerializer<T> resultSerializer, List<K> keys, Object... args)

在這兩個方法中,從參數的名稱可以知道,script就是我們定義的RedisScript接口對象,keys代表Redis的鍵,args是這段腳本的參數。
兩個方法最大區別是一個存在序列化器的參數,另外一個不存在。對於不存在序列化參數的方法,Spring將採用RedisTemplate提供的valueSerializer序列化器對傳遞的鍵和參數進行序列化。

這裏我們採用了第二個方法調度腳本,並且設置爲字符串序列化器,其中第一個序列化器是鍵的序列化器,第二個是參數序列化器,這樣鍵和參數就在字符串序列化器下被序列化了。

測試Lua腳本
可以斷點然後從監控來看,RedisScript對象會存放對應的SHA1的字符串對象,這樣就可以通過它執行Lua腳本了。由於返回已經是“Hello Redis”,顯然測試是成功的。

下面我們再考慮存在參數的情況。例如,我們寫一段Lua腳本用來判斷兩個字符串是否相同。

帶有參數的Lua

redis.call('set', KEYS[1], ARGV[1]) 
redis.call('set', KEYS[2], ARGV[2]) 
local str1 = redis.call('get', KEYS[1]) 
local str2 = redis.call('get', KEYS[2]) 
if str1 == str2 then  
return 1 
end 
return 0

這裏的腳本中使用了兩個鍵去保存兩個參數,然後對這兩個參數進行比較,如果相等則返回1,否則返回0。
注意腳本中KEYS[1]和KEYS[2]的寫法,它們代表客戶端傳遞的第一個鍵和第二個鍵,而ARGV[1]和ARGV[2]則表示客戶端傳遞的第一個和第二個參數。

測試帶有參數的Lua腳本

@RequestMapping("/lua2")
@ResponseBody
public Map<String, Object> testLua2(String key1, String key2, String value1, String value2) {
    // 定義Lua腳本
    String lua = "redis.call('set', KEYS[1], ARGV[1]) \n"
            + "redis.call('set', KEYS[2], ARGV[2]) \n"
            + "local str1 = redis.call('get', KEYS[1]) \n"
            + "local str2 = redis.call('get', KEYS[2]) \n"
            + "if str1 == str2 then  \n"
            + "return 1 \n"
            + "end \n"
            + "return 0 \n";
    System.out.println(lua);
    // 結果返回爲Long
    DefaultRedisScript<Long> rs = new DefaultRedisScript<Long>();
    rs.setScriptText(lua);
    rs.setResultType(Long.class);
    // 採用字符串序列化器
    RedisSerializer<String> stringSerializer = redisTemplate.getStringSerializer();
    // 定義key參數
    List<String> keyList = new ArrayList<>();
    keyList.add(key1);
    keyList.add(key2);
    // 傳遞兩個參數值,其中第一個序列化器是key的序列化器,第二個序列化器是參數的序列化器
    Long result = (Long) redisTemplate.execute(
        rs, stringSerializer, stringSerializer, keyList, value1, value2);
    Map<String, Object> map = new HashMap<String, Object>();
    map.put("result", result);
    return map;
}

測試:

http://localhost:8080/redis/lua2?key1=key1&key2=key2&value1=1&value2=1

這裏使用keyList保存了各個鍵,然後通過Redis的execute方法傳遞,參數則可以使用可變化的方式傳遞,且設置了給鍵和參數的序列化器都爲字符串序列化器,這樣便能夠運行這段腳本了。
我們的腳本返回爲一個數字,這裏值得注意的是,因爲Java會把整數當作長整型(Long),所以這裏返回值設置爲Long型。

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