文章目录

Redis Lua脚本保证原子操作的原理与实战案例

发布于 2026-04-28 15:31:05 · 浏览 3 次 · 评论 0 条

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等。

优化 脚本性能,避免在脚本中进行不必要的计算或类型转换。

评论 (0)

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

扫一扫,手机查看

扫描上方二维码,在手机上查看本文