大厂是如何使用redis | 青训营;

95 阅读14分钟

大厂是如何使用redis | 青训营;

1.Redis

image.png

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:

  1. 快速访问性能:Redis数据存储在内存中,因此具有非常高的读写速度。相比传统的磁盘存储系统,如关系型数据库,Redis可以提供更低的延迟和更高的吞吐量,适用于对读写性能要求较高的应用场景。
  2. 缓存:作为一种高性能缓存系统,Redis可以将经常使用的数据存储在内存中,加速数据的访问速度。通过将常用的查询结果、计算结果或热门数据缓存到Redis中,可以减轻后端数据库的负载,提高整体系统的性能和响应速度。
  3. 分布式系统:Redis支持分布式部署,可以将数据分布在多个节点上,提供高可用性和可伸缩性。通过使用Redis的主从复制或集群模式,可以实现数据的备份和负载均衡,确保系统的可靠性和扩展性。
  4. 数据结构丰富:Redis支持多种数据结构,如字符串、哈希、列表、集合、有序集合等,可以更灵活地存储和操作数据。这使得Redis不仅可以作为简单的键值存储,还可以实现更复杂的数据模型和算法,如计数器、排行榜、消息队列等。
  5. 发布/订阅模式:Redis提供了发布/订阅功能,可以实现消息的发布和订阅,用于构建实时应用、事件驱动系统和消息队列等场景。发布者可以将消息发布到指定的频道,而订阅者可以订阅感兴趣的频道,接收相应的消息,实现异步通信和解耦。
  6. 事务支持:Redis支持事务操作,可以将多个命令打包成一个原子操作,确保这些命令要么全部执行成功,要么全部失败。这对于需要保持数据一致性的场景非常重要,可以避免数据更新过程中的并发问题。

1.3 为什么redis服务重启后数据不丢失

Redis之所以在服务重启后数据不丢失,是因为它提供了持久化功能,可以将数据保存到磁盘上,以防止数据丢失。

Redis的持久化功能有两种方式:

  1. 快照(snapshotting):Redis可以周期性地将内存中的数据快照保存到磁盘上,形成一个二进制文件(RDB文件)。这个快照文件包含了Redis在某个时间点的数据状态,包括键值对、过期时间等信息。当Redis重启时,它可以读取这个快照文件,将数据恢复到内存中,从而实现数据的持久化和恢复。
  2. 日志(append-only file):除了快照方式,Redis还可以使用日志方式来持久化数据。在这种模式下,Redis将每个写操作追加到一个日志文件中,称为AOF文件(Append-Only File)。这个文件包含了所有写操作的日志记录,当Redis重启时,它可以通过重新执行这些日志记录来还原数据。

通过使用快照和/或日志持久化机制,Redis可以将数据保存到磁盘上,并在服务重启后加载和恢复数据。这样即使在异常情况下(如服务器崩溃、断电等),Redis也能够保证数据的可靠性和持久性。

1.4 redis基本工作原理

  1. 内存存储:Redis将数据存储在内存中,这使得读写操作非常快速。内存中的数据以键值对的形式存储,其中键是唯一的标识符,而值可以是各种数据类型。
  2. 客户端-服务器模型:Redis采用客户端-服务器模型,客户端与Redis服务器进行通信。客户端可以通过网络连接到Redis服务器,发送命令并接收相应的结果。
  3. 单线程处理:Redis通常采用单线程的方式处理客户端请求。这是因为Redis的主要瓶颈在于内存和网络带宽,而不是CPU计算能力。通过单线程处理请求,避免了线程切换和锁竞争的开销,提高了并发性能。
  4. 响应式设计:Redis使用事件驱动的方式处理请求。当客户端发送命令时,Redis会根据命令类型执行相应的操作,并将结果返回给客户端。通过异步非阻塞的方式处理请求,Redis可以同时处理多个客户端请求,提高了系统的吞吐量。
  5. 持久化:Redis提供了持久化功能,可以将数据保存到磁盘上,以防止数据丢失。它支持快照(snapshotting)和日志(append-only file)两种持久化方式,可以根据配置选择其中一种或同时使用两种方式。
  6. 数据结构和操作:Redis支持多种数据结构,如字符串(strings)、哈希(hashes)、列表(lists)、集合(sets)、有序集合(sorted sets)等。它提供了丰富的操作命令,可以对这些数据结构进行增删改查等操作。

2.redis应用

2.1 连续签到

掘金每日连续签到

  • 用户每日有一次签到的机会,如果断签,连续签到计数将归0
  • 连续签到的定义:每天必须在23:59:59前签到
  • Key: cc_uid_1165894833417101
  • value: 252
  • expireAt:后天的0点

image.png

数据结构 - sds

  • 可以存储 字符串、数字、二进制数据
  • 通常和expire配合使用
  • 场景: 存储计数、Session

2.2 消息通知

image.png

消息通知

  • 例如当文章更新时,将更新
  • 后的文章推送到ES,用户就
  • 能搜索到最新的文章数据

image.png

List数据结构Quicklist

  • Quicklist由一一个双向链表和listpack实现

2.3 计数

image.png

一个用户有多项计数需求,可通过hash结构存储

