Golang 操作 Redis:为 Hash 中的字段设置过期时间 - go-redis 使用指南

文章目录

在上一篇 《Golang 操作 Redis:Set 集合数据类型操作用法 - go-redis 使用指南》 文章中,我们介绍了 Redis 的 Set 集合数据类型,并展示如何在 Golang 中使用 go-redis 进行相关操作。本文将介绍如何设置 Redis Hash 哈希数据类型的单个字段设置过期时间。

👉 点击查看《go-redis使用指南》系列文章目录

在《go-redis使用指南》系列文章中,我们将详细介绍如何在 Golang 项目中使用 redis/go-redis 库与 Redis 进行交互。以下是该系列文章的全部内容:

  1. Golang 操作 Redis:快速上手 - go-redis 使用指南
  2. Golang 操作 Redis:连接设置与参数详解 - go-redis 使用指南
  3. Golang 操作 Redis:基础的字符串键值操作 - go-redis 使用指南
  4. Golang 操作 Redis:如何设置 key 的过期时间 - go-redis 使用指南
  5. Golang 操作 Redis:Hash 哈希数据类型操作用法 - go-redis 使用指南
  6. Golang 操作 Redis:Set 集合数据类型操作用法 - go-redis 使用指南
  7. Golang 操作 Redis:为 Hash 中的字段设置过期时间 - go-redis 使用指南
  8. Golang 操作 Redis:List 列表数据类型操作用法 - go-redis 使用指南
  9. Golang 操作 Redis:SortedSet 有序集合数据类型操作用法 - go-redis 使用指南
  10. Golang 操作 Redis:bitmap 数据类型操作用法 - go-redis 使用指南
  11. Golang 操作 Redis:事务处理操作用法 - go-redis 使用指南
  12. Golang 操作 Redis:地理空间数据类型操作用法 - go-redis 使用指南
  13. Golang 操作 Redis:HyperLogLog 操作用法 - go-redis 使用指南
  14. Golang 操作 Redis:Pipeline 操作用法 - go-redis 使用指南
  15. Golang 操作 Redis:PubSub发布订阅用法 - go-redis 使用指南
  16. Golang 操作 Redis:布隆过滤器(Bloom Filter)操作用法 - go-redis 使用指南
  17. Golang 操作 Redis:Cuckoo Filter操作用法 - go-redis 使用指南
  18. Golang 操作 Redis:Stream操作用法 - go-redis 使用指南
golang redis go-redis

Redis 7.4.0 直接支持单个 hash 字段设置过期时间

相关 go-redis 使用示例请阅读:《Golang 操作 Redis:Hash 哈希数据类型操作用法 - go-redis 使用指南》

低版本 Redis 支持单个 hash 字段设置过期时间的替代方案

如果你的生产环境使用的 Redis 低于 7.4.0 且不便升级时,Hash 类型的字段 (field) 是不支持单独设置过期时间的。Redis 的过期时间是针对整个 key 而不是 key 下的某个字段设置的。因此,你只能对整个 Hash key 设置过期时间,而不能直接单独对 Hash 中的某个字段设置过期时间,这意味着,Hash 中的所有 field 都会在 Hash 键的过期时间到达时一起被删除。

虽然低版本 Redis 不支持对 Hash 中的单个字段设置过期时间,但可以使用以下替代方案来实现类似的功能:

  1. 使用额外的 Redis key 来跟踪字段的过期时间: 你可以为每个需要过期时间的字段设置一个额外的 Redis key,用于记录该字段的过期时间。

  2. 自定义过期机制: 在访问字段时,检查该字段的过期时间是否已到期。如果到期,则删除该字段并进行相应处理。如果需要设置过期时间的字段数量过于庞大,可以定期扫描和清理这些字段。你可以使用 Redis 的扫描命令来找到过期的字段并将其删除。

go-redis 对 Hash 中的个别 field 设置过期时间

以下是一个示例,展示了如何使用额外的 Redis key 来跟踪 Hash 中字段的过期时间并实现定期清理过期的 Hash 字段:

package main

import (
    "context"
    "fmt"
    "github.com/redis/go-redis/v9"
    "time"
)

var ctx = context.Background()

// 设置 Hash 中的字段和字段的过期时间
func setFieldWithExpiration(rdb *redis.Client, hashKey, field, value string, expiration time.Duration) error {
    // 设置 Hash 中的字段值
    if err := rdb.HSet(ctx, hashKey, field, value).Err(); err != nil {
        return err
    }

    // 设置字段过期时间的辅助 key
    expirationKey := fmt.Sprintf("%s:%s:expiration", hashKey, field)
    return rdb.Set(ctx, expirationKey, time.Now().Add(expiration).Unix(), expiration).Err()
}

