大厂是如何使用redis | 青训营;
1.Redis
1.1 什么是redis
Redis(Remote Dictionary Server)是一个开源的内存数据结构存储系统,它可以用作数据库、缓存和消息中间件。它支持多种数据结构,如字符串(strings)、哈希(hashes)、列表(lists)、集合(sets)、有序集合(sorted sets)等。
Redis通常被用作缓存系统,因为它将数据存储在内存中,使得数据的读写速度非常快。它还提供持久化功能,可以将数据定期保存到磁盘上,以防止数据丢失。Redis的持久化机制有两种选择:快照(snapshotting)和日志(append-only file)。
除了缓存之外,Redis还可以用作消息中间件。它提供了发布/订阅(pub/sub)功能,允许不同的应用程序之间通过消息进行通信。这使得Redis在构建实时应用、消息队列和事件驱动系统时非常有用。
Redis具有非常丰富的功能和灵活的配置选项,它支持各种操作和命令,可以通过网络进行访问。它提供了多种客户端库,使得各种编程语言都可以与Redis进行交互。
1.2 为什么需要redis
Redis有以下几个主要的使用场景和优势,解释了为什么需要Redis:
- 快速访问性能:Redis数据存储在内存中,因此具有非常高的读写速度。相比传统的磁盘存储系统,如关系型数据库,Redis可以提供更低的延迟和更高的吞吐量,适用于对读写性能要求较高的应用场景。
- 缓存:作为一种高性能缓存系统,Redis可以将经常使用的数据存储在内存中,加速数据的访问速度。通过将常用的查询结果、计算结果或热门数据缓存到Redis中,可以减轻后端数据库的负载,提高整体系统的性能和响应速度。
- 分布式系统:Redis支持分布式部署,可以将数据分布在多个节点上,提供高可用性和可伸缩性。通过使用Redis的主从复制或集群模式,可以实现数据的备份和负载均衡,确保系统的可靠性和扩展性。
- 数据结构丰富:Redis支持多种数据结构,如字符串、哈希、列表、集合、有序集合等,可以更灵活地存储和操作数据。这使得Redis不仅可以作为简单的键值存储,还可以实现更复杂的数据模型和算法,如计数器、排行榜、消息队列等。
- 发布/订阅模式:Redis提供了发布/订阅功能,可以实现消息的发布和订阅,用于构建实时应用、事件驱动系统和消息队列等场景。发布者可以将消息发布到指定的频道,而订阅者可以订阅感兴趣的频道,接收相应的消息,实现异步通信和解耦。
- 事务支持:Redis支持事务操作,可以将多个命令打包成一个原子操作,确保这些命令要么全部执行成功,要么全部失败。这对于需要保持数据一致性的场景非常重要,可以避免数据更新过程中的并发问题。
1.3 为什么redis服务重启后数据不丢失
Redis之所以在服务重启后数据不丢失,是因为它提供了持久化功能,可以将数据保存到磁盘上,以防止数据丢失。
Redis的持久化功能有两种方式:
- 快照(snapshotting):Redis可以周期性地将内存中的数据快照保存到磁盘上,形成一个二进制文件(RDB文件)。这个快照文件包含了Redis在某个时间点的数据状态,包括键值对、过期时间等信息。当Redis重启时,它可以读取这个快照文件,将数据恢复到内存中,从而实现数据的持久化和恢复。
- 日志(append-only file):除了快照方式,Redis还可以使用日志方式来持久化数据。在这种模式下,Redis将每个写操作追加到一个日志文件中,称为AOF文件(Append-Only File)。这个文件包含了所有写操作的日志记录,当Redis重启时,它可以通过重新执行这些日志记录来还原数据。
通过使用快照和/或日志持久化机制,Redis可以将数据保存到磁盘上,并在服务重启后加载和恢复数据。这样即使在异常情况下(如服务器崩溃、断电等),Redis也能够保证数据的可靠性和持久性。
1.4 redis基本工作原理
- 内存存储:Redis将数据存储在内存中,这使得读写操作非常快速。内存中的数据以键值对的形式存储,其中键是唯一的标识符,而值可以是各种数据类型。
- 客户端-服务器模型:Redis采用客户端-服务器模型,客户端与Redis服务器进行通信。客户端可以通过网络连接到Redis服务器,发送命令并接收相应的结果。
- 单线程处理:Redis通常采用单线程的方式处理客户端请求。这是因为Redis的主要瓶颈在于内存和网络带宽,而不是CPU计算能力。通过单线程处理请求,避免了线程切换和锁竞争的开销,提高了并发性能。
- 响应式设计:Redis使用事件驱动的方式处理请求。当客户端发送命令时,Redis会根据命令类型执行相应的操作,并将结果返回给客户端。通过异步非阻塞的方式处理请求,Redis可以同时处理多个客户端请求,提高了系统的吞吐量。
- 持久化:Redis提供了持久化功能,可以将数据保存到磁盘上,以防止数据丢失。它支持快照(snapshotting)和日志(append-only file)两种持久化方式,可以根据配置选择其中一种或同时使用两种方式。
- 数据结构和操作:Redis支持多种数据结构,如字符串(strings)、哈希(hashes)、列表(lists)、集合(sets)、有序集合(sorted sets)等。它提供了丰富的操作命令,可以对这些数据结构进行增删改查等操作。
2.redis应用
2.1 连续签到
掘金每日连续签到
- 用户每日有一次签到的机会,如果断签,连续签到计数将归0
- 连续签到的定义:每天必须在23:59:59前签到
- Key: cc_uid_1165894833417101
- value: 252
- expireAt:后天的0点
数据结构 - sds
- 可以存储 字符串、数字、二进制数据
- 通常和expire配合使用
- 场景: 存储计数、Session
2.2 消息通知
消息通知
- 例如当文章更新时,将更新
- 后的文章推送到ES,用户就
- 能搜索到最新的文章数据
List数据结构Quicklist
- Quicklist由一一个双向链表和listpack实现
2.3 计数
一个用户有多项计数需求,可通过hash结构存储
使用Go-Redis库进行批处理的基本步骤:
- 安装依赖:使用Go模块管理工具(如go mod)进行依赖管理,将Go-Redis库添加到项目中。
go get github.com/go-redis/redis/v8
- 导入库:在Go代码中导入Go-Redis库。
import "github.com/go-redis/redis/v8"
- 创建Redis客户端:使用Go-Redis库提供的方法创建Redis客户端实例。
client := redis.NewClient(&redis.Options{ Addr: "localhost:6379", // Redis服务器地址 Password: "", // Redis密码,如果没有设置则为空 DB: 0, // Redis数据库索引,默认为0 })
- 执行批处理操作:使用Redis客户端提供的批处理命令进行批处理操作。
// 创建一个批处理管道 pipe := client.Pipeline() // 添加多个命令到管道中 pipe.Set("key1", "value1", 0) pipe.Set("key2", "value2", 0) pipe.Set("key3", "value3", 0) // 执行批处理命令 _, err := pipe.Exec() if err != nil { // 处理错误 } // 关闭管道 pipe.Close()在上述代码中,我们创建了一个批处理管道(Pipeline),然后通过管道添加多个命令(如Set)到管道中,最后使用
Exec方法执行批处理操作。批处理操作会将多个命令一次性发送给Redis服务器执行,可以提高操作的效率。需要注意的是,在批处理操作中,如果某个命令执行失败,
Exec方法会返回一个错误。因此,我们需要根据实际情况进行错误处理。
Hash数据结构dict
- rehash: rehash操作是将ht[O]中的数据 全部迁移到ht[1]中。数据量小的场景下 直接将数据从ht[O]拷贝到ht[1]速度是较 快的。数据量大的场景,例如存有上百 万的KV时,迁移过程将会明显阻塞用户 请求。
- 渐进式rehash:为避免出现这种情况,使 用了rehash方案。基本原理就是,每次用 户访问时都会迁移少量数据。将整个迁 移过程,平摊到所有的访问用不请求过 程中
2.4 排行榜
积分变化时,排名要实时变更 结合dict后,可实现通过key操作跳表的功能
zset数据结构 zskiplist
- 查找数字7的路径,head,3.3.7
- 结合dict后,可实现通过key操作跳表的功能
- ZINCRBY myzset 2 "Alex'
- ZSCORE myzset "Alex"
2.5 限流
要求1秒内放行的请求为N,超过N则禁止访问
- Key: comment_freq_limit_1671356046
- 对这个Key调用incr,超过限制N则禁止访问
- 1671356046 是当前时间戳
在Go语言中,可以使用Redis实现简单的限流功能。以下是一个使用Redis实现令牌桶算法进行限流的示例:
- 安装依赖:使用Go模块管理工具(如go mod)进行依赖管理,将Go-Redis库添加到项目中。
go get github.com/go-redis/redis/v8
- 导入库:在Go代码中导入Go-Redis库。
import ( "github.com/go-redis/redis/v8" "time" )
- 创建Redis客户端:使用Go-Redis库提供的方法创建Redis客户端实例。
client := redis.NewClient(&redis.Options{ Addr: "localhost:6379", // Redis服务器地址 Password: "", // Redis密码,如果没有设置则为空 DB: 0, // Redis数据库索引,默认为0 })
- 限流函数:实现一个限流函数,该函数将根据令牌桶算法判断是否允许执行操作。
func IsAllowed(key string, capacity int64, rate time.Duration) bool { // 获取当前时间戳 now := time.Now().UnixNano() // 使用Redis的ZADD命令将当前时间戳添加到有序集合中 // 有序集合的分值为时间戳,成员为时间戳对应的字符串 client.ZAdd(context.Background(), key, &redis.Z{ Score: float64(now), Member: strconv.FormatInt(now, 10), }) // 使用Redis的ZREMRANGEBYSCORE命令移除指定范围内的时间戳 // 保留的时间范围为当前时间戳减去限流时间窗口的长度 client.ZRemRangeByScore(context.Background(), key, "-inf", strconv.FormatInt(now-int64(rate), 10)) // 使用Redis的ZCARD命令获取有序集合的成员数量 // 如果成员数量超过限流容量,则表示限流,返回false // 否则,表示未限流,返回true count, err := client.ZCard(context.Background(), key).Result() if err != nil { // 处理错误 return false } return count <= capacity }在上述代码中,我们使用Redis的有序集合来实现令牌桶算法。每次请求时,将当前时间戳作为有序集合的分值和成员添加到Redis中,然后使用ZREMRANGEBYSCORE命令移除过期的时间戳,最后使用ZCARD命令获取有序集合的成员数量,判断是否超过限流容量。
使用该限流函数时,可以在需要进行限流的地方进行调用,例如:
if IsAllowed("my_key", 100, time.Second) { // 执行操作 } else { // 进行限流处理 }上述示例中,
my_key是Redis中的键,指定了限流的作用范围;100是限流容量,表示在每秒内允许执行的操作数量;time.Second表示限流的时间窗口,这里设置为1秒。
2.6 分布式锁
分布式锁
- 并发场景,要求一次只能有一个协程执行
- 执行完成后,其它等待中的协程才能执行
- 可以使用redis的setnx实现,利用了两个特性
- Redis是单线程执行命令
- setnx只有未设置过才能执行成功
3.Redis使用的注意事项
3.1 大key与热key
Redis中,"大key"和"热key"是两个常见的性能问题。
- 大key(Big Key):大key指的是存储在Redis中占用较大内存空间的键。当一个键值对的值非常大时,就会成为大key。大key可能会对Redis的性能产生负面影响,因为在进行某些操作时,需要一次性加载整个大key的值到内存中,导致占用大量的内存和网络带宽。
为了解决大key问题,可以采取以下策略:
将大key拆分成多个较小的键值对,以减少单个键值对的大小。
使用Redis提供的数据结构,如列表、集合或有序集合,来代替大key,以实现更高效的数据存储和访问。
- 热key(Hot Key):热key指的是在Redis中频繁访问的键。当一个键被频繁地读取或写入时,就会成为热key。热key可能会导致某些操作的延迟增加,因为Redis是单线程的,对于热key的读写操作会造成其他操作的阻塞。
为了解决热key问题,可以考虑以下方法:
- 使用Redis的主从复制或集群模式,将读操作分摊到多个Redis节点上,以减轻单个节点的压力。
- 使用Redis的缓存淘汰策略,如LRU(最近最少使用)或LFU(最近最不常用),在内存不足时自动淘汰一些热key,以保证整体性能。
3.2 慢查询
容易导致redis慢查询的操作 (1)批量操作一次性传入过多的key/value,如mset/hmset/sadd/zadd等o(n)操作 建议单批次不要超过100,超过100之后性能下降明显。 (2)zset大部分命令都是o(logln)),当大小超过5k以上时,简单的zadd/zrem也可能导致慢查 询 (3)操作的单个value过大,超过10KB。也即,避免使用大Key (4)对大key的delete/expire操作也可能导致慢查询,Redis4.0之前不支持异步删除unlink,大key删除会阻塞Redis
3.3 缓存穿透与缓存雪崩
缓存穿透(Cache Penetration)和缓存雪崩(Cache Avalanche)是两个与缓存相关的常见问题。
- 缓存穿透:缓存穿透指的是对于一个不存在于缓存中的数据,每次请求都会直接查询数据库或其他数据源,导致大量的请求直接击穿缓存,给后端系统带来很大压力。
缓存穿透可能发生的原因包括:
- 恶意攻击:故意请求不存在的数据,以达到消耗后端资源的目的。
- 高并发场景:大量并发请求同时查询不存在的数据。
为了解决缓存穿透问题,可以采取以下策略:
布隆过滤器(Bloom Filter):使用布隆过滤器对请求的键进行过滤,将一部分确定不存在的请求快速过滤掉,减轻对后端系统的压力。
缓存空值(Cache Null Values):对于查询结果为空的请求,将空结果缓存一段时间,避免大量无效请求直接访问后端系统。
- 缓存雪崩:缓存雪崩指的是在缓存中的大量数据同时过期失效,导致大量请求直接访问后端系统,造成后端系统压力剧增,甚至导致系统崩溃。
缓存雪崩可能发生的原因包括:
- 缓存服务器故障:由于缓存服务器故障或宕机,导致缓存中的数据全部失效。
- 缓存数据过期时间设置相近:如果大量缓存数据的过期时间设置相近,可能会导致它们同时过期,引发缓存雪崩。
为了解决缓存雪崩问题,可以采取以下策略:
- 设置合理的缓存过期时间:合理分散缓存数据的过期时间,避免同时失效。
- 实施热点数据永不过期策略:对于热点数据,可以将其设置为永不过期,确保热点数据一直可用。
- 限制缓存数据的并发更新:在缓存数据失效时,只允许一台服务器或线程去更新缓存,其他请求等待加载缓存。