攔截Redis命令導致的Lua腳本執(zhí)行失敗的問題解決
大家好,今天分享一個在使用 redis lua 腳本過程中遇到的一個問題,問題不難,但是容易踩坑。
lua 腳本使用方式
// 定義腳本資源 DefaultRedisScript redisScript = new DefaultRedisScript<>(); redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(SCRIPT_PATH + scriptName))); redisScript.setResultType(List.class); // 預加載腳本(可選) redisTemplate.getConnectionFactory().getClusterConnection().scriptLoad(redisScript.getScriptAsString().getBytes()); // 執(zhí)行腳本 stringRedisTemplate.execute(redisScript, keys, args);
redis script 相關(guān)命令說明
- script load:將 lua 腳本加載到 redis 的腳本緩存中,返回該腳本的 sha1 校驗和,之后通過 evalsha 命令用此校驗和調(diào)用該腳本。
- evalsha:根據(jù) sha1 執(zhí)行已加載入的 lua 腳本。
- eval:執(zhí)行一段 lua 腳本代碼,執(zhí)行完后該腳本也會緩存到 redis 腳本緩存中。
- script exists:根據(jù) sha1 檢查腳本是否已經(jīng)存在于腳本緩存中。
- script flush:清空 redis 的腳本緩存,刪除所有已加載的 lua 腳本。
- script kill:kill 正在執(zhí)行的 lua 腳本。
execute 方法執(zhí)行過程
reidsTemplate 里持有一個 ScriptExecutor,最終的執(zhí)行都代理給 ScriptExecutor,ScriptExecutor 會通過 evalsha 命令去 redis server 端執(zhí)行腳本。
如果之前已經(jīng)通過 script load 命令預加載了 lua 腳本,則 evalsha 會正常執(zhí)行;如果沒有事先加載腳本且第一次執(zhí)行該腳本,則 evalsha 會返回 "NOSCRIPT No matching script. Please use EVAL." 異常,該異常會封裝成 RedisSystemException 拋出。
捕獲異常后,判斷如果異常類型是 NonTransientDataAccessException,且異常信息里包含 "NOSCRIPT" 關(guān)鍵詞,則再通過 eval 命令傳遞完整的腳本來執(zhí)行一次,執(zhí)行完之后會緩存腳本,以后每次調(diào)用只需通過 evalsha 命令傳遞 sha1 即可執(zhí)行。
項目中遇到的問題
負責的項目中有一段 lua 腳本用來做短信發(fā)送頻率的限流處理,服務部署到全新的一套環(huán)境后發(fā)現(xiàn)請求報錯 "NOSCRIPT No matching script. Please use EVAL.",根據(jù)上述介紹,該錯誤表示 redis server 通過傳遞的 sha1 找不到相應的腳本。
該服務目前做法是沒事先通過 script load 預加載腳本,是通過懶加載方式,由第一個請求去做加載操作。因為新的這套環(huán)境 redis 集群也是新搭建的,所以肯定是沒緩存此腳本的,但是按照上述分析,第一個請求 evalsha 失敗后是會執(zhí)行 eval 的。
所以可以推斷是異常類型不是 NonTransientDataAccessException,或者異常信息里沒有包含 "NOSCRIPT" 關(guān)鍵詞,導致異常直接拋出去了。
經(jīng)過排查發(fā)現(xiàn)是前兩周是接入了 sentinel-redis 流控功能引起的問題。
<dependency> <groupId>com.xxx</groupId> <artifactId>xxx-sentinel-spring-boot-starter-redis</artifactId> <version>${xxx-sentinel.version}</version> </dependency>
該模塊會對每一個 redis 命令做攔截,然后通過 sentinel 流控 api 進行包裹。
實際命令是通過 method.invoke() 反射執(zhí)行的。如果執(zhí)行內(nèi)部有異常,會拋出 InvocationTargetException。
綜上,第一次執(zhí)行 evalsha 命令拋出的 "NOSCRIPT" RedisSystemException 被包裝成了 InvocationTargetException 異常,所以在此判斷直接返回 false,導致異常直接拋出了,并沒有執(zhí)行后續(xù)的 eval 命令。
怎么攔截 redis 命令
我們知道 redis 命令都是通過 RedisConnection 對象執(zhí)行的,RedisConnection 是從 RedisConnectionFactory 中 get 的。
RedisConnectionFactory 一般有 jedis、lettuce 這兩種實現(xiàn)。
通過 SpringBoot 自動裝配裝載進 Spring 容器的就是具體的 RedisConnectionFactory 實現(xiàn)。
所以我們可以通過攔截 RedisConnectionFactory 的 getConnection 方法得到 RedisConnection,然后對 RedisConnection 在進行一次代理,這樣所有的 redis 命令就都能走到我們自己的攔截器里了。
解決辦法
回到主題,我們要怎么解決這個問題呢?
- 使用 lua 腳本最好在服務啟動后通過 script load 做預加載。
- 對 redis 命令(不限于)做攔截后,最好返回原始異常。
總結(jié)
- 該問題還是比較坑的,不好復現(xiàn),在遷移新環(huán)境之前,一直沒出現(xiàn)過該問題,主要原因是 sentine-redis 包是最近才引入的,不管 dev、test、prod 各環(huán)境 lua 腳本其實早就已經(jīng)緩存到 redis server 了,走不到第一次加載的邏輯里去。
- 對各種組件的執(zhí)行流程做攔截、擴展前需仔細看下原有的執(zhí)行流程,是否對異常有特殊處理,最好返回原始異常。
到此這篇關(guān)于攔截Redis命令導致的Lua腳本執(zhí)行失敗的問題解決的文章就介紹到這了,更多相關(guān)redis lua執(zhí)行內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解Redis中的簡單動態(tài)字符串和C字符串的區(qū)別
簡單動態(tài)字符串(SDS)和?C?字符串在實現(xiàn)和特性上存在一些區(qū)別,這些區(qū)別使得?SDS?更適合作為?Redis?中字符串對象的內(nèi)部表示,本文給大家介紹一下Redis中的簡單動態(tài)字符串和C字符串的區(qū)別,需要的朋友可以參考下2023-12-12微服務Spring Boot 整合 Redis 實現(xiàn)好友關(guān)注功能
這篇文章主要介紹了微服務Spring Boot 整合 Redis 實現(xiàn) 好友關(guān)注,本文結(jié)合示例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-12-12websocket+redis動態(tài)訂閱和動態(tài)取消訂閱的實現(xiàn)示例
本文主要介紹了websocket+redis動態(tài)訂閱和動態(tài)取消訂閱,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2022-05-05Redis通用命令介紹以及key的層級結(jié)構(gòu)講解
這篇文章主要介紹了Redis通用命令以及key的層級結(jié)構(gòu),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習吧2022-12-12一文搞懂阿里云服務器部署Redis并整合Spring?Boot
這篇文章主要介紹了一文搞懂阿里云服務器部署Redis并整合Spring?Boot,文章圍繞主題展開詳細的內(nèi)容介紹,具有一定的參考價值,需要的小伙伴可以參考一下2022-09-09