Redis 的应用场景与性能特点 | 豆包MarsCode AI 刷题

186 阅读6分钟

Redis 使用

为什么需要 Redis

  1. 数据量和压力增长
    • 单表无法满足需求,演进出分库分表。
    • MySQL 单机难以承载大数据量和高并发需求,逐步发展为集群模式。
  2. 冷热数据分离
    • 热数据:经常访问的数据。
    • 使用 Redis 将热数据存储到内存中,降低数据库压力并提高访问速度。

Redis 工作基本原理

  1. 内存操作
    • 数据从内存中读写,访问速度极快。
  2. 持久化机制
    • 数据操作日志保存到硬盘,防止因重启导致数据丢失。
    • 通过定期写入硬盘(如 RDB、AOF)减少频繁 IO 影响性能。
  3. 单线程模型
    • Redis 使用单线程处理命令,避免多线程锁竞争问题。
    • 通过高效的 I/O 多路复用和内存操作,仍能支持高并发。

Redis 应用案例

1. 验证码服务
  • 验证码的生成与验证是高频场景,使用 Redis 的优势:
    • 临时性存储:验证码通常短时间内有效(如 5 分钟),Redis 提供高效的短期存储。
    • 高并发支持:Redis 轻松处理大规模并发请求。
    • 到期管理:使用 EXPIRESETEX 设置验证码的过期时间。

实现流程

  • 用户请求验证码时,生成验证码并存储在 Redis 中,设置到期时间(如 5 分钟)。
  • 用户输入验证码后,从 Redis 读取并验证。如果正确,删除验证码。
2. 连续签到
  • Redis 维护用户签到记录(如连续天数、最后签到时间等)。
  • 利用数据结构如 bitmapzset 记录每日签到信息。
3. 消息通知
  • 列表模式:使用 list 存储消息,结合 LPUSH 和 BRPOP 模拟消息队列。

    • Redis QuickList 将双向链表和 listpack 结合,节约内存。
  • 发布订阅模式pub/sub 实现消息广播。

4. 计数系统
  • 记录高频访问量数据(如点赞、浏览量):
    • 通过 INCR/DECR 进行高效计数。
    • 使用 Pipeline 提高批量计数效率。
5. 排行榜
  • 使用

    zset
    

    (有序集合)实现排行榜,支持:

    • 按分数排序(如用户积分排名)。
    • 定位用户排名(ZRANK)。
    • 查询分数范围内的数据(ZRANGEBYSCORE)。
6. 限流
  • 基于时间窗口限制请求频率:
    • Redis 提供 INCR 和过期时间(如 EXPIRE),限制 1 秒内请求数。
    • 如果请求数超过阈值(如 100 次/秒),拒绝请求。
7. 分布式锁
  • 并发场景下,确保资源独占访问:
    • 使用 SETNX 实现互斥锁。
    • 问题:
      • 锁过期时间不足,业务执行超时。
      • Redis 主从切换或脑裂问题可能导致锁重复获取。
    • 解决方案
      • 使用 Redis 官方推荐的分布式锁算法 Redlock
8. 缓存层
  • 使用 Redis 缓存数据库中热点数据,减少数据库查询压力。
  • 配合缓存策略(如 LRU、LFU)自动清理不常用数据。

Redis 使用注意事项

1. 大 Key 和热 Key
  • 大 Key

    • 定义:存储的数据量较大(如超过 10MB)的 Key。
    • 问题:
      • 读取成本高,容易造成阻塞。
      • 删除或过期可能导致慢查询。
      • 影响主从复制,导致 Redis 卡顿。
    • 解决
      • 拆分大 Key:如将一个大 Hash 拆成多个小 Hash。
      • 提前评估 Key 大小,设置阈值报警。
  • 热 Key

    • 定义:访问频率特别高(如 QPS 超过 500)的 Key。

    • 问题:导致服务器 CPU 负载突增。

    • 解决

      • 本地缓存:如在服务中引入 BigCacheGuava
      • 代理分担:使用 Redis 代理检测并分流热 Key。
      • 数据分片:将一个 Key 数据拆分到多个 Key。
2. 慢查询
  • 慢查询的场景:
    • 查询大 Key。
    • 批量操作未使用 Pipeline
  • 优化建议
    • 设置合理的慢查询阈值,开启慢查询日志。
    • 使用索引结构(如 zset)加快查询速度。
