学习笔记:Redis及其在Go中的操作 | 豆包MarsCode AI刷题

265 阅读8分钟

学习笔记:Redis及其在Go中的操作

Redis 是一种高性能的开源键值存储(Key-Value Store),被广泛应用于缓存、会话管理、排行榜、消息队列等场景。Redis 的特点是支持丰富的数据结构和高效的读写性能,同时提供强大的持久化和分布式功能。本文将从 Redis 的基本概念、常见应用场景、以及如何在 Go 中使用 Redis 进行操作等方面进行深入探讨,最后分享一些个人的思考和实践经验。

一、Redis 基本概念

1. 什么是 Redis?

Redis(Remote Dictionary Server)是一个基于内存的 NoSQL 数据库。它通过将数据存储在内存中,提供了极快的访问速度,同时支持持久化将数据写入磁盘。Redis 是单线程模型,利用了非阻塞 I/O 和事件驱动机制来实现高性能。

2. Redis 的特点

  • 数据类型丰富:支持字符串(String)、哈希表(Hash)、列表(List)、集合(Set)和有序集合(Sorted Set)等多种数据结构。
  • 高性能:读取速度可达 10 万 QPS(每秒查询请求),写入速度约 8 万 QPS。
  • 多功能
    • 支持事务(Transaction)。
    • 提供发布/订阅(Pub/Sub)功能。
    • 提供 Lua 脚本支持。
    • 支持主从复制和分布式架构(如 Redis Cluster)。
  • 持久化:通过 RDB(快照)和 AOF(增量日志)两种方式将内存中的数据持久化到磁盘。
  • 简单易用:命令操作直观,学习成本低。

3. Redis 的常见应用场景

  • 缓存:使用 Redis 存储高频访问的数据,减少对后端数据库的压力。
  • 分布式锁:利用 Redis 的原子性操作实现分布式系统的锁机制。
  • 消息队列:使用列表(List)或发布/订阅机制实现简单的消息队列功能。
  • 排行榜和计数器:利用有序集合(Sorted Set)实现排行榜,利用字符串存储计数器。

二、Redis 基础操作

1. 常用命令

  1. 字符串(String)
    SET key value      # 设置值
    GET key            # 获取值
    INCR key           # 自增
    DECR key           # 自减
    
  2. 哈希(Hash)
    HSET hash key value  # 设置哈希字段
    HGET hash key        # 获取哈希字段值
    HDEL hash key        # 删除字段
    
  3. 列表(List)
    LPUSH list value     # 左侧插入
    RPUSH list value     # 右侧插入
    LPOP list            # 左侧弹出
    RPOP list            # 右侧弹出
    
  4. 集合(Set)
    SADD set value       # 添加元素
    SREM set value       # 移除元素
    SMEMBERS set         # 获取所有元素
    
  5. 有序集合(Sorted Set)
    ZADD zset score value  # 添加带分数的元素
    ZRANGE zset start end  # 获取指定范围内的元素
    ZSCORE zset value      # 获取某个元素的分数
    

三、Go 操作 Redis

1. 准备工作

在 Go 中操作 Redis,可以使用 go-redis 这一官方推荐的 Redis 客户端库。它功能全面,支持常见的 Redis 命令、管道、事务等操作。

安装依赖

使用 go get 安装 go-redis 包:

go get github.com/redis/go-redis/v9

2. 基础操作

(1)建立连接

package main

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

var ctx = context.Background()

func main() {
	// 创建 Redis 客户端
	rdb := redis.NewClient(&redis.Options{
		Addr:     "localhost:6379", // Redis 地址
		Password: "",               // 密码(默认无)
		DB:       0,                // 使用默认数据库
	})

	// 测试连接
	_, err := rdb.Ping(ctx).Result()
	if err != nil {
		fmt.Println("连接失败:", err)
		return
	}
	fmt.Println("Redis 连接成功!")
}

(2)字符串操作

