大厂中怎么用Redis | 豆包MarsCode AI刷题

84 阅读6分钟

一、为什么用Redis?

一开始,Web访问服务端,服务端访问Mysql获取数据。随着时间的发展,一个数据库支撑不下去了,发展成了集群:

随着访问量的增加,有些数据是热点数据,会被经常访问到,因此,如何快速访问到热数据是一个关键问题。

在这个背景下,Redis出现了,Redis是一种基于内存框架,将热数据存在Redis中等价于存在内存中。

那么会有人问了,在内存中,电脑重启了数据不就是没了吗?

是这样的,基于此,Redis提供了持久化操作:

  • 全量数据RDB文件
  • 增量数据AOF文件

全量RDB文件是对数据当前状态保存一个快照,是二进制文件。

增量数据AOF是记录对Redis的操作至一个文件中。

启动的时候(开启AOF,RDB混合模式),先从RDB中加载全量文件,再根据AOF加载增量的数据文件,完成数据恢复。

最重要的一点是,Redis是单线程处理所有的操作命令:

二、数据结构详解

01、数据结构:

考虑三点设计:

  • 足够节省空间
  • 能够很快的读出数据
  • 能够很快的修改数据,也就是写进去。

在Redis中,String与其他语言定义的不同,底层实现是格式如下:

  • len:buf处实际占用大小
  • alloc:buf处分配的空间大小
  • flags:标志
  • buf:实际存储数据的位置

首先会在指针处左移获取alloc, len信息,然后右移读取具体的数据。

02、数据结构

数据结构,由双向链表和实现:

双向链表处有一个entry字段,该字段对应的是listpack数据结构,为什么要用这个呢? 主要是为了更多的存储数据。 下面仔细看一下listpack结构:

由于element是变长的,因此,我们需要知道起始位置,还需要知道数据类型。因此,element的数据格式如下:

03、

Hash和大部分一样,但是有一点,扩槽的时候如何做?

  1. Rehash:直接将中所有数据全部迁移到 中,数据量大的时候会阻塞用户请求。
  2. 渐进式Rehash:每次用户访问时都会迁移少量数据,将整个迁移过程,平摊到所有访问用户的请求过程中。

04、:

首先看一下skiplist(跳跃表):

假设我们要找,先在顶层的Head节点出发,走到发现后续没节点了,到下一层。

处,发现大,因此向右走,走到,然后没节点了,进入下一层。

处,发现小,因此向左走,正好找了。时间复杂度

zskiplist + hash

三、实用案例

1、连续签到

用户每日有一次签到的机会,如果断签,连续签到的技术将会归0.

连续签到的定义:每天必须在前签到。

Key: 用户唯一id
Value: 连续签到次数
expireAt: 过期时间,默认应该是后天0点。 

用到了数据结构

2、消息通知:

list作为消息队列:

使用场景:消息通知

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

3、计数

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

例如,个人主页统计这些数据,如果存在mysql中,访问量很大的话,对Mysql的负荷很大。

因此可以用Redis中的Hash表。

4、排行榜

积分变化时,排名要实时变更:zset

5、限流

  • 要求秒内放行的请求为,超过则禁止访问。
  • 数据结构:String
Key: comment_freq_limit_$timeStamp$

对上面的Key调用incr,超过限制则禁止访问。

6、分布式锁:

使用setnx命令,利用两个特性:

  • Redis是单线程执行命令。
  • setnx只有未设置过的才能执行成功。

SetNX不是高可用的分布式锁实现,还可能有以下问题:

  1. 某个占用锁的业务超时了,没办法释放分布式锁,导致其他线程没办法获取锁。
  2. 主从切换的时候,主节点还没有将分布式锁的状态同步到新主节点,这时候有人在新节点请求分布式锁,引起并发安全问题。
  3. Redis集群脑裂,导致出现多个主节点。

四、Redis使用注意事项

01、大Key、热Key

数据类型大key标准
Stringvalue的字节数大于10KB
Hash/Set/ZSet/list等元素个数大于5000个或总value字节数大于10MB

大Key的危害:

  • 读取成本高
  • 容易导致慢查询(过期、删除)
  • 主从复制异常导致读操作不可用,服务阻塞,无法正常响应请求

业务侧使用大key的表现

  • 请求Redis超时

消除大key的方法:

String

  1. 拆分key:

  1. 压缩

value压缩后写入Redis,读取时解压后再使用,一般使用gzip、snappy、lz4等。具体选择哪一个,需要测试一下哪个效果好。

如果存储的时JSON字符串,可以考虑使用MessagePack进行序列化。

集合类hash、list、set、zset:

  1. 拆分:用Hash取余+ 位掩码的方式决定放在哪个key中
  2. 区分冷热:例如,榜单列表场景使用zset,只缓存前10页数据,后续走database

热Key:用户访问一个Key的QPS特别高,导致Server实例出现CPU负载突增或者不均。

解决策略:

  1. LocalCache:将热key存在用户端,降低访问RedisQPS,如果LocalCache中缓存没有命中,或者过期了,从Redis中将数据更新到LocalCache中。Java中的Guava、Golang的BigCache就是Localcache。
  2. 拆分:将热key复制写入多份,例如:key1:value; key2:value,访问的时候随机key几。缺点就是,更新的时候需要更新多份,一旦有一个没有更新上,就会导致不一致。
  3. 使用Redis代理的热Key承载能力:

localcache挺好,但是每个服务都要配置一个localCache,业务多了,这个难免也多了。字节跳动内部使用Redis访问代理,本质上结合了热Key发现localCache两个功能。

02、慢查询

  1. 批量操作,一次性传入过多的key/value,如mset, hmset等,即便是有pipeLine,建议单批次不要超过
  2. zset命令大部分都是的,当大小超过的时候,也可能导致慢查询。
  3. 操作大key

03、缓存穿透、缓存雪崩

  • 缓存穿透:访问的数据Redis不存在,直接查找数据库。
  • 缓存雪崩:大量缓存同时过期。

解决缓存穿透:

  1. 对于不存在的key,在redis中缓存一个空值。
  2. 布隆过滤器

缓存雪崩:缓存存两份,差异化两个缓存过期时间。