Redis 的使用
Redis 是一个开源的(BSD 许可)内存数据结构存储,可用作数据库、缓存和消息代理。它支持多种数据结构,如字符串、哈希、列表、集合、有序集合等。在 Gin 框架中集成 Redis 可以极大地提高应用程序的性能和可扩展性。
本教程将介绍如何在 Gin 应用程序中连接 Redis、进行基本操作(如设置和获取键值)、以及如何使用 Redis 进行会话管理和缓存。
1. 环境准备
在开始之前,请确保您的开发环境已准备就绪。
1.1 安装 Go Redis 客户端
我们将使用 go-redis/redis/v8 库作为 Redis 客户端。它是 Go 语言中最流行和功能最丰富的 Redis 客户端之一。
1
| go get -u github.com/go-redis/redis/v8
|
1.2 安装和运行 Redis 服务器
您可以通过多种方式安装和运行 Redis 服务器:
安装完成后,确保 Redis 服务器正在运行(默认监听 6379 端口)。您可以使用 redis-cli ping 来测试连接。如果返回 PONG 则表示连接成功。
1.3 Redis 配置修改 (Windows)
在 Windows 系统中,Redis 通常以服务形式运行或通过命令行手动启动。它的行为通过 redis.windows.conf 文件进行配置。
1.3.1 找到 Redis 配置文件
如果您是手动安装 Redis for Windows,redis.windows.conf 文件通常位于您解压 Redis 安装包的根目录下。
例如:C:\Program Files\Redis\redis.windows.conf 或 C:\redis-x.x.x\redis.windows.conf。
找到文件后,使用记事本、Notepad++ 或 VS Code 等文本编辑器打开它。
1.3.2 主要配置项更改
以下是一些您可能会经常更改或需要注意的重要配置项及其修改建议。请根据您的需求取消注释(删除行首的 #)并修改对应的值。
bind
作用:指定 Redis 服务器监听的 IP 地址。
修改:默认情况下,Redis for Windows 可能没有明确的 bind 设置,或者它会监听所有可用接口。如果您需要限制访问,可以将其设置为特定 IP 地址。
1 2
| # bind 127.0.0.1 # 默认可能没有这一行,或只监听本地 bind 0.0.0.0 # 监听所有可用 IP (仅用于测试,生产环境请谨慎)
|
重要提示:在没有密码或防火墙保护的情况下,将 bind 设置为 0.0.0.0 并暴露在公网是非常危险的。
port
protected-mode
requirepass
databases
maxmemory
daemonize
loglevel
1.3.3 重启 Redis (Windows)
修改配置文件后,您需要重启 Redis 服务器以使更改生效。重启方式取决于您如何在 Windows 上运行 Redis。
1.3.3.1 作为 Windows 服务运行
如果您的 Redis 实例是作为 Windows 服务安装并运行的(这是推荐的生产环境部署方式),可以通过 Windows 的服务管理器来重启:
- 按下
Win + R 键,输入 services.msc,然后按回车键打开 服务 管理器。
- 在服务列表中找到名为
Redis 或类似名称(例如 Redis6379)的服务。
- 右键点击该服务,然后选择 重启。
1.3.3.2 通过命令行手动运行
如果您是通过命令行直接运行 redis-server.exe,您需要先停止当前的进程,然后重新启动它:
停止当前 Redis 进程:
使用更新后的配置文件启动 Redis 服务器:
打开一个新的命令提示符 (CMD) 或 PowerShell 窗口。
导航到 redis-server.exe 所在的目录。
执行以下命令,指定您的配置文件路径:
DOS
1 2 3
| redis-server.exe redis.windows.conf # 或如果配置文件在其他路径: # redis-server.exe C:\path\to\your\redis.windows.conf
|
这将会在当前控制台窗口中启动 Redis。如果您想让它在后台运行(不占用控制台),可以考虑使用 start 命令(但通常建议安装为服务)。
重启后,您可以使用 redis-cli.exe 连接到 Redis 并执行 PING 命令或 CONFIG GET <config_name> 命令来验证新的配置是否已生效。
2. 基本 Redis 操作
在使用 Gin 之前,我们先了解 go-redis 库的基本操作。
2.1 连接 Redis
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| package main
import ( "context" "fmt"
"github.com/go-redis/redis/v8" )
var ctx = context.Background()
func main() { rdb := redis.NewClient(&redis.Options{ Addr: "localhost:6379", Password: "", DB: 0, })
pong, err := rdb.Ping(ctx).Result() if err != nil { fmt.Println("Error connecting to Redis:", err) return } fmt.Println("Redis connected:", pong) }
|
2.2 设置和获取字符串
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| package main
import ( "context" "fmt" "time"
"github.com/go-redis/redis/v8" )
var ctx = context.Background()
func main() { rdb := redis.NewClient(&redis.Options{ Addr: "localhost:6379", DB: 0, })
err := rdb.Set(ctx, "mykey", "Hello Redis from Go!", 0).Err() if err != nil { fmt.Println("Error setting key:", err) return } fmt.Println("Key 'mykey' set successfully.")
val, err := rdb.Get(ctx, "mykey").Result() if err == redis.Nil { fmt.Println("Key 'mykey' does not exist.") } else if err != nil { fmt.Println("Error getting key:", err) return } else { fmt.Println("Value of 'mykey':", val) }
_, err = rdb.Del(ctx, "mykey").Result() if err != nil { fmt.Println("Error deleting key:", err) return } fmt.Println("Key 'mykey' deleted successfully.")
val, err = rdb.Get(ctx, "mykey").Result() if err == redis.Nil { fmt.Println("Key 'mykey' is now deleted.") } }
|
2.3 设置过期时间
Redis 可以为键设置过期时间,使其在一段时间后自动删除。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| package main
import ( "context" "fmt" "time"
"github.com/go-redis/redis/v8" )
var ctx = context.Background()
func main() { rdb := redis.NewClient(&redis.Options{ Addr: "localhost:6379", DB: 0, })
err := rdb.Set(ctx, "expire_key", "This key will expire.", 5*time.Second).Err() if err != nil { fmt.Println("Error setting key with expiry:", err) return } fmt.Println("Key 'expire_key' set with 5s expiry.")
val, _ := rdb.Get(ctx, "expire_key").Result() fmt.Println("Initial value of 'expire_key':", val)
time.Sleep(6 * time.Second)
val, err = rdb.Get(ctx, "expire_key").Result() if err == redis.Nil { fmt.Println("Key 'expire_key' has expired.") } else if err != nil { fmt.Println("Error getting expired key:", err) } else { fmt.Println("Value of 'expire_key' (should be expired):", val) } }
|
3. 在 Gin 中集成 Redis
现在我们将把 Redis 功能集成到 Gin 应用程序中。
3.1 初始化 Redis 客户端
在 Gin 应用程序启动时,最好只初始化一次 Redis 客户端,并将其作为依赖注入到处理函数中,或者存储在一个全局变量中(但推荐依赖注入)。
我们通常将 Redis 客户端实例存储在 Gin 的 gin.Context 中,或者作为自定义结构体的字段传递。
main.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| package main
import ( "context" "fmt" "log" "net/http" "time"
"github.com/gin-gonic/gin" "github.com/go-redis/redis/v8" )
var Rdb *redis.Client var Ctx = context.Background()
func initRedis() { Rdb = redis.NewClient(&redis.Options{ Addr: "localhost:6379", Password: "", DB: 0, })
pong, err := Rdb.Ping(Ctx).Result() if err != nil { log.Fatalf("Could not connect to Redis: %v", err) } fmt.Println("Connected to Redis:", pong) }
func main() { initRedis()
router := gin.Default()
router.GET("/set/:key/:value", setKeyValue) router.GET("/get/:key", getKeyValue) router.DELETE("/delete/:key", deleteKey)
fmt.Println("Gin server running on :8080") router.Run(":8080") }
|
3.2 创建 Gin 路由处理 Redis 操作
接下来,我们将为 Redis 操作创建 Gin 的路由处理函数。
main.go (续)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| func setKeyValue(c *gin.Context) { key := c.Param("key") value := c.Param("value")
err := Rdb.Set(Ctx, key, value, 1*time.Minute).Err() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to set key: %v", err)}) return }
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("Key '%s' set successfully with value '%s'", key, value)}) }
func getKeyValue(c *gin.Context) { key := c.Param("key")
val, err := Rdb.Get(Ctx, key).Result() if err == redis.Nil { c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("Key '%s' not found", key)}) return } else if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to get key: %v", err)}) return }
c.JSON(http.StatusOK, gin.H{"key": key, "value": val}) }
func deleteKey(c *gin.Context) { key := c.Param("key")
deleted, err := Rdb.Del(Ctx, key).Result() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to delete key: %v", err)}) return }
if deleted == 0 { c.JSON(http.StatusNotFound, gin.H{"message": fmt.Sprintf("Key '%s' not found or already deleted", key)}) } else { c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("Key '%s' deleted successfully", key)}) } }
|
3.3 处理错误
在实际应用中,处理 Redis 操作可能出现的错误至关重要。go-redis 库在键不存在时会返回 redis.Nil 错误,其他网络或 Redis 服务器错误则会返回不同的错误类型。务必对这些错误进行检查和处理。
4. 使用 Redis 作为缓存
Redis 最常见的用途之一是作为应用程序的缓存层。我们可以创建一个 Gin 中间件来实现简单的缓存功能。
4.1 实现缓存中间件
这个中间件将尝试从 Redis 获取响应,如果命中缓存,则直接返回;如果未命中,则继续执行后续处理函数,并将结果缓存起来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
| package main
import ( "bytes" "context" "fmt" "io/ioutil" "log" "net/http" "time"
"github.com/gin-gonic/gin" "github.com/go-redis/redis/v8" )
func CacheMiddleware(cacheDuration time.Duration) gin.HandlerFunc { return func(c *gin.Context) { cacheKey := "cache:" + c.Request.RequestURI log.Printf("Attempting to fetch from cache for key: %s\n", cacheKey)
cachedResponse, err := Rdb.Get(Ctx, cacheKey).Result() if err == nil { log.Printf("Cache HIT for key: %s\n", cacheKey) c.Header("X-Cache", "HIT") c.Data(http.StatusOK, "application/json", []byte(cachedResponse)) c.Abort() return } else if err == redis.Nil { log.Printf("Cache MISS for key: %s\n", cacheKey) c.Header("X-Cache", "MISS") } else { log.Printf("Error getting from Redis cache: %v\n", err) }
blw := &bodyLogWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer} c.Writer = blw
c.Next()
if c.Writer.Status() == http.StatusOK { err = Rdb.Set(Ctx, cacheKey, blw.body.String(), cacheDuration).Err() if err != nil { log.Printf("Error setting Redis cache: %v\n", err) } else { log.Printf("Response cached for key: %s, duration: %s\n", cacheKey, cacheDuration) } } } }
type bodyLogWriter struct { gin.ResponseWriter body *bytes.Buffer }
func (w bodyLogWriter) Write(b []byte) (int, error) { w.body.Write(b) return w.ResponseWriter.Write(b) }
func (w bodyLogWriter) WriteString(s string) (int, error) { w.body.WriteString(s) return w.ResponseWriter.WriteString(s) }
func main() { initRedis() router := gin.Default()
router.Use(gin.Logger())
cachedGroup := router.Group("/cached") cachedGroup.Use(CacheMiddleware(10 * time.Second)) { cachedGroup.GET("/data", func(c *gin.Context) { log.Println("Executing /cached/data handler (simulating slow operation)...") time.Sleep(2 * time.Second) c.JSON(http.StatusOK, gin.H{"data": "This is cached data!", "timestamp": time.Now().Format(time.RFC3339)}) }) }
router.GET("/no-cache-data", func(c *gin.Context) { log.Println("Executing /no-cache-data handler...") c.JSON(http.StatusOK, gin.H{"data": "This data is not cached.", "timestamp": time.Now().Format(time.RFC3339)}) })
fmt.Println("Gin server running on :8080") router.Run(":8080") }
|
4.2 在路由中使用缓存
在上面的例子中,我们已经演示了如何将 CacheMiddleware 应用到一个路由组 (/cached)。所有该组下的路由都会受益于缓存。
测试缓存:
- 启动 Gin 服务器。
- 第一次访问
http://localhost:8080/cached/data:您会看到日志输出 Executing /cached/data handler... 和 Cache MISS,请求会延迟 2 秒。
- 在 10 秒内再次访问
http://localhost:8080/cached/data:您会看到日志输出 Cache HIT,请求会立即返回,且 timestamp 值与第一次相同(因为它返回的是缓存的数据)。
- 等待 10 秒以上,再次访问
http://localhost:8080/cached/data:缓存过期,您会再次看到 Cache MISS 和 2 秒延迟,timestamp 值会更新。
- 访问
http://localhost:8080/no-cache-data:每次访问都会立即返回,没有缓存。
5. 高级用法(可选)
5.1 Redis 连接池
go-redis 客户端本身就包含了连接池管理。在 redis.NewClient 中,您可以配置 PoolSize、MinIdleConns 等选项来优化连接池行为。
1 2 3 4 5 6 7 8 9
| rdb := redis.NewClient(&redis.Options{ Addr: "localhost:6379", Password: "", DB: 0, PoolSize: 10, MinIdleConns: 5, PoolTimeout: 30 * time.Second, IdleTimeout: 5 * time.Minute, })
|
5.2 使用 Redis 实现分布式锁
当您有多个服务实例需要对共享资源进行互斥访问时,可以使用 Redis 实现分布式锁。go-redis 提供了 SetNX(Set if Not eXists)和 Expire 命令的封装,可以用来构建简单的锁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| import ( "time" "github.com/go-redis/redis/v8" )
func ObtainLock(client *redis.Client, key string, value string, expiration time.Duration) (bool, error) { ok, err := client.SetNX(Ctx, key, value, expiration).Result() if err != nil { return false, err } return ok, nil }
func ReleaseLock(client *redis.Client, key string, value string) (bool, error) { script := ` if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end ` result, err := client.Eval(Ctx, script, []string{key}, value).Result() if err != nil { return false, err } return result.(int64) == 1, nil }
|
5.3 Pub/Sub(发布/订阅)
Redis 支持发布/订阅模式,允许客户端订阅频道并接收其他客户端发布的消息,这在实现实时通知或事件驱动架构时非常有用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
| package main
import ( "context" "fmt" "log" "net/http" "time"
"github.com/gin-gonic/gin" "github.com/go-redis/redis/v8" )
var Rdb *redis.Client var Ctx = context.Background()
func initRedis() { Rdb = redis.NewClient(&redis.Options{ Addr: "localhost:6379", DB: 0, }) _, err := Rdb.Ping(Ctx).Result() if err != nil { log.Fatalf("Could not connect to Redis: %v", err) } fmt.Println("Connected to Redis for Pub/Sub") }
func SubscribeToChannel(channel string) { pubsub := Rdb.Subscribe(Ctx, channel) defer pubsub.Close()
_, err := pubsub.Receive(Ctx) if err != nil { log.Printf("Error receiving from pubsub: %v", err) return } log.Printf("Subscribed to channel: %s", channel)
ch := pubsub.Channel()
for msg := range ch { log.Printf("Received message from channel '%s': %s", msg.Channel, msg.Payload) } }
func main() { initRedis()
router := gin.Default()
go SubscribeToChannel("my_chat_channel")
router.GET("/publish/:message", func(c *gin.Context) { message := c.Param("message") err := Rdb.Publish(Ctx, "my_chat_channel", message).Err() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to publish message: %v", err)}) return } c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("Message '%s' published to 'my_chat_channel'", message)}) })
fmt.Println("Gin server running on :8080") router.Run(":8080") }
|
测试 Pub/Sub:
- 运行上面的 Gin 应用程序。
- 访问
http://localhost:8080/publish/hello_world。
- 查看应用程序的控制台输出,您会看到
Received message from channel 'my_chat_channel': hello_world,表明消息已成功发布并被订阅者接收。