Redis Lua脚本深度解析

Redis Lua脚本深度解析 Redis Lua脚本深度解析引言Redis从2.6版本开始支持Lua脚本这是Redis最强大的特性之一。Lua脚本允许在Redis服务器端执行原子操作保证多个命令的原子性同时减少网络往返次数提高系统性能。本文将详细介绍Redis Lua脚本的使用方法、常见模式和最佳实践。Lua脚本基础1.1 基本概念Redis Lua脚本具有以下特点原子性Redis保证Lua脚本在执行期间不会被打断串行化Lua脚本中的所有命令串行执行高效性减少网络往返提高性能可扩展性自定义复杂逻辑# 执行简单的Lua脚本 redis-cli EVAL return Hello World 0 # 带参数的脚本 redis-cli EVAL return redis.call(GET, KEYS[1]) 1 mykey # 多参数脚本 redis-cli EVAL return redis.call(SET, KEYS[1], ARGV[1], EX, ARGV[2]) \ 1 mykey hello 601.2 脚本执行命令# EVAL - 执行脚本 redis-cli EVAL return redis.call(GET, KEYS[1]) 1 keyName # EVALSHA - 执行已加载的脚本通过SHA1 redis-cli SCRIPT LOAD return redis.call(GET, KEYS[1]) # 返回: a4904d36a4eb0c3d3c0d8e3a4f9f7b5c8d9e1f2a redis-cli EVALSHA a4904d36a4eb0c3d3c0d8e3a4f9f7b5c8d9e1f2a 1 keyName # SCRIPT EXISTS - 检查脚本是否存在 redis-cli SCRIPT EXISTS a4904d36a4eb0c3d3c0d8e3a4f9f7b5c8d9e1f2a # SCRIPT FLUSH - 清空脚本缓存 redis-cli SCRIPT FLUSH # SCRIPT KILL - 终止正在执行的脚本 redis-cli SCRIPT KILLJava中执行Lua脚本2.1 使用RedisTemplate执行import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.RedisScript; import org.springframework.beans.factory.annotation.Autowired; import java.util.*; public class RedisLuaScriptExecutor { Autowired private RedisTemplateString, String redisTemplate; /** * 执行简单的Lua脚本 */ public String simpleScript() { String script return Hello Lua; RedisScriptString redisScript RedisScript.of(script); return redisTemplate.execute(redisScript); } /** * 执行带参数的Lua脚本 */ public String scriptWithParams(String key, String value) { String script redis.call(SET, KEYS[1], ARGV[1]) return redis.call(GET, KEYS[1]); RedisScriptString redisScript RedisScript.of(script); return redisTemplate.execute( redisScript, Collections.singletonList(key), value ); } /** * 执行带返回值的脚本 */ public ListString scriptWithListResult(String key) { String script local result {} result[1] redis.call(GET, KEYS[1]) result[2] redis.call(EXISTS, KEYS[1]) return result; RedisScriptList redisScript RedisScript.of(script, List.class); return redisTemplate.execute(redisScript, Collections.singletonList(key)); } /** * 执行带条件的脚本 */ public boolean conditionalScript(String key, String expected, String newValue) { String script local current redis.call(GET, KEYS[1]) if current ARGV[1] then redis.call(SET, KEYS[1], ARGV[2]) return 1 else return 0 end; RedisScriptLong redisScript RedisScript.of(script, Long.class); Long result redisTemplate.execute( redisScript, Collections.singletonList(key), expected, newValue ); return result ! null result 1; } }2.2 使用RedisScript接口import org.springframework.data.redis.core.script.RedisScript; import java.util.*; public class LuaScriptDefinitions { /** * 分布式锁Lua脚本 */ public static final String DISTRIBUTED_LOCK_SCRIPT if redis.call(SETNX, KEYS[1], ARGV[1]) 1 then redis.call(EXPIRE, KEYS[1], ARGV[2]) return 1 else return 0 end; /** * 释放锁Lua脚本带校验 */ public static final String RELEASE_LOCK_SCRIPT if redis.call(GET, KEYS[1]) ARGV[1] then return redis.call(DEL, KEYS[1]) else return 0 end; /** * 限流Lua脚本 */ public static final String RATE_LIMIT_SCRIPT local key KEYS[1] local limit tonumber(ARGV[1]) local window tonumber(ARGV[2]) local current tonumber(redis.call(GET, key) or 0) if current 1 limit then return 0 else redis.call(INCR, key) if current 0 then redis.call(EXPIRE, key, window) end return 1 end; /** * 批量获取多个key的脚本 */ public static final String BATCH_GET_SCRIPT local result {} for i, key in ipairs(KEYS) do result[i] redis.call(GET, key) end return result; /** * Hash字段批量更新的脚本 */ public static final String HASH_BATCH_UPDATE_SCRIPT local count 0 for i 1, #ARGV, 2 do redis.call(HSET, KEYS[1], ARGV[i], ARGV[i 1]) count count 1 end return count; }2.3 Lua脚本执行器import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.RedisScript; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.*; Component public class LuaScriptRunner { Autowired private RedisTemplateString, String redisTemplate; /** * 执行分布式锁 */ public boolean acquireLock(String resource, String lockValue, int expireSeconds) { RedisScriptLong script RedisScript.of( LuaScriptDefinitions.DISTRIBUTED_LOCK_SCRIPT, Long.class); Long result redisTemplate.execute( script, Collections.singletonList(lock: resource), lockValue, String.valueOf(expireSeconds) ); return result ! null result 1; } /** * 释放分布式锁 */ public boolean releaseLock(String resource, String lockValue) { RedisScriptLong script RedisScript.of( LuaScriptDefinitions.RELEASE_LOCK_SCRIPT, Long.class); Long result redisTemplate.execute( script, Collections.singletonList(lock: resource), lockValue ); return result ! null result 1; } /** * 执行限流 */ public boolean isAllowed(String userId, int limit, int windowSeconds) { RedisScriptLong script RedisScript.of( LuaScriptDefinitions.RATE_LIMIT_SCRIPT, Long.class); String key ratelimit: userId : (System.currentTimeMillis() / 1000 / windowSeconds); Long result redisTemplate.execute( script, Collections.singletonList(key), String.valueOf(limit), String.valueOf(windowSeconds) ); return result ! null result 1; } /** * 批量获取 */ public ListString batchGet(ListString keys) { RedisScriptList script RedisScript.of( LuaScriptDefinitions.BATCH_GET_SCRIPT, List.class); return redisTemplate.execute(script, keys); } }常用场景3.1 分布式锁public class RedisDistributedLockWithLua { Autowired private RedisTemplateString, String redisTemplate; private static final String LOCK_PREFIX lock:; /** * 获取锁可重入版本 */ public String acquireLockWithLease(String resource, long expireTime, TimeUnit unit) { String lockKey LOCK_PREFIX resource; String lockValue Thread.currentThread().getId() : UUID.randomUUID().toString(); String script if redis.call(SETNX, KEYS[1], ARGV[1]) 1 then redis.call(PEXPIRE, KEYS[1], ARGV[2]) return ARGV[1] else local currentValue redis.call(GET, KEYS[1]) if currentValue ARGV[1] then redis.call(PEXPIRE, KEYS[1], ARGV[2]) return ARGV[1] end return nil end; Long result redisTemplate.execute( RedisScript.of(script, Long.class), Collections.singletonList(lockKey), lockValue, String.valueOf(unit.toMillis(expireTime)) ); return result ! null ? lockValue : null; } /** * 释放锁可重入版本 */ public boolean releaseLockWithLease(String resource, String lockValue) { String lockKey LOCK_PREFIX resource; String script if redis.call(GET, KEYS[1]) ARGV[1] then return redis.call(DEL, KEYS[1]) else return 0 end; Long result redisTemplate.execute( RedisScript.of(script, Long.class), Collections.singletonList(lockKey), lockValue ); return result ! null result 1; } /** * 延长锁过期时间 */ public boolean extendLock(String resource, String lockValue, long extendTime, TimeUnit unit) { String lockKey LOCK_PREFIX resource; String script if redis.call(GET, KEYS[1]) ARGV[1] then return redis.call(PEXPIRE, KEYS[1], ARGV[2]) else return 0 end; Long result redisTemplate.execute( RedisScript.of(script, Long.class), Collections.singletonList(lockKey), lockValue, String.valueOf(unit.toMillis(extendTime)) ); return result ! null result 1; } }3.2 限流器public class RedisRateLimiter { Autowired private RedisTemplateString, String redisTemplate; /** * 滑动窗口限流 */ public boolean isAllowedSlidingWindow(String userId, int maxRequests, int windowSeconds) { String key ratelimit:sliding: userId; long now System.currentTimeMillis(); long windowStart now - windowSeconds * 1000; String script redis.call(ZREMRANGEBYSCORE, KEYS[1], 0, ARGV[1]) local count redis.call(ZCARD, KEYS[1]) if count tonumber(ARGV[3]) then redis.call(ZADD, KEYS[1], ARGV[2], ARGV[2]) redis.call(EXPIRE, KEYS[1], ARGV[4]) return 1 else return 0 end; Long result redisTemplate.execute( RedisScript.of(script, Long.class), Collections.singletonList(key), String.valueOf(windowStart), String.valueOf(now), String.valueOf(maxRequests), String.valueOf(windowSeconds) ); return result ! null result 1; } /** * 令牌桶限流 */ public boolean tryAcquireToken(String key, int capacity, double refillRate, double requested) { String script local tokens_key KEYS[1] local timestamp tonumber(ARGV[1]) local capacity tonumber(ARGV[2]) local refill_rate tonumber(ARGV[3]) local requested tonumber(ARGV[4]) local fill_time capacity / refill_rate local ttl tonumber(redis.call(PTTL, tokens_key)) local stored_ttl redis.call(GET, tokens_key .. :ttl) if stored_ttl then local last_refill_time tonumber(redis.call(GET, tokens_key .. :last)) local time_passed timestamp - last_refill_time local to_add time_passed * refill_rate local current_tokens math.min(capacity, tonumber(redis.call(GET, tokens_key)) to_add) else local current_tokens capacity end if current_tokens requested then redis.call(SET, tokens_key, current_tokens - requested) redis.call(SET, tokens_key .. :last, timestamp) redis.call(SET, tokens_key .. :ttl, fill_time) return 1 else return 0 end; Long result redisTemplate.execute( RedisScript.of(script, Long.class), Collections.singletonList(key), String.valueOf(System.currentTimeMillis()), String.valueOf(capacity), String.valueOf(refillRate), String.valueOf(requested) ); return result ! null result 1; } }3.3 计数器public class RedisAtomicCounter { Autowired private RedisTemplateString, String redisTemplate; /** * 点赞计数去重 */ public boolean like(String userId, String targetId) { String likeSetKey likes:set: targetId; String likeCountKey likes:count: targetId; String script local added redis.call(SADD, KEYS[1], ARGV[1]) if added 1 then redis.call(INCR, KEYS[2]) end return added; Long result redisTemplate.execute( RedisScript.of(script, Long.class), Arrays.asList(likeSetKey, likeCountKey), userId ); return result ! null result 1; } /** * 取消点赞 */ public boolean unlike(String userId, String targetId) { String likeSetKey likes:set: targetId; String likeCountKey likes:count: targetId; String script local removed redis.call(SREM, KEYS[1], ARGV[1]) if removed 1 then redis.call(DECR, KEYS[2]) end return removed; Long result redisTemplate.execute( RedisScript.of(script, Long.class), Arrays.asList(likeSetKey, likeCountKey), userId ); return result ! null result 1; } /** * 获取点赞状态 */ public boolean isLiked(String userId, String targetId) { String likeSetKey likes:set: targetId; return Boolean.TRUE.equals( redisTemplate.opsForSet().isMember(likeSetKey, userId)); } /** * 获取点赞数 */ public long getLikeCount(String targetId) { String likeCountKey likes:count: targetId; String count redisTemplate.opsForValue().get(likeCountKey); return count ! null ? Long.parseLong(count) : 0; } }3.4 排行榜public class RedisLuaLeaderboard { Autowired private RedisTemplateString, String redisTemplate; /** * 增加分数带过期时间 */ public double incrementScoreWithExpiry(String leaderboard, String member, double delta) { String script local newScore redis.call(ZINCRBY, KEYS[1], ARGV[1], ARGV[2]) redis.call(EXPIRE, KEYS[1], ARGV[3]) return newScore; Double result redisTemplate.execute( RedisScript.of(script, Double.class), Collections.singletonList(leaderboard: leaderboard), String.valueOf(delta), member, String.valueOf(7 * 24 * 3600) // 7天过期 ); return result ! null ? result : 0; } /** * 获取排名带分数 */ public MapString, Object getRankWithScore(String leaderboard, String member) { String script local rank redis.call(ZREVRANK, KEYS[1], ARGV[1]) local score redis.call(ZSCORE, KEYS[1], ARGV[1]) if rank then return {rank 1, score} else return {-1, 0} end; ListObject result redisTemplate.execute( RedisScript.of(script, List.class), Collections.singletonList(leaderboard: leaderboard), member ); MapString, Object map new HashMap(); if (result ! null result.size() 2) { map.put(rank, ((Long) result.get(0)).intValue()); map.put(score, Double.parseDouble(result.get(1).toString())); } return map; } }脚本管理4.1 脚本缓存import org.springframework.data.redis.core.script.RedisScript; import java.util.*; public class ScriptCacheManager { Autowired private RedisTemplateString, String redisTemplate; private MapString, String scriptCache new HashMap(); /** * 加载脚本到Redis */ public String loadScript(String script) { if (scriptCache.containsKey(script)) { return scriptCache.get(script); } String sha redisTemplate.execute( (RedisCallbackString) connection - { return connection.scriptingCommands().scriptLoad( script.getBytes()); } ); scriptCache.put(script, sha); return sha; } /** * 执行已加载的脚本 */ public T T executeLoadedScript(RedisScriptT script, ListString keys, Object... args) { String sha loadScript(script.getScriptAsString()); ListString argList new ArrayList(); for (Object arg : args) { argList.add(arg.toString()); } return redisTemplate.execute( RedisScript.of(sha, script.getResultType()), keys, argList.toArray() ); } /** * 检查脚本是否存在 */ public boolean scriptExists(String script) { String sha scriptCache.get(script); if (sha null) { return false; } ListBoolean exists redisTemplate.execute( (RedisCallbackListBoolean) connection - connection.scriptingCommands().scriptExists(sha.getBytes()) ); return exists ! null !exists.isEmpty() exists.get(0); } /** * 清空脚本缓存 */ public void flushScripts() { redisTemplate.execute( (RedisCallbackVoid) connection - { connection.scriptingCommands().scriptFlush(); return null; } ); scriptCache.clear(); } }最佳实践5.1 脚本编写规范-- 良好的Lua脚本规范 -- 1. 使用局部变量代替全局变量 local function helper_function() -- 函数定义 end -- 2. 错误处理 local function safe_call(command) local ok, result pcall(redis.call, command) if not ok then return nil, result end return result, nil end -- 3. 参数校验 local function validate_params(key, value) if not key or #key 0 then return false, key is required end if not value or #value 0 then return false, value is required end return true, nil end -- 4. 日志记录 redis.log(redis.LOG_WARNING, script execution started) redis.log(redis.LOG_NOTICE, operation completed)5.2 性能优化public class LuaScriptOptimization { /** * 脚本优化建议 */ public void optimizationTips() { // 1. 避免过长的脚本 // 2. 使用局部变量 // 3. 预编译脚本并缓存SHA // 4. 减少KEYS数量 // 5. 使用EVALSHA代替EVAL } /** * 预热脚本缓存 */ public void warmUpScriptCache() { ListString scripts Arrays.asList( LuaScriptDefinitions.DISTRIBUTED_LOCK_SCRIPT, LuaScriptDefinitions.RELEASE_LOCK_SCRIPT, LuaScriptDefinitions.RATE_LIMIT_SCRIPT, LuaScriptDefinitions.BATCH_GET_SCRIPT ); scripts.forEach(script - { // 加载脚本到Redis redisTemplate.execute( (RedisCallbackString) connection - connection.scriptingCommands().scriptLoad( script.getBytes()) ); }); } }总结Redis Lua脚本是实现复杂原子操作的有力工具。通过合理使用Lua脚本可以减少网络往返、保证原子性、实现复杂的业务逻辑。在实际应用中需要注意脚本的性能和可维护性避免编写过长或过于复杂的脚本同时建立完善的脚本管理和监控机制。