Redis Lua脚本实战:5分钟搞定分布式锁与原子计数器
Redis Lua脚本实战分布式锁与原子计数器的高效实现1. 为什么选择Lua脚本解决分布式问题在分布式系统中数据一致性始终是开发者面临的核心挑战之一。传统单机环境下的锁机制和原子操作在分布式场景下往往失效而Redis的Lua脚本功能恰好填补了这一空白。Lua脚本在Redis中的两大核心优势原子性保证整个脚本作为一个整体执行中间不会被其他命令插入网络优化多个操作合并为一个脚本执行减少网络往返时间-- 简单示例原子性递增 local current redis.call(GET, KEYS[1]) current tonumber(current) or 0 redis.call(SET, KEYS[1], current 1) return current 1注意Redis 7.0默认使用效果复制而非脚本复制性能更优2. 分布式锁的完整实现方案2.1 基础锁实现分布式锁需要解决三个核心问题互斥性锁只能被一个客户端持有避免死锁锁必须有超时机制释放锁的安全性只能由锁持有者释放-- tryLock.lua local key KEYS[1] local value ARGV[1] -- 建议使用UUID等唯一标识 local ttl tonumber(ARGV[2]) if redis.call(SETNX, key, value) 1 then redis.call(PEXPIRE, key, ttl) return 1 else return 0 end2.2 锁续期与释放锁的自动续期看门狗机制和安全的释放策略-- renewLock.lua local key KEYS[1] local value ARGV[1] local ttl tonumber(ARGV[2]) if redis.call(GET, key) value then return redis.call(PEXPIRE, key, ttl) else return 0 end-- releaseLock.lua local key KEYS[1] local value ARGV[1] if redis.call(GET, key) value then return redis.call(DEL, key) else return 0 end2.3 Spring Boot集成示例Bean public RedisScriptLong tryLockScript() { ScriptSource scriptSource new ResourceScriptSource( new ClassPathResource(lua/tryLock.lua)); DefaultRedisScriptLong script new DefaultRedisScript(); script.setScriptSource(scriptSource); script.setResultType(Long.class); return script; } public boolean tryLock(String key, String value, long ttl) { Long result redisTemplate.execute( tryLockScript(), Collections.singletonList(key), value, String.valueOf(ttl)); return result ! null result 1; }3. 原子计数器的进阶用法3.1 基础计数器-- atomicIncr.lua local key KEYS[1] local increment tonumber(ARGV[1]) local max tonumber(ARGV[2]) -- 可选最大值 local current tonumber(redis.call(GET, key)) or 0 if max and current increment max then return {err Exceed maximum value} end return redis.call(INCRBY, key, increment)3.2 带限流的计数器-- rateLimitedCounter.lua local key KEYS[1] local window tonumber(ARGV[1]) -- 时间窗口(秒) local limit tonumber(ARGV[2]) -- 限制次数 local current tonumber(redis.call(GET, key)) or 0 if current limit then return 0 end redis.call(INCR, key) if current 0 then redis.call(EXPIRE, key, window) end return 13.3 性能对比操作方式网络开销原子性复杂度单命令1次是低事务(MULTI)2N次是中Lua脚本1次是高4. 生产环境最佳实践4.1 脚本缓存优化使用SCRIPT LOAD预加载脚本通过SHA1值执行# 预加载脚本 SCRIPT LOAD return redis.call(GET, KEYS[1]) # 返回: 55b22c0d0cedf3866879ce7c854970626dcef0c3 # 通过SHA1执行 EVALSHA 55b22c0d0cedf3866879ce7c854970626dcef0c3 1 mykey4.2 错误处理策略Redis提供了两种命令执行方式redis.call()出错时直接返回错误redis.pcall()出错时返回Lua table包含错误信息-- 安全调用示例 local ok, result pcall(function() return redis.call(INCR, counter) end) if not ok then redis.log(redis.LOG_WARNING, Counter increment failed: ..result) return 0 end return result4.3 集群环境注意事项在Redis Cluster中所有操作的key必须属于同一个slot使用hash tag确保相关key路由到同一节点-- 集群安全示例 local order_key {order123}:info local items_key {order123}:items redis.call(HSET, order_key, status, paid) redis.call(EXPIRE, items_key, 3600)4.4 性能监控指标关键指标监控建议used_memory_scripts脚本缓存内存使用number_of_cached_scripts缓存的脚本数量script_cpu_time脚本执行CPU耗时# 查看脚本相关指标 redis-cli info memory | grep scripts redis-cli info stats | grep script5. 真实业务场景案例5.1 库存扣减场景-- deductStock.lua local stock_key KEYS[1] local order_key KEYS[2] local product_id ARGV[1] local quantity tonumber(ARGV[2]) local user_id ARGV[3] local stock tonumber(redis.call(HGET, stock_key, product_id)) or 0 if stock quantity then return {err Insufficient stock} end redis.call(HINCRBY, stock_key, product_id, -quantity) redis.call(HSET, order_key, product_id, quantity) redis.call(EXPIRE, order_key, 86400) -- 订单有效期1天 return {ok user_id..s order placed}5.2 秒杀系统实现-- flashSale.lua local stock_key KEYS[1] local success_key KEYS[2] local user_id ARGV[1] local max_per_user 2 -- 每人限购2件 -- 检查用户是否已购买 local bought tonumber(redis.call(HGET, success_key, user_id)) or 0 if bought max_per_user then return {err Purchase limit reached} end -- 检查库存 local stock tonumber(redis.call(GET, stock_key)) or 0 if stock 0 then return {err Sold out} end -- 扣减库存并记录 redis.call(DECR, stock_key) redis.call(HINCRBY, success_key, user_id, 1) return {ok Purchase successful}5.3 分布式ID生成器-- nextId.lua local key KEYS[1] local step tonumber(ARGV[1]) or 1 local min tonumber(ARGV[2]) or 100000 -- 起始ID local max tonumber(ARGV[3]) or 999999 -- 最大ID local current tonumber(redis.call(GET, key)) or (min - step) local next current step if next max then next min -- 循环使用 end redis.call(SET, key, next) return next