HSET+EXPIRE非原子操作易导致过期失效或TTL被覆盖,须用Lua脚本封装:通过redis.call('HSET',KEYS[1],ARGV[1],ARGV[2])写入字段、redis.call('EXPIRE',KEYS[1],tonumber(ARGV[3]))重设TTL,确保原子性。

为什么不能直接用HSET+EXPIRE组合更新Hash并续期
因为这两步不是原子的:HSET成功后,EXPIRE可能因网络中断、Redis临时不可用或命令被丢弃而失败,导致key长期不淘汰。业务上表现为“数据还在但本该过期”,尤其在分布式会话、临时配置等场景中容易引发状态错乱。更隐蔽的问题是,如果多个客户端并发执行该组合,后执行的EXPIRE会覆盖前一次设置的TTL,造成过期时间不可控。
HSET + EXPIRE必须封装进Lua脚本才能原子执行
Redis服务端保证整个Lua脚本串行执行,中间不会被打断。关键点在于:所有操作(写字段、设过期)必须在同一个EVAL或EVALSHA调用中完成,且使用redis.call()而非redis.pcall()(除非你明确要捕获错误并降级处理)。
-
redis.call('HSET', KEYS[1], ARGV[1], ARGV[2])写入单个field,支持覆盖或新增 -
redis.call('EXPIRE', KEYS[1], tonumber(ARGV[3]))重置整个key的TTL;注意ARGV[3]必须是字符串形式的整数,如"3600",传3600(number)在某些Redis版本会报错 - 若需批量更新多个field,把
HSET替换成HMSET(Redis HSET带多组field/value参数(Redis ≥ 7.0),但参数总数不能超1024
读取时如何确认字段是否有效——不能只靠HGET
HGET本身不返回过期信息,哪怕key已过期但尚未被惰性删除,HGET仍可能返回旧值。真正可靠的判断方式是:先EXISTS检查key是否存在,再HGET取值。但要注意:EXISTS返回1只说明key存在且未过期,返回0可能是key真不存在,也可能是已过期但还未被清理(惰性删除机制决定的)。所以业务逻辑里应把EXISTS为0视作“不可用”,避免后续误用。
示例调用:EVAL "redis.call('HSET', KEYS[1], ARGV[1], ARGV[2]); redis.call('EXPIRE', KEYS[1], tonumber(ARGV[3])); return 1" 1 user:1001 token "abc123" "1800"
高并发下更新部分字段时,别用HGETALL+HMSET重构整个Hash
常见误区是:先HGETALL读出全部字段 → 在Lua里修改其中几个 → 再HMSET全量写回。这会导致两个问题:一是大Hash(几千字段)阻塞Redis主线程;二是HGETALL返回的是扁平数组{'f1','v1','f2','v2'},遍历时容易索引错位;三是HMSET会覆盖所有未显式指定的字段,相当于“清空再重建”,破坏原语义。
正确做法是只用HSET更新目标字段,其他字段不动。如果确实需要基于原值计算(比如给score字段加1),就用HGET单独读那个field,再HSET写回,避免全量加载。
EXPIRE会静默失败或报错ERR value is not an integer or out of range。


















