Redis 使用
为什么需要 Redis
- 数据量和压力增长:
- 单表无法满足需求,演进出分库分表。
- MySQL 单机难以承载大数据量和高并发需求,逐步发展为集群模式。
- 冷热数据分离:
- 热数据:经常访问的数据。
- 使用 Redis 将热数据存储到内存中,降低数据库压力并提高访问速度。
Redis 工作基本原理
- 内存操作:
- 数据从内存中读写,访问速度极快。
- 持久化机制:
- 数据操作日志保存到硬盘,防止因重启导致数据丢失。
- 通过定期写入硬盘(如 RDB、AOF)减少频繁 IO 影响性能。
- 单线程模型:
- Redis 使用单线程处理命令,避免多线程锁竞争问题。
- 通过高效的 I/O 多路复用和内存操作,仍能支持高并发。
Redis 应用案例
1. 验证码服务
- 验证码的生成与验证是高频场景,使用 Redis 的优势:
- 临时性存储:验证码通常短时间内有效(如 5 分钟),Redis 提供高效的短期存储。
- 高并发支持:Redis 轻松处理大规模并发请求。
- 到期管理:使用
EXPIRE或SETEX设置验证码的过期时间。
实现流程:
- 用户请求验证码时,生成验证码并存储在 Redis 中,设置到期时间(如 5 分钟)。
- 用户输入验证码后,从 Redis 读取并验证。如果正确,删除验证码。
2. 连续签到
- Redis 维护用户签到记录(如连续天数、最后签到时间等)。
- 利用数据结构如
bitmap或zset记录每日签到信息。
3. 消息通知
-
列表模式:使用 list 存储消息,结合 LPUSH 和 BRPOP 模拟消息队列。
- Redis
QuickList将双向链表和listpack结合,节约内存。
- Redis
-
发布订阅模式 :
pub/sub实现消息广播。
4. 计数系统
- 记录高频访问量数据(如点赞、浏览量):
- 通过
INCR/DECR进行高效计数。 - 使用
Pipeline提高批量计数效率。
- 通过
5. 排行榜
-
使用
zset(有序集合)实现排行榜,支持:
- 按分数排序(如用户积分排名)。
- 定位用户排名(
ZRANK)。 - 查询分数范围内的数据(
ZRANGEBYSCORE)。
6. 限流
- 基于时间窗口限制请求频率:
- Redis 提供
INCR和过期时间(如EXPIRE),限制 1 秒内请求数。 - 如果请求数超过阈值(如 100 次/秒),拒绝请求。
- Redis 提供
7. 分布式锁
- 并发场景下,确保资源独占访问:
- 使用
SETNX实现互斥锁。 - 问题:
- 锁过期时间不足,业务执行超时。
- Redis 主从切换或脑裂问题可能导致锁重复获取。
- 解决方案
- 使用 Redis 官方推荐的分布式锁算法
Redlock。
- 使用 Redis 官方推荐的分布式锁算法
- 使用
8. 缓存层
- 使用 Redis 缓存数据库中热点数据,减少数据库查询压力。
- 配合缓存策略(如 LRU、LFU)自动清理不常用数据。
Redis 使用注意事项
1. 大 Key 和热 Key
-
大 Key:
- 定义:存储的数据量较大(如超过 10MB)的 Key。
- 问题:
- 读取成本高,容易造成阻塞。
- 删除或过期可能导致慢查询。
- 影响主从复制,导致 Redis 卡顿。
- 解决
- 拆分大 Key:如将一个大 Hash 拆成多个小 Hash。
- 提前评估 Key 大小,设置阈值报警。
-
热 Key:
-
定义:访问频率特别高(如 QPS 超过 500)的 Key。
-
问题:导致服务器 CPU 负载突增。
-
解决
- 本地缓存:如在服务中引入
BigCache或Guava。 - 代理分担:使用 Redis 代理检测并分流热 Key。
- 数据分片:将一个 Key 数据拆分到多个 Key。
- 本地缓存:如在服务中引入
-
2. 慢查询
- 慢查询的场景:
- 查询大 Key。
- 批量操作未使用
Pipeline。
- 优化建议
- 设置合理的慢查询阈值,开启慢查询日志。
- 使用索引结构(如
zset)加快查询速度。
3. 缓存穿透、雪崩与击穿
- 缓存穿透:
- 场景:查询一定不存在的数据(如
null)。 - 解决
- 缓存空值。
- 使用布隆过滤器避免无效查询直接访问数据库。
- 场景:查询一定不存在的数据(如
- 缓存雪崩:
- 场景:大量缓存同时过期,导致大量请求直击数据库。
- 解决
- 设置随机失效时间,避免同时过期。
- 增加热点数据缓存时间。
- 缓存击穿:
- 场景:高并发场景下热点 Key 过期,导致大量请求直接访问数据库。
- 解决
- 热点数据采用本地缓存。
- 缓存预热:提前加载热点数据。
Redis 的性能特点
- 高性能:
- 每秒处理百万级别的请求。
- 丰富的数据结构:
- 支持
String、List、Hash、Set、ZSet等多种数据结构。
- 支持
- 单线程架构:
- 单线程避免了多线程锁竞争问题,通过 I/O 多路复用提升性能。
- 高可用:
- 支持主从复制、哨兵模式和集群模式,保障数据的高可用性和一致性。
使用示例:验证码服务
以下代码演示了一个简单的 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)
}
}
使用说明:
- 启动服务器:
go run main.go。- 模拟注册:浏览器访问
http://localhost:8080/sign-up?phone=66666。- 验证验证码:浏览器访问
http://localhost:8080/verify?phone=66666&code=验证码。