對於Redis腳本使用過的同學都知道,這個主要是為了防止競態條件而用的。因為腳本是順序執行的。(不用擔心效率問題)比如我在工作用,用來設置考試最高分。
如果還沒有用過的話,先去看Redis腳本的介紹,發送腳本,緩存腳本,發送sha1執行腳本,以及基本的lua腳本的語法。
1. Redis腳本的使用場景
在一些緩存的設置中,經常會出現競態條件,由於並發導致數據有誤。比如大家熟知的++操作。我們自己通過Redis實現++的話,很容易在並發下出現誤差。所以Redis提供了incr函數。我在設置最高分的時候,獲取分數70,A得分80,然后設置80。然后在同時,B獲取分數70,但是B得分78,78>70,然后設置78。這樣的話,B的修改就把A的修改替換了。這也是數據庫隔離級別中的第一類更新丟失。
為了防止這個問題,Redis提供了順序執行命令的方法,就是使用腳本。
2. 腳本類RedisScript
RedisTemplate對腳本提供了很高的支持,執行方法同之前的類似,都是通過connection回調。但是這里要注意的是:腳本不支持事務,所以腳本之前不能進行connection.multi()開啟事務,也不能用@Transactional注解讓spring來開啟事務,這些都會拋出異常,大家可以自行測試下。
看到這里,我們新建一個最高分的腳本類,MaxscoreScript,繼承自接口RedisScript,需要實現三個方法:
public interface RedisScript<T> { /** * @return The SHA1 of the script, used for executing Redis evalsha command */ String getSha1(); /** * @return The script result type. Should be one of Long, Boolean, List, or deserialized value type. Can be null if * the script returns a throw-away status (i.e "OK") */ Class<T> getResultType(); /** * @return The script contents */ String getScriptAsString(); }
getSha1(),是獲取腳本摘要。看過腳本的應該知道,Redis支持緩存腳本,第一次發送腳本以后,后面都直接發送該腳本的sha1摘要信息來直接執行,大概是為了節省傳輸成本吧。畢竟摘要只需要32個字符。
getResultType(),是獲取返回類型的class,需要和定義的泛型T一致。其實之前在Hibernate的Basedao里面,都用過直接通過實例來獲取泛型的class。雖然不明白這里為什么加,但是實現這個方法,絕對是沒有錯。
getScriptAsString(),毫無疑問,這個是返回腳本內容。至於怎么寫腳本,這里不做說明。
3. 使用RedisTemplate執行腳本
public <T> T execute(RedisScript<T> script, List<K> keys, Object... args) { return scriptExecutor.execute(script, keys, args); } public <T> T execute(RedisScript<T> script, RedisSerializer<?> argsSerializer, RedisSerializer<T> resultSerializer, List<K> keys, Object... args) { return scriptExecutor.execute(script, argsSerializer, resultSerializer, keys, args); }
相比之下,第二個是自己傳入序列化器,第一個是用默認的key和value序列化器。
強烈建議使用第二個。然后統一傳入(StringSerializer),里面的參數全部轉為String。我在使用的時候,lua進行大小比較,發現比較的有誤。后來調試才發現,是JdkSerializer中,Redis把他當一串\xu..這樣的字符串,字符串比較顯然不准確。但是如果是"11","12"這樣的字符串,就可以通過lua轉為num類型,再進行比較。
當然,也許有別的業務場景,不需要這么做,也有可能。自行分析斟酌。
我們看到執行腳本的源碼
protected <T> T eval(RedisConnection connection, RedisScript<T> script, ReturnType returnType, int numKeys, byte[][] keysAndArgs, RedisSerializer<T> resultSerializer) { Object result; try { result = connection.evalSha(script.getSha1(), returnType, numKeys, keysAndArgs); } catch (Exception e) { if (!exceptionContainsNoScriptError(e)) { throw e instanceof RuntimeException ? (RuntimeException) e : new RedisSystemException(e.getMessage(), e); } result = connection.eval(scriptBytes(script), returnType, numKeys, keysAndArgs); } if (script.getResultType() == null) { return null; } return deserializeResult(resultSerializer, result); }
這一段代碼是最終調用的,之前還有一些序列化參數的操作沒有貼出來。
默認直接提交sha1摘要,得到異常,如果異常屬於exceptionContainsNoScriptError ,再發送執行腳本,獲取返回結果后通過用戶定義的結果序列化器進行反序列化。
4. 總結
這一篇講了RedisTemplate如何使用腳本。可能對於不懂腳本在connection 或者在redis控制台下使用的同學來說,還是不能理解。但是如果懂得控制台發送腳本,那么通過RedisTemplate的使用,會讓很多問題迎刃而解。
到這里我所能帶給大家的也就說完了。下一篇我將把自己遇到的問題羅列下,前車之鑒,后事之師。希望大家以后遇到后,不會在卡太長時間。