Redis - 大厂程序员是怎么用的
虽然青训营伴学笔记创作活动已经结束了,但是我还是想以这种写文章的方式来记录一下自己的学习过程,一方面可以起到敦促的作用,另一方面可以对学习内容进行简单的记录,供之后复盘学习使用。
一、本堂课重点内容
本堂课对 Redis 进行了初步的介绍,从是什么、为什么、怎么做三个方面展开对 Redis 的原理及应用详细讲解,最后结合 Redis 的实际应用场景,讲述了在字节跳动使用 Redis 需要注意的事项。
二、详细知识点介绍
1. Redis 概述
Redis是一种高性能的开源内存数据库,用于存储和检索数据。它支持多种数据结构,如字符串、哈希表、列表、集合和有序集合等,可以通过网络进行访问。Redis还提供了一些高级功能,如事务、持久性、复制和Lua脚本等,可以满足各种不同的应用程序需求。
为什么需要 Redis?
随着数据量的不断增长,数据读写压力的不断增加,数据表从单表演进到了分库分表,MySQL也从单机演进到了集群,但仍不能够满足日益发展的数据需求。
所以在实际应用中,Redis和MySQL一般结合使用,充分发挥各自的优势。例如,可以将MySQL用于数据的持久化存储和复杂查询,而将Redis用于缓存和高速读写。这样可以提高系统的性能和可扩展性,并减少MySQL的负担。同时,Redis和MySQL之间也可以进行数据同步和备份,保证数据的可靠性和一致性。
Redis 的基本工作原理
- 内存存储:Redis将所有数据存储在内存中,因此读写速度非常快。同时,Redis还支持数据持久化,可以将数据保存到磁盘上,以便在重启时恢复数据。
- 基于键值的存储:Redis是一种键值存储系统,每个数据都是通过唯一的键来访问。这种存储方式非常简单,可以方便地存储各种类型的数据。
- 多种数据结构支持:Redis支持多种数据结构,如字符串、哈希表、列表、集合和有序集合等,这些数据结构可以方便地满足不同应用场景的需求。
- 基于网络的访问:Redis是一种网络服务,客户端可以通过TCP/IP协议与Redis进行通信。Redis使用一种简单的协议来处理客户端请求,协议非常简单,易于实现客户端程序。
- 原子性操作:Redis支持原子性操作,这意味着在同一时间只有一个客户端能够对同一键执行操作。这可以避免数据竞争问题,并保证系统的数据一致性。
- 发布订阅模式:Redis支持发布订阅模式,可以方便地实现消息传递和事件驱动等功能。
2. Redis 应用案例
连续签到
// addContinuesDays 为用户签到续期
func addContinuesDays(ctx context.Context, userID int64) {
key := fmt.Sprintf(continuesCheckKey, userID)
// 1. 连续签到数+1
err := RedisClient.Incr(ctx, key).Err()
if err != nil {
fmt.Errorf("用户[%d]连续签到失败", userID)
} else {
expAt := beginningOfDay().Add(48 * time.Hour)
// 2. 设置签到记录在后天的0点到期
if err := RedisClient.ExpireAt(ctx, key, expAt).Err(); err != nil {
panic(err)
} else {
// 3. 打印用户续签后的连续签到天数
day, err := getUserCheckInDays(ctx, userID)
if err != nil {
panic(err)
}
fmt.Printf("用户[%d]连续签到:%d(天), 过期时间:%s", userID, day, expAt.Format("2006-01-02 15:04:05"))
}
}
}
- 根据用户ID生成签到记录的Redis Key。
- 使用Redis的INCR命令,将该Key对应的值自增1,表示用户连续签到天数增加了1。
- 使用Redis的EXPIREAT命令,设置该Key的过期时间为后天的0点,即48小时后。
- 使用getUserCheckInDays函数获取用户的当前连续签到天数,并打印用户续签后的连续签到天数和过期时间。
消息通知
用 list 作为消息队列
当文章更新时,将更新后的文章推送到ES,用户就能够搜索到最新的文章数据。
计数
对于一个用户有多项计数需求的情况,可以使用Redis的哈希表(hash)数据结构来实现。具体来说,可以将用户ID作为哈希表的键(key),不同的计数器类型作为哈希表的字段(field),对应的计数值作为哈希表的值(value)。
- 定义哈希表的键(key),通常可以使用用户ID来作为键。
- 对于每个计数器类型,使用Redis的HSET命令将计数值存储到哈希表中。例如,可以使用HSET命令将用户的文章计数值存储到哈希表的"article"字段中,将用户的评论计数值存储到哈希表的"comment"字段中等等。
- 可以使用Redis的HGET命令获取指定字段的计数值,或者使用HGETALL命令获取整个哈希表的计数信息。
排行榜
// 添加用户积分
func addUserScore(ctx context.Context, userID int64, score float64) error {
key := "user_scores"
return RedisClient.ZAdd(ctx, key, &redis.Z{
Score: score,
Member: userID,
}).Err()
}
// 获取用户排名
func getUserRank(ctx context.Context, userID int64) (int64, error) {
key := "user_scores"
rank, err := RedisClient.ZRevRank(ctx, key, userID).Result()
if err == redis.Nil {
// 用户不存在排行榜中
return -1, nil
}
return rank, err
}
// 获取前 N 名用户
func getTopUsers(ctx context.Context, n int64) ([]int64, error) {
key := "user_scores"
res, err := RedisClient.ZRevRange(ctx, key, 0, n-1).Result()
if err != nil {
return nil, err
}
users := make([]int64, len(res))
for i, member := range res {
users[i] = member.Member.(int64)
}
return users, nil
}
限流
func ex03Work(ctx context.Context, cInstParam common.CInstParams) {
routine := cInstParam.Routine
eventLogger := cInstParam.ConcurrentEventLogger
key := ex03LimitKey(time.Now())
currentQPS, err := RedisClient.Incr(ctx, key).Result()
if err != nil || err == redis.Nil {
err = RedisClient.Incr(ctx, ex03LimitKey(time.Now())).Err()
if err != nil {
panic(err)
}
}
if currentQPS > ex03MaxQPS {
// 超过流量限制,请求被限制
eventLogger.Append(common.EventLog{
EventTime: time.Now(),
Log: common.LogFormat(routine, "被限流[%d]", currentQPS),
})
// sleep 模拟业务逻辑耗时
time.Sleep(50 * time.Millisecond)
err = RedisClient.Decr(ctx, key).Err()
if err != nil {
panic(err)
}
} else {
// 流量放行
eventLogger.Append(common.EventLog{
EventTime: time.Now(),
Log: common.LogFormat(routine, "流量放行[%d]", currentQPS),
})
atomic.AddInt32(&accessQueryNum, 1)
time.Sleep(20 * time.Millisecond)
}
}
- 获取当前时间所在的秒数作为限流的 key。
- 使用 Redis 中的 Incr 命令对该 key 的值进行自增操作。
- 判断当前计数器的值是否超过了限流的阈值,如果超过了,就说明需要限流,请求被阻止。
- 如果计数器的值没有超过限流的阈值,就说明流量未达到限制,请求可以继续处理。
- 请求处理完毕后,使用 Redis 中的 Decr 命令对计数器的值进行自减操作。
分布式锁
可以使用 Redis 的 setnx 实现,利用了两个特性:
- Redis 是单线程执行命令
- setnx 只有未设置过才能执行成功
func ex02Work(ctx context.Context, cInstParam common.CInstParams) {
routine := cInstParam.Routine
eventLogger := cInstParam.ConcurrentEventLogger
defer ex02ReleaseLock(ctx, routine, eventLogger)
for {
// 1. 尝试获取锁
// exp - 锁过期设置,避免异常死锁
acquired, err := RedisClient.SetNX(ctx, resourceKey, routine, exp).Result() // 尝试获取锁
if err != nil {
eventLogger.Append(common.EventLog{
EventTime: time.Now(), Log: fmt.Sprintf("[%s] error routine[%d], %v", time.Now().Format(time.RFC3339Nano), routine, err),
})
panic(err)
}
if acquired {
// 2. 成功获取锁
eventLogger.Append(common.EventLog{
EventTime: time.Now(), Log: fmt.Sprintf("[%s] routine[%d] 获取锁", time.Now().Format(time.RFC3339Nano), routine),
})
// 3. sleep 模拟业务逻辑耗时
time.Sleep(10 * time.Millisecond)
eventLogger.Append(common.EventLog{
EventTime: time.Now(), Log: fmt.Sprintf("[%s] routine[%d] 完成业务逻辑", time.Now().Format(time.RFC3339Nano), routine),
})
return
} else {
// 没有获得锁,等待后重试
time.Sleep(100 * time.Millisecond)
}
}
}
- 尝试获取锁:使用 Redis 的 SetNX 命令尝试在 Redis 中创建指定的锁资源。SetNX 命令会将键值对存储到 Redis 中,如果该键值对不存在,则存储成功,并且返回 true,表示获取锁成功;否则返回 false,表示获取锁失败。
- 成功获取锁:当获取锁成功时,执行业务逻辑代码。在这个例子中,模拟了一个 10 毫秒的业务逻辑耗时。
- 释放锁:业务逻辑执行完毕后,使用 Redis 的 DEL 命令删除该锁资源。
3. Redis 使用注意事项
- 大Key:Value大于10KB就是大Key,使用大Key将导致Redis系统不稳定
解决方法:
- 分割大 key:将大 key 拆分成多个小的 key 存储,例如将一个大的列表分割成多个小的列表。这种方法适用于那些可以分割的数据结构。
- 使用 Redis 的数据结构:例如使用 Hash 替换多个 String,或者使用 Set 替换多个 List,这样可以减少内存使用。
- 删除不必要的数据:删除过期数据或者不必要的数据,可以减少内存占用。
- 压缩存储数据:对于大的 value,可以使用压缩算法进行压缩,减少内存占用。
- 热Key:一个Key的QPS特别高,将导致Redis实例出现负载突增,负责均衡流量不均的情况。导致单实例故障
解决方法:
- 数据预热:在 Redis 启动之前,提前将一些热门数据加载到内存中,避免服务启动后出现热 Key 导致的访问压力。
- 增加缓存层:在 Redis 之前增加一层缓存,如 Memcached,将热门数据缓存到这一层,减轻 Redis 的访问压力。
- 增加副本:将热门 Key 的副本分散到多个 Redis 节点上,减轻单个节点的访问压力。
-
慢查询:大Key、热Kye的读写;一次操作过多的Key(mset/hmset/sadd/zadd)
-
导致缓存穿透、缓存雪崩的场景及避免方案
缓存穿透是指查询一个不存在的key,由于缓存中没有对应的值,每次请求都会穿透到后端数据库,可能导致数据库压力过大甚至宕机。
缓存雪崩是指缓存中大量的key在同一时间失效,导致大量请求直接访问数据库,可能导致数据库压力过大甚至宕机。
常见的避免缓存穿透和缓存雪崩的方案包括:
- Bloom Filter 避免缓存穿透:使用 Bloom Filter 过滤掉不存在的 key,如果 Bloom Filter 认为 key 存在,则继续查询缓存,否则直接返回。
- 常规缓存使用过期时间,避免缓存雪崩:在缓存中设置随机的过期时间,使得不同缓存的过期时间不同,避免缓存同时失效导致的雪崩效应。
- 加锁,避免缓存雪崩:在缓存失效时,使用分布式锁锁住对该 key 的访问,等待缓存更新完成后释放锁,避免多个请求同时访问数据库。
- 永不过期,避免缓存雪崩:将热点数据设置为永不过期,避免缓存同时失效导致的雪崩效应。
- 限流,避免缓存雪崩:在缓存失效时,限制对数据库的请求并发数,避免瞬间对数据库的压力过大。
- 使用多级缓存,避免缓存穿透和缓存雪崩:使用多级缓存,例如本地缓存和分布式缓存,减少单一缓存的压力,避免缓存穿透和缓存雪崩。
三、课后个人总结
这堂课对于一个刚刚入门 Redis 的小白菜来说难度有点稍大,尤其是 Redis 的应用案例里的功能实现。不过本堂课学到的知识还是不少的,大致了解了 Redis 的基本原理,对 Redis 有了新的认识,同时对其的应用案例也有了更加深入的理解,同时也认识到了 Redis 在使用时需要注意的问题及针对性的解决方案,整体上有了一个更为系统、有序的 Redis 知识框架,日后还需要结合实践来多多练习巩固!
最近学习了很多知识,真的发现需要学习的内容越学越多,这边 MySQL 还没有吃透,Redis 又来凑热闹发😭,继续加油吧~
从头越,苍山如海,残阳如血!