3. 缓存穿透、雪崩与击穿
  • 缓存穿透
    • 场景:查询一定不存在的数据(如 null)。
    • 解决
      • 缓存空值。
      • 使用布隆过滤器避免无效查询直接访问数据库。
  • 缓存雪崩
    • 场景:大量缓存同时过期,导致大量请求直击数据库。
    • 解决
      • 设置随机失效时间,避免同时过期。
      • 增加热点数据缓存时间。
  • 缓存击穿
    • 场景:高并发场景下热点 Key 过期,导致大量请求直接访问数据库。
    • 解决
      • 热点数据采用本地缓存。
      • 缓存预热:提前加载热点数据。

Redis 的性能特点

  1. 高性能
    • 每秒处理百万级别的请求。
  2. 丰富的数据结构
    • 支持 StringListHashSetZSet 等多种数据结构。
  3. 单线程架构
    • 单线程避免了多线程锁竞争问题,通过 I/O 多路复用提升性能。
  4. 高可用
    • 支持主从复制、哨兵模式和集群模式,保障数据的高可用性和一致性。

使用示例:验证码服务

以下代码演示了一个简单的 Go 应用,使用 Redis 实现验证码存储与验证。

package main

import (
	"context"
	"fmt"
	"github.com/redis/go-redis/v9"
	"math/rand"
	"net/http"
	"time"
)

var (
	rdb  *redis.Client
	ctx  = context.Background()
	port = ":8080" // 服务监听端口
)

func init() {
	// 初始化 Redis 客户端
	rdb = redis.NewClient(&redis.Options{
		Addr:     "localhost:6379", // Redis 地址
		Password: "",               // Redis 密码
		DB:       0,                // Redis 数据库
	})
}

// 生成随机验证码
func generateCode(length int) string {
	rand.Seed(time.Now().UnixNano())
	digits := "0123456789"
	code := make([]byte, length)
	for i := range code {
		code[i] = digits[rand.Intn(len(digits))]
	}
	return string(code)
}

// 生成验证码并存储到 Redis
func signUpCodeHandler(w http.ResponseWriter, r *http.Request) {
	// 从请求中获取手机号
	phone := r.URL.Query().Get("phone")
	if phone == "" {
		http.Error(w, "请输入手机号", http.StatusBadRequest)
		return
	}

	// 生成 6 位验证码
	code := generateCode(6)
	// 存储到 Redis,并设置过期时间
	exp := 60 * time.Second // 验证码有效期(60秒)
	err := rdb.Set(ctx, phone, code, exp).Err()
	if err != nil {
		http.Error(w, "设置验证码失败", http.StatusInternalServerError)
		return
	}
	fmt.Fprintf(w, "验证码已发送到 %s: %s, 有效期为%v秒\n", phone, code, exp.Seconds()) // 模拟输出验证码
}

// 验证用户输入的验证码
func verifyCodeHandler(w http.ResponseWriter, r *http.Request) {
	phone := r.URL.Query().Get("phone")
	code := r.URL.Query().Get("code")
	if phone == "" || code == "" {
		http.Error(w, "请输入的手机号和验证码", http.StatusBadRequest)
		return
	}
	// 从 Redis 获取验证码
	storedCode, err := rdb.Get(ctx, phone).Result()
	if err == redis.Nil {
		http.Error(w, "验证码已过期或不存在", http.StatusBadRequest)
		return
	} else if err != nil {
		http.Error(w, "获取验证码失败", http.StatusInternalServerError)
		return
	}
	// 验证码匹配
	if storedCode == code {
		// 验证成功,删除验证码
		rdb.Del(ctx, phone)
		fmt.Fprintf(w, "验证码验证成功\n")
	} else {
		http.Error(w, "验证码错误", http.StatusBadRequest)
	}
}

func main() {
	http.HandleFunc("/sign-up", signUpCodeHandler)
	http.HandleFunc("/verify", verifyCodeHandler)
	fmt.Println("服务启动中,监听端口", port)
	if err := http.ListenAndServe(port, nil); err != nil {
		panic(err)
	}
}

使用说明

  1. 启动服务器:go run main.go
  2. 模拟注册:浏览器访问 http://localhost:8080/sign-up?phone=66666
  3. 验证验证码:浏览器访问 http://localhost:8080/verify?phone=66666&code=验证码