分布式锁
单体应用可以使用 synchronized 或 Lock 来加锁,synchronized 推荐使用类锁,也就是字节码锁,这样保证是全局唯一的,如果使用对象锁,要根据业务确定这个对象锁在这个业务中是唯一的。
对于微服务架构下,单体应用锁就不合适了,每个服务多个节点部署,虚拟机都不是用一个,肯定保证不了唯一性
LUA 脚本
redis 通过 EVAL
来执行 lua 脚本,格式为:EVAL <lua_script> <numkeys> <key1> <key2> ... <keyN> <value1> <value2> ... <valueM>
-
numkeys:键的个数
- 如果
numkeys
为 0,不用传参,比如:EVAL "return redis.call('set', 'name', 'jack')" 0
- 如果
numkeys
不为 0,需要传入对应数量的键,比如 :EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 name Marry
- 如果
-
key valye:参数,键是必须的,值是可选的(值个数可以大于键个数,也可以小于键个数,也可以等于键个数)
EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 name Marry abc
合法的,abc
不会被使用EVAL "return redis.call('set', KEYS[1], KEYS[2])" 2 name Marry
合法的,两个参数都视为键EVAL "redis.call('lpush', KEYS[1], ARGV[1]) redis.call('lpush', KEYS[1], ARGV[2])" 1 mylist value1 value2
合法的,1个键,两个值
-
lua_script:lua 脚本
-
取参数的 key:
KEYS[下标]
,lua 第一项是 1(不是从 0 开始,取第一个 key 要写成KEYS[1]
) -
取参数的 value:
ARGV[下标]
-
判断参数规则
EVAL "lua_script" 1 xxx xxx
,1 个键,第一个是键,第二个是值 ==>key value
EVAL "lua_script" 1 xxx xxx xxx
,1 个键,第一个是键,后面两个都是值 ==>key value value
EVAL "lua_script" 2 xxx xxx
,2 个键,参数是 2 个键,没有值 ==>key key
EVAL "lua_script" 2 xxx xxx xxx
,2 个键,前面两个是键,第三个是值 ==>key key valye
EVAL "lua_script" 2 xxx xxx xxx xxx xxx
,2 个键,前面两个是键,后面三个是值 ==>key key value value value
-
条件判断
EVAL " local score = tonumber(redis.call('get', KEYS[1])) -- tonumber 是 lua 内置函数,把一个值转成数字 if score >= 90 then -- 每一个 if 后面要跟一个 then(不是花括号) redis.call('set', KEYS[2], '优秀') -- 设置 90+ 的等级 elseif score >= 80 then -- 每一个 if 后面要跟一个 then redis.call('set', KEYS[2], '良好') -- 设置 80+ 的等级 elseif score >= 70 then -- 每一个 if 后面要跟一个 then redis.call('set', KEYS[2], '中等') -- 设置 70+ 的等级 else -- 结尾的 else 不跟 then redis.call('set', KEYS[2], '需要改进') -- 设置小于 70 的等级 end -- if 代码块用 end 结尾 return redis.call('get', KEYS[2]) -- 返回设置的等级 " 2 user:score user:status -- 参数:2个参数,只有 key 没有 value
-
-
示例
# 执行 set name "jack"(0 表示没有参数传进脚本) EVAL "return redis.call('set', 'name', 'jack')" 0 # 执行 mset name "Rose" age 22(设置多个值,也没有参数) EVAL "return redis.call('mset', 'name', 'Rose', 'age', 22)" 0 # 带参数的示例,执行命令: mset name "Marry" age 23。2 表示 2 个键,因为脚本有 ARGV 所以参数被视为键值对 EVAL "return redis.call('mset', KEYS[1], ARGV[1], KEYS[2], ARGV[2])" 2 name age Marry 23 # 判断 key 再删除(官网示例) # if redis.call('get', 'name') == 'Rose' then ---- 如果 name 的值是 Rose # return redis.call('del', name) ---- 删除 name,删除成功会返回 1 # else ---- 如果条件不成立 # return 0 ---- 就不删除,返回0 # end ---- 结束 EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 name Rose
SETNX
SETNX key value
当 key 不存在时添加缓存,等同于SET key value NX
。最简易版的锁EXPIRE key seconds
单独给 key 指定过期时间。给锁加过期时间,避免死锁SET key value NX EX seconds
当 key 不存在时添加,并设置过期时间(具有原子性,如果要使用简易的锁用这个)SETEX
添加一个 key 并设置过期时间。这个不适合用作锁,因为当锁存在就不允许添加,这个命令会覆盖原来的值
SETNX 问题
-
锁误删除
当业务时间大于锁过期时间时会发生。假设锁 tll 是 10 秒,但是业务需要 20s。10 秒后锁自动删除,业务结束时自己家的锁过期了,再删除的锁就是别的线程加的
解决方案是给 value 设置特殊的值,当删除锁时,先获取锁的值,对比一样才删除,反之不删除(不一样说明自己加的锁过期自动删除了)
-
删除锁不是原子操作
当删除锁时,是先判断锁,然后存在再删除(避免误删),这是两条命令,不具有原子性
解决方案是使用 lua 脚本
-
不支持重入
- 不能使用 String 数据类型,要使用 Hash 数据类型。key - field - value ==> 锁 - 值 - 重入次数
- Hash 的命令要灵活使用,比如
HINCRBY
不存在会自动创建 - 要结合程序编码,当同一个线程获取锁,使用 lua 脚本让 hash 的 filed 的值 +1
-
不支持续期
- 结合程序编码,每添加锁后同时创建一个循环的定时任务
- 每当 ttl 时间过去 1/3,触发一次定时任务的调用
- 定时任务就是检查锁是否还存在,如果存在就把 ttl 恢复成初始时间
- 如果定时任务发现锁不存在了,说明业务结束删除了锁,定时任务也就终止了
RedLock
名字叫红锁,这其实是一种算法
解决分布式缓存系统中锁丢失的方案(锁丢失就是锁不存在了,所以能重新加锁,也可以理解为锁失效)
产生原因:当加锁成功后,主节点还没来得及同步给从节点,主节点宕机,锁就丢失了
解决方案:
- 锁和缓存的 redis 实例要区分开来,缓存的 redis 不保存锁
- 用于锁的 redis 实例都是单独的 master(没有主从,也不是集群)
- 加锁和释放锁要在所有的实例都操作,大部分实例操作成功,才算成功
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具