Redis Lua脚本保证原子操作的原理与实战案例
Redis Lua脚本基础
了解 Redis Lua脚本的概念和基本使用是开始的第一步。Redis从2.6.0版本开始引入Lua脚本功能,允许用户在服务器端执行自定义逻辑。
掌握 EVAL命令的基本语法,这是执行Lua脚本的主要方式:
EVAL script numkeys key [key ...] arg [arg ...]
其中:
script:Lua脚本代码numkeys:使用的键数量key [key ...]:脚本中使用的键arg [arg ...]:脚本所需的额外参数
认识 EVALSHA命令,它是EVAL的优化版本,通过脚本的SHA1哈希值来执行已缓存的脚本,减少网络传输:
EVALSHA sha1 numkeys key [key ...] arg [arg ...]
学习 如何通过redis.call()函数在Lua脚本中调用Redis命令:
redis.call('SET', KEYS[1], ARGV[1])
Redis Lua脚本原子操作原理
理解 Redis Lua脚本保证原子性的核心原理:Redis使用单个Lua解释器运行所有脚本,并保证脚本以原子方式执行。当某个脚本运行时,不会有其他脚本或Redis命令被执行。
认识 Redis的执行模型:Redis是单线程模型,Lua脚本执行期间会阻塞其他命令,形成一个完整的原子操作单元。
区分 redis.call()和redis.pcall()的错误处理方式:
redis.call():遇到错误时会中断脚本执行并将错误抛回客户端redis.pcall():遇到错误会捕获错误并返回nil和错误信息,不会中断脚本执行
掌握 脚本参数的访问方式:
- 键通过
KEYS[1]、KEYS[2]等方式访问 - 附加参数通过
ARGV[1]、ARGV[2]等方式访问
注意 Redis中的数组下标是从1开始的,与多数编程语言不同。
实战案例一:原子计数器
实现 一个简单但实用的原子计数器,使用Lua脚本来确保递增操作的原子性:
local current = redis.call('GET', KEYS[1])
if current == false then
current = 0
else
current = tonumber(current)
end
current = current + 1
redis.call('SET', KEYS[1], current)
return current
执行 此脚本:
EVAL "local current = redis.call('GET', KEYS[1]); if current == false then current = 0 else current = tonumber(current) end; current = current + 1; redis.call('SET', KEYS[1], current); return current" 1 counter
验证 结果:无论多少个客户端同时执行此脚本,计数器都会正确递增,不会出现计数丢失或重复的问题。
实战案例二:分布式锁
实现 一个简单但实用的分布式锁,使用Lua脚本来确保获取锁和释放锁的原子性:
-- KEYS[1]: 锁的key
-- ARGV[1]: 锁的过期时间(毫秒)
-- ARGV[2]: 请求标识
-- 尝试获取锁
if redis.call('SET', KEYS[1], ARGV[2], 'NX', 'PX', ARGV[1]) == 'OK' then
return 1
else
return 0
end
释放锁 的Lua脚本:
-- KEYS[1]: 锁的key
-- ARGV[1]: 请求标识
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
else
return 0
end
使用 Java代码实现:
// 获取锁
String lockKey = "my_lock";
String requestId = UUID.randomUUID().toString();
Long expireTime = 30000L; // 30秒
String lockScript = "if redis.call('SET', KEYS[1], ARGV[2], 'NX', 'PX', ARGV[1]) == 'OK' then return 1 else return 0 end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(lockScript, Long.class);
Boolean locked = redisTemplate.execute(redisScript, Collections.singletonList(lockKey), expireTime.toString(), requestId);
// 释放锁
String unlockScript = "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end";
DefaultRedisScript<Long> unlockRedisScript = new DefaultRedisScript<>(unlockScript, Long.class);
redisTemplate.execute(unlockRedisScript, Collections.singletonList(lockKey), requestId);
实战案例三:滑动窗口限流
实现 一个滑动窗口限流算法,使用Lua脚本保证请求处理的原子性:
-- KEYS[1]: 限流key
-- ARGV[1]: 当前时间戳(毫秒)
-- ARGV[2]: 时间窗口大小(毫秒)
-- ARGV[3]: 阈值(允许的请求数)
-- 移除窗口外的请求
redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, ARGV[1] - ARGV[2])
-- 添加当前请求
redis.call('ZADD', KEYS[1], ARGV[1], ARGV[1])
-- 设置过期时间
redis.call('EXPIRE', KEYS[1], ARGV[2] / 1000)
-- 获取窗口内的请求数量
local current = redis.call('ZCARD', KEYS[1])
-- 判断是否超过阈值
if current <= tonumber(ARGV[3]) then
return 1
else
return 0
end
执行 此脚本来控制请求频率:
EVAL "redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, ARGV[1] - ARGV[2]); redis.call('ZADD', KEYS[1], ARGV[1], ARGV[1]); redis.call('EXPIRE', KEYS[1], ARGV[2] / 1000); local current = redis.call('ZCARD', KEYS[1]); if current <= tonumber(ARGV[3]) then return 1 else return 0 end" 1 rate_limit 1672531200000 60000 100
实战案例四:礼品领取系统
实现 一个防止重复领取和超发的礼品领取系统:
-- KEYS[1]: 领取记录key
-- KEYS[2]: 礼品数量key
-- ARGV[1]: 用户ID
-- 先判断该用户是否领过礼品
local isMember = redis.call('SISMEMBER', KEYS[1], ARGV[1])
if isMember == 1 then
return 1 -- 已领取过
end
-- 获取当前礼品数量
local giftNum = redis.call('GET', KEYS[2])
if not giftNum then
giftNum = 0
else
giftNum = tonumber(giftNum)
end
-- 判断是否有剩余礼品
if giftNum <= 0 then
return 0 -- 礼品已领完
end
-- 减少礼品数量
redis.call('DECR', KEYS[2])
-- 记录用户领取
redis.call('SADD', KEYS[1], ARGV[1])
return 2 -- 领取成功
执行 示例:
EVAL "local isMember = redis.call('SISMEMBER', KEYS[1], ARGV[1]); if isMember == 1 then return 1 end; local giftNum = redis.call('GET', KEYS[2]); if not giftNum then giftNum = 0 else giftNum = tonumber(giftNum) end; if giftNum <= 0 then return 0 end; redis.call('DECR', KEYS[2]); redis.call('SADD', KEYS[1], ARGV[1]); return 2" 2 record giftNum user123
最佳实践与注意事项
控制 脚本执行时间,避免长时间运行的脚本阻塞Redis服务器。复杂的业务逻辑应拆分为多个简单脚本。
使用 EVALSHA替代EVAL,利用Redis的脚本缓存机制减少网络传输。
避免 在脚本中使用循环,特别是可能执行时间较长的循环。
处理 错误情况,使用redis.pcall替代redis.call时要注意检查返回值。
限制 KEYS数组的大小,推荐不超过10000个键。
管理 脚本复杂度,保持脚本简洁易读,必要时添加注释。
考虑 使用Redis的SCRIPT命令管理脚本,如SCRIPT FLUSH、SCRIPT EXISTS等。
优化 脚本性能,避免在脚本中进行不必要的计算或类型转换。

暂无评论,快来抢沙发吧!