一、Lua脚本操作Redis的优势
特性 | 说明 |
---|
原子性 | Lua脚本在Redis中单线程执行,所有操作要么全部成功,要么全部失败。 |
减少网络开销 | 将多个Redis命令合并为一个脚本执行,减少客户端与服务端的通信次数。 |
复杂逻辑封装 | 可实现条件判断、循环、计算等复杂逻辑(Redis原生命令无法直接实现)。 |
集群兼容性 | 通过显式声明KEYS ,保证在Redis集群模式下正确路由到目标节点。 |
二、Lua脚本基本规范
1. 参数传递
2. 返回值
- Lua脚本的最后一个值会作为执行结果返回给客户端。
- 可返回
nil
、数值、字符串、表(自动转为Redis多行回复)。
3. 脚本编写原则
- 禁止使用全局变量:所有变量需用
local
声明。 - 避免长耗时操作:Lua脚本会阻塞Redis其他请求,需确保高效性。
三、常用Lua脚本操作
1. 数据操作
操作 | 示例代码 |
---|
String | redis.call('SET', KEYS[1], ARGV[1]) |
Hash | redis.call('HSET', KEYS[1], 'field', ARGV[1]) |
Set | redis.call('SADD', KEYS[1], ARGV[1]) |
ZSet | redis.call('ZADD', KEYS[1], ARGV[1], ARGV[2]) |
List | redis.call('LPUSH', KEYS[1], ARGV[1]) |
2. 条件判断
if redis.call('EXISTS', KEYS[1]) == 1 thenreturn redis.call('GET', KEYS[1])
elsereturn nil
end
3. 循环操作
for i=1, #ARGV doredis.call('SADD', KEYS[1], ARGV[i])
end
4. 错误处理
local ok, err = pcall(redis.call, 'INCRBY', KEYS[1], ARGV[1])
if not ok thenreturn {err = err}
end
四、注意事项
场景 | 解决方案 |
---|
集群模式 | 确保所有KEYS 在同一个哈希槽(可通过Hash Tag实现,如{user}:123:likes )。 |
脚本性能 | 避免复杂循环或大范围数据遍历,优先用Redis原生命令。 |
脚本缓存 | 使用SCRIPT LOAD 预加载脚本,通过EVALSHA 执行(减少网络传输)。 |
调试脚本 | 通过redis.log(redis.LOG_DEBUG, 'message') 输出日志(需配置Redis日志级别)。 |
五、典型应用场景
场景 | Lua脚本作用 |
---|
分布式锁 | 原子化实现锁的获取、续期、释放(避免锁误删)。 |
计数器 | 原子化增减计数(如点赞数、库存扣减)。 |
排行榜 | 计算分数并更新ZSet,返回排名结果。 |
批量操作 | 合并多个命令(如先检查条件再删除数据)。 |
六、调试与测试
- 直接执行脚本(通过
redis-cli
):redis-cli --eval script.lua key1 key2 , arg1 arg2
- 捕获错误:
local ok, result = pcall(redis.call, 'COMMAND', params)
if not ok thenreturn {error = result}
end
点赞实现
编写lua脚本
public class RedisLuaScript {public static final RedisScript<Long> LIKE_SCRIPT = new DefaultRedisScript<>("local tempLikeKey = KEYS[1]\n" +"local userLikeKey = KEYS[2]\n" +"local userId = ARGV[1]\n" +"local picId = ARGV[2]\n" +"\n" +"-- 1. 检查是否已点赞(避免重复操作)\n" +"if redis.call('HEXISTS', userLikeKey, picId) == 1 then\n" +" return -1\n" +"end\n" +"\n" +"-- 2. 获取旧值(不存在则默认为 0)\n" +"local hashKey = userId .. ':' .. picId\n" +"local oldNumber = tonumber(redis.call('HGET', tempLikeKey, hashKey) or 0)\n" +"\n" +"-- 3. 计算新值\n" +"local newNumber = oldNumber + 1\n" +"\n" +"-- 4. 原子性更新:写入临时计数 + 标记用户已点赞\n" +"redis.call('HSET', tempLikeKey, hashKey, newNumber)\n" +"redis.call('HSET', userLikeKey, picId, 1)\n" +"\n" +"return 1", Long.class);public static final RedisScript<Long> UNLIKE_SCRIPT = new DefaultRedisScript<>("local tempLikeKey = KEYS[1]\n" + "local userLikeKey = KEYS[2]\n" +"local userId = ARGV[1]\n" +"local picId = ARGV[2]\n" +"\n" + "-- 1. 检查用户是否已点赞(若未点赞,直接返回失败)\n" +"if redis.call('HEXISTS', userLikeKey, picId) ~= 1 then\n" +" return -1\n" +"end\n" +"\n" +"-- 2. 获取当前临时计数(若不存在则默认为 0)\n" +"local hashKey = userId .. ':' .. picId\n" +"local oldNumber = tonumber(redis.call('HGET', tempLikeKey, hashKey) or 0)\n" +"\n" +"-- 3. 计算新值并更新\n" +"local newNumber = oldNumber - 1\n" +"\n" +"-- 4. 原子性操作:更新临时计数 + 删除用户点赞标记\n" +"redis.call('HSET', tempLikeKey, hashKey, newNumber)\n" +"redis.call('HDEL', userLikeKey, picId)\n" +"\n" + "return 1", Long.class);
}
执行lua脚本
redisTemplate.execute(RedisLuaScriptConstant.LIKE_SCRIPT,Arrays.asList(tempLikeKey,userLikeKey),loginUser.getId(),picId
);
redisTemplate.execute(RedisLuaScriptConstant.LIKE_SCRIPT,Arrays.asList(tempLikeKey,userLikeKey),loginUser.getId(),picId
);