用Lua腳本實現(xiàn)Redis原子操作的示例
1. 環(huán)境準備
依賴:在pom.xml
中添加Spring Data Redis:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
配置RedisTemplate:
@Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); return template; } }
2. 編寫Lua腳本
以分布式鎖為例,實現(xiàn)加鎖和解鎖的原子操作:
加鎖腳本 lock.lua
local key = KEYS[1] local value = ARGV[1] local expire = ARGV[2] -- 如果key不存在則設(shè)置,并添加過期時間 if redis.call('setnx', key, value) == 1 then redis.call('expire', key, expire) return 1 -- 加鎖成功 else return 0 -- 加鎖失敗 end
解鎖腳本 unlock.lua
local key = KEYS[1] local value = ARGV[1] -- 只有鎖的值匹配時才刪除 if redis.call('get', key) == value then return redis.call('del', key) else return 0 end
3. 加載并執(zhí)行腳本
定義腳本Bean:
@Configuration public class LuaScriptConfig { @Bean public DefaultRedisScript<Long> lockScript() { DefaultRedisScript<Long> script = new DefaultRedisScript<>(); script.setLocation(new ClassPathResource("lock.lua")); script.setResultType(Long.class); return script; } }
調(diào)用腳本:
@Service public class RedisLockService { @Autowired private RedisTemplate<String, Object> redisTemplate; @Autowired private DefaultRedisScript<Long> lockScript; public boolean tryLock(String key, String value, int expireSec) { List<String> keys = Collections.singletonList(key); Long result = redisTemplate.execute( lockScript, keys, value, String.valueOf(expireSec) ); return result != null && result == 1; } }
開發(fā)中的常見問題與解決方案
1. Lua腳本緩存問題
- 問題:每次執(zhí)行腳本會傳輸整個腳本內(nèi)容,增加網(wǎng)絡(luò)開銷。
- 解決:Redis會自動緩存腳本并返回SHA1值,Spring Data Redis的
DefaultRedisScript
會自動管理SHA1。確保腳本對象是單例,避免重復加載。
2. 參數(shù)傳遞錯誤
問題:KEYS
和ARGV
數(shù)量或類型不匹配,導致腳本執(zhí)行失敗。
解決:明確區(qū)分參數(shù)類型:
// 正確傳參示例 List<String> keys = Arrays.asList("key1", "key2"); // KEYS數(shù)組 Object[] args = new Object[]{"arg1", "arg2"}; // ARGV數(shù)組
3. Redis集群兼容性
問題:集群模式下,所有操作的Key必須位于同一slot。
解決:使用{}
定義hash tag,強制Key分配到同一節(jié)點:
String key = "{user}:lock:" + userId; // 所有包含{user}的Key分配到同一節(jié)點
4. 腳本性能問題
問題:復雜Lua腳本可能阻塞Redis,影響性能。
解決:
- 避免在Lua中使用循環(huán)或復雜邏輯。
- 優(yōu)先使用Redis內(nèi)置命令(如
SETNX
、EXPIRE
)。
5. 異常處理
問題:腳本執(zhí)行超時或返回非預期結(jié)果。
解決:捕獲異常并設(shè)計重試機制:
public boolean tryLockWithRetry(String key, int maxRetry) { int retry = 0; while (retry < maxRetry) { if (tryLock(key, "value", 30)) { return true; } retry++; Thread.sleep(100); // 短暫等待 } return false; }
完整示例:分布式鎖
// 加鎖 public boolean lock(String key, String value, int expireSec) { return redisTemplate.execute( lockScript, Collections.singletonList(key), value, String.valueOf(expireSec) ) == 1; } // 解鎖 public void unlock(String key, String value) { Long result = redisTemplate.execute( unlockScript, Collections.singletonList(key), value ); if (result == null || result == 0) { throw new RuntimeException("解鎖失?。烘i已過期或非持有者"); } }
調(diào)試與優(yōu)化建議
Redis CLI調(diào)試:
# 直接在Redis服務(wù)器測試腳本 EVAL "return redis.call('setnx', KEYS[1], ARGV[1])" 1 mykey 123
日志配置:
# application.properties logging.level.org.springframework.data.redis=DEBUG
監(jiān)控腳本執(zhí)行時間:
# Redis慢查詢?nèi)罩? slowlog-log-slower-than 5 slowlog-max-len 128
總結(jié)
通過Lua腳本,可以輕松實現(xiàn)Redis復雜操作的原子性,解決高并發(fā)下的競態(tài)條件問題。在Spring Boot中,結(jié)合RedisTemplate
和DefaultRedisScript
,能夠高效集成Lua腳本。開發(fā)時需注意參數(shù)傳遞、集群兼容性和異常處理,避免踩坑。
到此這篇關(guān)于用Lua腳本實現(xiàn)Redis原子操作的示例的文章就介紹到這了,更多相關(guān)Lua腳本實現(xiàn)Redis原子操作內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
基于Redis實現(xiàn)短信驗證碼登錄項目示例(附源碼)
手機登錄驗證在很多網(wǎng)頁上都得到使用,本文主要介紹了基于Redis實現(xiàn)短信驗證碼登錄項目示例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2022-05-05