事務:不多解釋,保證數據的一致性。
管道:在需要大批量執行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型。