Redis Hash 仅支持 key 级过期,不支持字段级 TTL;必须用 EXPIRE/PEXPIRE 对整个 hash key 设置过期时间,HSET 等操作本身无过期参数;字段级过期需通过独立 string key、元数据 hash 或外部机制模拟。

Redis Hash 本身不支持字段级过期,只能对整个 key 设置 TTL;所谓“自动过期”,本质是给 myhash 这个 key 设置过期时间,而不是给 myhash.field1 单独设。
Hash 整体 key 设置过期是唯一原生支持的方式
Redis 的过期机制只作用于 key 级别,HSET、HMSET、HINCRBY 等所有 hash 操作都不接受过期参数。你必须在写入后单独调用 EXPIRE 或 PEXPIRE:
127.0.0.1:6379> HSET user:1001 name "Alice" age "30" (integer) 2 127.0.0.1:6379> EXPIRE user:1001 3600 (integer) 1
一旦 user:1001 过期,整个 hash(包括所有 field)被原子删除。这是唯一无需额外逻辑就能“自动过期”的路径。
- 必须在
HSET成功后立即执行EXPIRE,否则存在写入成功但未设 TTL 的风险 - 推荐用 Lua 脚本封装,保证原子性:
EVAL "redis.call('HSET', KEYS[1], unpack(ARGV)); redis.call('EXPIRE', KEYS[1], ARGV[#ARGV]);" 1 myhash field1 val1 field2 val2 3600 -
EXPIREAT更适合业务中已知绝对过期时间戳的场景(如优惠券截止时间)
字段级过期必须靠外部模拟,没有银弹
想让 user:1001.name 5 分钟过期、user:1001.token 2 小时过期?Redis 原生不支持。常见模拟方案有三种,各自有硬伤:
- 为每个 field 单独建一个 string key,如
user:1001:name+EXPIRE,再用HGET和GET配合读取 —— 读写放大,一致性难保 - 在 hash 外另存一个过期元数据 hash,如
user:1001:expire存{"name": "1744625220", "token": "1744626000"},每次读前检查并触发HDEL—— 需要定时扫描或读时惰性清理,无法真正“自动” - 用 Redis Stream 或 Sorted Set 实现延时队列,把过期事件推到消费者处理 —— 架构复杂,延迟不可控,适合大粒度任务而非字段生命周期管理
注意:threading.Timer 或后台线程轮询 TTL 的做法在生产环境极不推荐:进程重启即失效、多实例竞争、TTL 返回 -2(key 不存在)或 -1(无过期)需仔细判别。
定期删除策略不适用于字段级时效控制
Redis 的定期删除(active expiry)只扫描 keyspace,不会深入 hash 内部遍历 field。即使你手动在代码里每秒跑 HGETALL + time.time() 对比时间戳,也属于应用层轮询,不是 Redis 自动行为。
- Redis 不会因为某个 field 的值里存了时间戳,就自动删它 —— 它只认 key 的
expires字典项 - 高频轮询
HLEN或HKEYS会造成额外网络和 CPU 开销,尤其当 hash 字段数上万时 - 若依赖
EXISTS+HGET组合判断“字段是否有效”,需自行处理并发写入覆盖导致的时间戳错乱
真正需要字段级过期时,该换数据结构了
如果业务强依赖不同字段不同生命周期(比如 session 中的 csrf_token、last_active_at、login_ip 各自过期策略不同),继续硬套 hash 是自找麻烦。
- 改用多个独立 string key:语义清晰、过期可控、命令简单,只是 key 数量变多
- 改用 JSON 存入 string,配合客户端解析 + 时间戳字段 + 定时 job 清理(如用 Redisearch 或 Lua 扫描)
- 引入外部存储(如 PostgreSQL 的
pg_cron+jsonb)处理复杂生命周期逻辑,Redis 仅作高速缓存
最常被忽略的一点:很多人试图用 hash 模拟关系表,却忘了 Redis 的设计哲学——它不是数据库,而是键值缓存。字段级过期需求一出现,往往意味着模型抽象错了层级。


