func StringDemo(rdb *redis.Client) {
	// 设置值
	err := rdb.Set(ctx, "key", "value", 0).Err()
	if err != nil {
		fmt.Println("SET 错误:", err)
		return
	}

	// 获取值
	val, err := rdb.Get(ctx, "key").Result()
	if err != nil {
		fmt.Println("GET 错误:", err)
		return
	}
	fmt.Println("key 的值:", val)
}

(3)哈希操作

func HashDemo(rdb *redis.Client) {
	// 设置哈希值
	err := rdb.HSet(ctx, "user:1", "name", "Alice", "age", "23").Err()
	if err != nil {
		fmt.Println("HSET 错误:", err)
		return
	}

	// 获取哈希值
	name, err := rdb.HGet(ctx, "user:1", "name").Result()
	if err != nil {
		fmt.Println("HGET 错误:", err)
		return
	}
	fmt.Println("user:1 的 name:", name)
}

(4)列表操作

func ListDemo(rdb *redis.Client) {
	// 添加元素到列表
	err := rdb.LPush(ctx, "tasks", "task1", "task2").Err()
	if err != nil {
		fmt.Println("LPUSH 错误:", err)
		return
	}

	// 获取列表的长度
	length, _ := rdb.LLen(ctx, "tasks").Result()
	fmt.Println("列表长度:", length)
}

3. 高级功能

(1)事务操作

Redis 提供了事务功能,可以通过命令批量执行。Go-redis 使用 TxPipeline 来实现事务。

func TransactionDemo(rdb *redis.Client) {
	_, err := rdb.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
		pipe.Set(ctx, "key1", "value1", 0)
		pipe.Set(ctx, "key2", "value2", 0)
		return nil
	})
	if err != nil {
		fmt.Println("事务执行失败:", err)
		return
	}
	fmt.Println("事务执行成功")
}

(2)分布式锁

利用 Redis 的 SET NX 命令可以实现分布式锁:

func LockDemo(rdb *redis.Client) {
	lockKey := "mylock"
	val := "request1"

	// 获取锁
	ok, err := rdb.SetNX(ctx, lockKey, val, 10*time.Second).Result()
	if err != nil || !ok {
		fmt.Println("获取锁失败")
		return
	}
	fmt.Println("锁已获取")

	// 释放锁
	valCheck, _ := rdb.Get(ctx, lockKey).Result()
	if valCheck == val {
		rdb.Del(ctx, lockKey)
		fmt.Println("锁已释放")
	}
}

四、Redis 的常见问题

1. 缓存穿透

问题描述

缓存穿透是指用户频繁请求一些缓存中不存在的数据,导致每次都绕过缓存直接查询数据库。若请求量过大,数据库的压力会急剧上升,可能导致服务崩溃。

产生原因

  • 请求的键(Key)在缓存和数据库中都不存在。
  • 攻击者恶意发送大量无效请求,绕过缓存,直接冲击数据库。

解决方案

  1. 布隆过滤器(Bloom Filter)

    • 在 Redis 前加一个布隆过滤器,将所有可能存在的键以哈希方式存储在布隆过滤器中。
    • 每次请求先判断布隆过滤器是否存在该键,不存在则直接返回,避免访问缓存和数据库。
    • 示例代码:
      import "github.com/bits-and-blooms/bloom/v3"
      
      var filter = bloom.New(1000, 5) // 容量和哈希函数数量
      
      func initFilter() {
          filter.AddString("validKey") // 添加有效键
      }
      
      func CheckKey(key string) bool {
          return filter.TestString(key) // 检查键是否存在
      }
      
  2. 缓存空结果

    • 对于数据库中不存在的数据,将空结果存入缓存,并设置较短的过期时间(如几分钟)。
    • 示例代码:
      val, err := rdb.Get(ctx, "invalidKey").Result()
      if err == redis.Nil {
          rdb.Set(ctx, "invalidKey", "", 10*time.Minute) // 设置空值
      }
      

2. 缓存击穿

问题描述

缓存击穿是指某些 热点数据 的缓存失效后,大量请求同时查询该数据,直接冲击数据库,导致数据库压力骤增。

产生原因

  • 热点数据的缓存过期时间到了,导致瞬间大量请求涌向数据库。

