用 eval 执行 lua 脚本才能实现点赞/取消点赞的原子切换,因 redis 单脚本串行执行可避免客户端两次请求导致的竞态;脚本需用 sismember 判断后仅执行一次 sadd 或 srem,并严格区分 keys(key)与 argv(user id)。

直接说结论:用 EVAL 执行一个 Lua 脚本,内部用 redis.call("SISMEMBER", ...) 判断再决定调 SADD 还是 SREM,才能真正实现“点赞/取消点赞”的原子切换。靠客户端两次请求(先查再删/加)必然竞态。
为什么不能分开调 SADD 和 SREM
朋友圈点赞状态本质是“集合成员存在性”的二值切换:用户 A 对动态 B,要么在点赞集合里,要么不在。如果客户端先 SISMEMBER 再根据结果发 SADD 或 SREM,中间可能被其他客户端修改——比如你查到“未点赞”,正要发 SADD 时,另一个请求已抢先 SADD,你再加一次就重复了;反之亦然。
Redis 单个 Lua 脚本在服务端串行执行,天然隔离并发写入,这才是原子性的来源。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
EVAL 脚本怎么写才安全
脚本必须显式检查当前状态,并只做一次变更。不要依赖返回值做二次判断,所有逻辑压进 Lua 里:
local key = KEYS[1]
local member = ARGV[1]
local exists = redis.call("SISMEMBER", key, member)
if exists == 1 then
redis.call("SREM", key, member)
return 0 -- 已取消点赞
else
redis.call("SADD", key, member)
return 1 -- 已点赞
end
-
KEYS[1]是点赞集合的 key(如"post:123:likes"),必须传入,不可硬编码 -
ARGV[1]是用户 ID(如"user:456"),作为集合成员 - 返回
1或0表示操作后状态,客户端据此更新 UI - 不用
redis.call("SCARD", key)算总数——那是读操作,不干扰原子性,但别混进这个脚本里增加延迟
实际调用时容易漏掉的细节
很多人写对了脚本,却在调用层翻车:
- 用
EVALSHA前必须先SCRIPT LOAD,否则报错NOSCRIPT No matching script. Please use EVAL. - key 和 user ID 必须严格区分:
KEYS只能放 key(会被 Redis 集群路由校验),ARGV放值;把 user ID 放进KEYS会导致集群模式下CROSSSLOT错误 - 别在脚本里用
math.random()或os.time()——Lua 沙箱禁用这些函数,会直接报Lua redis() command arguments must be strings or integers - 如果业务需要同时更新点赞数(如
INCR一个计数器),得额外注意:计数器和集合 key 必须在同一 slot(例如都加相同 hash tag{post:123}:likes和{post:123}:like_count),否则集群下无法保证原子
最麻烦的不是写脚本,而是确保 key 设计兼容集群、脚本加载流程稳定、以及客户端错误处理覆盖 SCRIPT_KILL 或 TIMEOUT 场景——这些地方一漏,线上就出现“点两次才变状态”或者“一直显示已点赞但实际没加进集合”。