// 获取 Hash 中的字段值
func getField(rdb *redis.Client, hashKey, field string) (string, error) {
    expirationKey := fmt.Sprintf("%s:%s:expiration", hashKey, field)

    // 检查字段是否过期
    expirationTimeStr, err := rdb.Get(ctx, expirationKey).Result()
    if err != nil && err != redis.Nil {
        return "", err
    }

    if expirationTimeStr != "" {
        expirationTime, err := time.ParseInt(expirationTimeStr, 10, 64)
        if err != nil {
            return "", err
        }

        if time.Now().Unix() > expirationTime {
            // 如果字段已过期,删除字段和过期时间
            rdb.HDel(ctx, hashKey, field)
            rdb.Del(ctx, expirationKey)
            return "", nil
        }
    }

    // 返回 Hash 中的字段值
    return rdb.HGet(ctx, hashKey, field).Result()
}

// 定期扫描并清理过期的 Hash 字段
func cleanExpiredFields(rdb *redis.Client, hashKey string) error {
    cursor := uint64(0)
    for {
        // 扫描所有过期时间 key
        keys, nextCursor, err := rdb.Scan(ctx, cursor, fmt.Sprintf("%s:*:expiration", hashKey), 0).Result()
        if err != nil {
            return err
        }

        cursor = nextCursor
        if cursor == 0 {
            break
        }

        // 使用 MGet 批量获取所有过期时间 key 的值
        expirationValues, err := rdb.MGet(ctx, keys...).Result()
        if err != nil {
            return err
        }

        // 处理每个过期时间 key
        for i, key := range keys {
            expirationTimeStr := expirationValues[i]
            if expirationTimeStr != nil {
                expirationTime, err := time.ParseInt(expirationTimeStr.(string), 10, 64)
                if err != nil {
                    return err
                }

                if time.Now().Unix() > expirationTime {
                    // 如果字段已过期,删除 Hash 中的字段和过期时间 key
                    field := key[len(hashKey)+1:len(key)-len(":expiration")]
                    rdb.HDel(ctx, hashKey, field)
                    rdb.Del(ctx, key)
                }
            }
        }
    }
    return nil
}

func main() {
    rdb := redis.NewClient(&redis.Options{
        Addr:     "localhost:6379",
        Password: "",
        DB:       0,
    })

    // 设置字段 "field1" 的值和过期时间为 10 秒
    if err := setFieldWithExpiration(rdb, "myhash", "field1", "value1", 10*time.Second); err != nil {
        fmt.Println("Error setting field with expiration:", err)
        return
    }

    // 启动清理过期字段的定时任务
    ticker := time.NewTicker(5 * time.Second)
    defer ticker.Stop()

    go func() {
        for range ticker.C {
            if err := cleanExpiredFields(rdb, "myhash"); err != nil {
                fmt.Println("Error cleaning expired fields:", err)
            }
        }
    }()

    // 测试获取字段值
    value, err := getField(rdb, "myhash", "field1")
    if err != nil {
        fmt.Println("Error getting field:", err)
        return
    }

    fmt.Println("Field value:", value)

    // 等待 10 秒以超出字段的过期时间
    time.Sleep(10 * time.Second)

    // 再次获取字段值
    value, err = getField(rdb, "myhash", "field1")
    if err != nil {
        fmt.Println("Error getting field:", err)
        return
    }

    fmt.Println("Field value after expiration:", value)

    // 保持主线程运行
    select {}
}

代码说明

  1. setFieldWithExpiration:设置 Hash 中的字段值,并为该字段创建一个额外的过期时间 key。
  2. getField:在获取字段值之前检查其过期时间。如果字段已过期,删除该字段和过期时间 key。
  3. cleanExpiredFields:定期扫描所有过期时间 key,并删除已经过期的字段和过期时间 key。
  4. main
    • 设置一个字段的值和过期时间。
    • 启动一个定时任务来定期清理过期字段。
    • 测试获取字段值,并在字段过期后再次获取以验证功能。

总结

终于我们可以在 Redis 7.4.0 版本上直接对 Hash 的字段设置过期值了,但由于很多业务使用 Redis 低于该版本,生产环境去做升级迁移风险很大,所以还是很多的低版本 Redis 的使用场景,虽然低版本 Redis 不支持直接为 Hash 中的字段设置过期时间,但可以通过使用额外的 Redis key 来跟踪字段的过期时间,并在访问字段时检查其过期状态。这样,你可以实现类似的功能,并管理 Hash 中字段的生命周期。

希望这篇文章对你有所帮助!点击 go-redis 使用指南 可查看更多相关教程!


也可以看看