使用Go-Redis库进行批处理的基本步骤:

  1. 安装依赖:使用Go模块管理工具(如go mod)进行依赖管理,将Go-Redis库添加到项目中。
 go get github.com/go-redis/redis/v8
  1. 导入库:在Go代码中导入Go-Redis库。
 import "github.com/go-redis/redis/v8"
  1. 创建Redis客户端:使用Go-Redis库提供的方法创建Redis客户端实例。
 client := redis.NewClient(&redis.Options{
     Addr:     "localhost:6379", // Redis服务器地址
     Password: "",               // Redis密码,如果没有设置则为空
     DB:       0,                // Redis数据库索引,默认为0
 })
  1. 执行批处理操作:使用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方法会返回一个错误。因此,我们需要根据实际情况进行错误处理。

image.png

Hash数据结构dict

  • rehash: rehash操作是将ht[O]中的数据 全部迁移到ht[1]中。数据量小的场景下 直接将数据从ht[O]拷贝到ht[1]速度是较 快的。数据量大的场景,例如存有上百 万的KV时,迁移过程将会明显阻塞用户 请求。
  • 渐进式rehash:为避免出现这种情况,使 用了rehash方案。基本原理就是,每次用 户访问时都会迁移少量数据。将整个迁 移过程,平摊到所有的访问用不请求过 程中

2.4 排行榜

image.png

积分变化时,排名要实时变更 结合dict后,可实现通过key操作跳表的功能

image.png

zset数据结构 zskiplist

  • 查找数字7的路径,head,3.3.7
  • 结合dict后,可实现通过key操作跳表的功能
  • ZINCRBY myzset 2 "Alex'
  • ZSCORE myzset "Alex"

2.5 限流

image.png

要求1秒内放行的请求为N,超过N则禁止访问

  • Key: comment_freq_limit_1671356046
  • 对这个Key调用incr,超过限制N则禁止访问
  • 1671356046 是当前时间戳

在Go语言中,可以使用Redis实现简单的限流功能。以下是一个使用Redis实现令牌桶算法进行限流的示例:

  1. 安装依赖:使用Go模块管理工具(如go mod)进行依赖管理,将Go-Redis库添加到项目中。
 go get github.com/go-redis/redis/v8
  1. 导入库:在Go代码中导入Go-Redis库。
 import (
     "github.com/go-redis/redis/v8"
     "time"
 )
  1. 创建Redis客户端:使用Go-Redis库提供的方法创建Redis客户端实例。
 client := redis.NewClient(&redis.Options{
     Addr:     "localhost:6379", // Redis服务器地址
     Password: "",               // Redis密码,如果没有设置则为空
     DB:       0,                // Redis数据库索引,默认为0
 })
  1. 限流函数:实现一个限流函数,该函数将根据令牌桶算法判断是否允许执行操作。
 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 分布式锁

image.png

分布式锁

  • 并发场景,要求一次只能有一个协程执行
  • 执行完成后,其它等待中的协程才能执行
  • 可以使用redis的setnx实现,利用了两个特性
  • Redis是单线程执行命令
  • setnx只有未设置过才能执行成功

3.Redis使用的注意事项

3.1 大key与热key

image.png

Redis中,"大key"和"热key"是两个常见的性能问题。

  1. 大key(Big Key):大key指的是存储在Redis中占用较大内存空间的键。当一个键值对的值非常大时,就会成为大key。大key可能会对Redis的性能产生负面影响,因为在进行某些操作时,需要一次性加载整个大key的值到内存中,导致占用大量的内存和网络带宽。

为了解决大key问题,可以采取以下策略:

  • 将大key拆分成多个较小的键值对,以减少单个键值对的大小。

  • 使用Redis提供的数据结构,如列表、集合或有序集合,来代替大key,以实现更高效的数据存储和访问。

  1. 热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)是两个与缓存相关的常见问题。

  1. 缓存穿透:缓存穿透指的是对于一个不存在于缓存中的数据,每次请求都会直接查询数据库或其他数据源,导致大量的请求直接击穿缓存,给后端系统带来很大压力。

缓存穿透可能发生的原因包括:

  • 恶意攻击:故意请求不存在的数据,以达到消耗后端资源的目的。
  • 高并发场景:大量并发请求同时查询不存在的数据。

为了解决缓存穿透问题,可以采取以下策略:

  • 布隆过滤器(Bloom Filter):使用布隆过滤器对请求的键进行过滤,将一部分确定不存在的请求快速过滤掉,减轻对后端系统的压力。

  • 缓存空值(Cache Null Values):对于查询结果为空的请求,将空结果缓存一段时间,避免大量无效请求直接访问后端系统。

  1. 缓存雪崩:缓存雪崩指的是在缓存中的大量数据同时过期失效,导致大量请求直接访问后端系统,造成后端系统压力剧增,甚至导致系统崩溃。

缓存雪崩可能发生的原因包括:

  • 缓存服务器故障:由于缓存服务器故障或宕机,导致缓存中的数据全部失效。
  • 缓存数据过期时间设置相近:如果大量缓存数据的过期时间设置相近,可能会导致它们同时过期,引发缓存雪崩。

为了解决缓存雪崩问题,可以采取以下策略:

  • 设置合理的缓存过期时间:合理分散缓存数据的过期时间,避免同时失效。
  • 实施热点数据永不过期策略:对于热点数据,可以将其设置为永不过期,确保热点数据一直可用。
  • 限制缓存数据的并发更新:在缓存数据失效时,只允许一台服务器或线程去更新缓存,其他请求等待加载缓存。