解决方案

  1. 使用互斥锁(Mutex)

    • 在缓存失效时,为每个请求添加分布式锁,只有一个请求可以查询数据库并更新缓存,其他请求需等待缓存更新完成。
    • 示例代码:
      lockKey := "lock:hotKey"
      ok, _ := rdb.SetNX(ctx, lockKey, "1", 10*time.Second).Result()
      if ok {
          // 持有锁的请求从数据库加载数据并更新缓存
          data := queryDatabase("hotKey")
          rdb.Set(ctx, "hotKey", data, 10*time.Minute)
          rdb.Del(ctx, lockKey) // 释放锁
      } else {
          time.Sleep(50 * time.Millisecond) // 等待一段时间再查询缓存
      }
      
  2. 设置热点数据永不过期

    • 对于热点数据,可以定期后台任务主动更新缓存,而非让缓存自行过期。
    • 示例代码:
      go func() {
          for {
              data := queryDatabase("hotKey")
              rdb.Set(ctx, "hotKey", data, 0) // 不设置过期时间
              time.Sleep(5 * time.Minute)
          }
      }()
      
  3. 异步更新缓存

    • 当缓存过期时,先返回旧缓存值,同时异步更新新数据。
    • 示例代码:
      val, err := rdb.Get(ctx, "hotKey").Result()
      if err == redis.Nil {
          go func() {
              data := queryDatabase("hotKey")
              rdb.Set(ctx, "hotKey", data, 10*time.Minute)
          }()
      }
      

3. 缓存雪崩

问题描述

缓存雪崩是指大量缓存数据在同一时间过期,导致大量请求直接查询数据库,造成数据库负载过高,甚至宕机。

产生原因

  • 缓存数据的过期时间设置过于集中。
  • 外部因素(如 Redis 宕机)导致所有缓存失效。

解决方案

  1. 缓存过期时间分散

    • 在设置过期时间时,增加一个随机时间,避免大量键同时过期。
    • 示例代码:
      expiration := 10*time.Minute + time.Duration(rand.Intn(60))*time.Second
      rdb.Set(ctx, "key", "value", expiration)
      
  2. 多级缓存

    • 在 Redis 之上增加一层本地缓存(如 Guava、LRU 缓存),减少对 Redis 的直接访问。
    • 示例代码(使用 map 模拟本地缓存):
      var localCache = make(map[string]string)
      
      func GetData(key string) string {
          if val, ok := localCache[key]; ok {
              return val
          }
          val, _ := rdb.Get(ctx, key).Result()
          localCache[key] = val
          return val
      }
      
  3. 请求限流

    • 使用令牌桶算法或漏桶算法限制单位时间内的请求数,保护数据库。
    • 示例代码:
      import "golang.org/x/time/rate"
      
      limiter := rate.NewLimiter(10, 1) // 每秒最多处理 10 个请求
      
      func HandleRequest() {
          if !limiter.Allow() {
              fmt.Println("请求被限流")
              return
          }
          fmt.Println("处理请求")
      }
      
  4. Redis 高可用

    • 使用 Redis 集群或主从复制,避免单点故障。
    • 使用哨兵模式(Sentinel)实时监控主节点状态,自动切换节点。

五、总结与思考

  1. 对缓存问题的认识 缓存的核心是加速数据访问和减轻数据库压力,但在高并发场景下,如果没有合理的设计,缓存机制本身可能成为系统的不稳定因素。这就要求我们在设计系统时,必须充分考虑缓存的边界场景,确保系统能够平稳应对突发流量。

  2. 个人实践心得

    • 布隆过滤器是处理缓存穿透的利器,但需要注意容量设置和误判率问题。
    • 在热点数据场景中,互斥锁是简单实用的方案,但要警惕锁竞争导致的性能问题。
    • 遇到缓存雪崩时,提前分散缓存的过期时间是最直接有效的手段,尽量不要让缓存失效成为偶然性风险。
  3. Redis 的定位 Redis 非常适合作为高速缓存系统,但在实际开发中,它的作用不仅仅是缓存。通过结合分布式锁、消息队列等高级功能,Redis 可以为整个系统提供稳定、高效的基础设施支持。