写点什么

Redis 的常见问题

用户头像
赖猫
关注
发布于: 2021 年 04 月 24 日
Redis的常见问题

Redis 最常用的数据类型有 String 类型、Hash、List、Set、SortedSet(分数控制的有序 Set)。



Redis 常用数据类型

今天要介绍的是 Redis 的数据类型,顺便说说 Redis 的 ping-pong:



String 类型:最基本的数据类型,二进制安全,最大存储长度为 1G 的字符串,String 可以保存任何对象,无论是 JPG 图片,还是序列化的对象都是可以保存的!



如果要统计用户对网站的日访问量应该如何统计呢?其实很简单,如图只要把 UserId+日期当成 Key,并赋值为 0,用户每访问一次就把 key 对应的值+1,这样就可以轻松统计了:


string 类型的数据结构:


//保存字符串对象的数据结构struct sdshdr{    //buf中已占用空间长度    int len;
//buf中剩余空间 int free;
//数据空间 char buf[];}
复制代码

Hash:看看 Hash 数据类型,String 元素组成的,适合用于存储对象


List:列表,按照 String 元素插入顺序排序,大约可以存储 40 亿成员,List 可用于最新消息的展示,消息越新,越会立马展示


Set:String 元素组成的无序集合,通过 Hash 表实现,不允许重复


Redis 提供了求交集、并集、差集等操作,就可以很方便的实现如共同关注、共同喜好等功能


SortedSet:通过分数来为集合中的成员进行从小到大的排序


其实 Redis 还支持存储很多类型,用于计数的 HyperLogLog,用于支持存储地理位置信息的 Geo

Redis 海量数据里查询某固定前缀的 Key


首先看一段脚本,会向 Redis 插入 2000 万条数据:


for((i=1;i<=20000000;i++)); do echo "set k$i v$i" >> /tmp/redisTest.txt ;done;
复制代码

首先生成 2 千万条 redis 批量设置 kv 的语句(key=kn,value=vn)写入到/tmp 目录下的 redisTest.txt 文件中,去掉行尾的^M 符号:


vim /tmp/redisTest.txt:set fileformat=dos #设置文件的格式,通过这句话去掉每行结尾的^M符号:wq #保存退出
复制代码

通过 redis 提供的管道–pipe 形式,去跑 redis,传入文件的指令批量灌数据,需要花 10 分钟左右:


cat /tmp/redisTest.txt | redis-cli -h 主机ip -p 端口号 --pipe
复制代码


如果使用 keys 指令效果是这样的:


使用 keys 对线上的业务的影响 KEYS pattern:查找所有符合给定模式 pattern 的 key KEYS 指令会一次性返回所有匹配的 key,如果键的数量过大会使服务卡顿


那么应怎么做呢?


SCAN 指令可以帮我们解决这个问题,命令用于迭代当前数据库中的数据库键, 它们每次执行都只会返回少量元素, 所以这些命令可以用于生产环境, 而不会出现像 KEYS 命令带来的问题,当 KEYS 命令被用于处理一个大的数据库时,它们可能会阻塞服务器达数秒之久。


基于游标的迭代器,需要基于上一次的游标延续之前的迭代过程。以 0 作为游标开始一次新的迭代,直到命令返回游标 0 时完成一次遍历。不保证每次执行都返回某个给定数量的元素,支持模糊查询。一次返回的数量不可控,只能是大概率符合 count 参数。


虽然可能获取的 Key 是有重复的,但是只需要在应用中去重就好了。


Redis 如何实现分布式锁

这个问题其实在我之前的博客中已经说到过:《基于 Redis 实现分布式锁》 ,现在来回顾一下,其实主要就是几个指令:SETNX key value,如果 Key 不存在,则创建 Key、并赋值,时间复杂度为 O(1),如果设置成功则会返回 1,如果失败则返回 0。


如果一个线程成功设置了 Key,那么 Key 岂不是一直存在,别的线程根本不可能设置成功吗?是滴,所以要给 Key 加上一个过期时间,就要用到了 EXPIRE key seconds 这条指令了,当 Key 过期时(生存时间为 0),会被自动删除



于是乎我们可以得出伪代码如下:

int status = redisService.setnx(key, "1");if(status == 1){    redisService.expire(key, expire);    //TODO...}
复制代码

但是这样就有一个问题就是,如果在执行了 setnx 后程序挂掉了,那么就并没有设置超时时间,就形成了死锁,所以这样的做法是不可取的,因为必须要保证 sexnx 和设置超时时间这两个操作是原子的,从 Redis2.2.6 以后,set 命令可以把原来的 set 和 expire 命令合并在一起,成为一个原子操作:


SET key value[EX seconds][PX millisecond][NX][XX]
复制代码

EX seconds:设置键的过期时间为 seconds 秒


PX millisecond:设置键的过期时间为 millisecond 毫秒


NX:只有在键不存在的时候,才对键进行设置操作


XX:只有在键已经存在的时候,才对键进行设置操作


SET:操作完成时才会返回 OK,否则返回 nil


Redis 雪崩问题

目前电商首页以及热点数据都会去做缓存,一般缓存都是定时任务去刷新,或者查不到之后去更新缓存的,定时任务刷新就有一个问题, 如果首页所有 Key 的失效时间都是 12 小时,中午 12 点刷新的,我零点有个大促活动大量用户涌入,假设每秒 6000 个请求,本来缓存可以抗住每秒 5000 个请求,但是缓存中所有 Key 都失效了。此时 6000 个/秒的请求全部落在了数据库上,数据库必然扛不住,真实情况可能 DBA 都没反应过来直接挂了。此时,如果没什么特别的方案来处理,DBA 很着急,重启数据库,但是数据库立马又被新流量给打死了。这就是我理解的缓存雪崩。


简单来说就是在同一时间缓存大面积失效,瞬间 Redis 跟没有一样,那这个数量级别的请求直接打到数据库几乎是灾难性的。


那应该如何处理呢?在批量往 Redis 存数据的时候,把每个 Key 的失效时间都加个随机值就好了,这样可以保证数据不会再同一时间大面积失效。

setRedis(key, value, time+Math.random()*10000);
复制代码


如果 Redis 是集群部署,将热点数据均匀分布在不同的 Redis 库中也能避免全部失效。


或者设置热点数据永不过期,有更新操作就更新缓存就好了(比如运维更新了首页商品,那你刷下缓存就好了,不要设置过期时间),电商首页的数据也可以用这个操作,比较保险的做法。


缓存穿透和击穿

缓存穿透是指缓存和数据库中都没有的数据,而用户(黑客)不断发起请求。例如我们数据库的 id 都是从 1 自增的,如果发起 id=-1 的数据或者 id 特别大不存在的数据,这样的不断攻击导致数据库压力很大,严重会击垮数据库。


缓存穿透的两种解决方式:


方法一:在接口层增加校验,比如用户鉴权,参数做校验,不合法的校验直接 return,比如 id 做基础校验,id<=0 直接拦截;


方法二:Redis 里还有一个高级用法布隆过滤器(Bloom Filter)这个也能很好的预防缓存穿透的发生。它的原理也很简单,就是利用高效的数据结构和算法快速判断出你这个 Key 是否在数据库中存在,不存在你 return 就好了,存在你就去查 DB 刷新 KV 再 return;


缓存击穿和缓存雪崩有点像,但是又有一点不一样,缓存雪崩是因为大面积的缓存失效,打崩了 DB。而缓存击穿不同的是缓存击穿是指一个 Key 非常热点,在不停地扛着大量的请求,大并发集中对这一个点进行访问,当这个 Key 在失效的瞬间,持续的大并发直接落到了数据库上,就在这个 Key 的点上击穿了缓存。


缓存击穿的解决方式:


设置热点数据永不过期;或者加上互斥锁就搞定了,代码如下:

public static String getData(String key) throws InterruptedException {  //从Redis查询数据    String result = getDataByKV(key);  //参数校验  if (StringUtils.isBlank(result)) {    try {      //获得锁      if (reenLock.tryLock()) {        //去数据库查询        result = getDataByDB(key);        //校验        if (StringUtils.isNotBlank(result)) {          //插进缓存          setDataToKV(key, result);        }      } else {        //睡一会再拿        Thread.sleep(100L);        result = getData(key);      }    } finally {      //释放锁      reenLock.unlock();    }  }  return result;}
复制代码


Redis 如何做异步队列


使用 list 作为队列,RPUSH 生产消息,LPOP 消费消息。


如图所示,RPUSH 生产消息,LPOP 消费消息,但是当消息被消费完毕的时候 LPOP 不会等待,而是立即返回,通常的做法是让线程 Sleep 一会儿,再去尝试 LPOP。有没有更好的办法呢?有的:


BLPOP 指令:阻塞直到队列有消息或者超时


BLPOP key [key...] timeout
复制代码



但是这样做也有一个缺点就是,只能提供一个消费者进行消费,那么怎么解决这个问题呢?


pub/sub 主题订阅者模式:


  • 发送者(pub)发送消息,订阅者(sub)接收消息

  • 订阅者可以订阅定义数量的频道



下面演示一下:



pub/sub 订阅者模式的缺点:消息的发布是无状态的,无法保证可达,如果要解决这种问题就必须要使用专门的消息队列中间件来解决了,如 Kafka 等。


Redis 如何做持久化

Redis 为了保证效率,数据缓存在了内存中,但是会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件中,以保证数据的持久化。


Redis 的持久化策略有两种:


  • RDB:快照形式是直接把内存中的数据保存到一个 dump 的文件中,定时保存,保存策略。

  • AOF:把所有的对 Redis 的服务器进行修改的命令都存到一个文件里,命令的集合。Redis 默认是快照 RDB 的持久化方式。


当 Redis 重启的时候,它会优先使用 AOF 文件来还原数据集,因为 AOF 文件保存的数据集通常比 RDB 文件所保存的数据集更完整。你甚至可以关闭持久化功能,让数据只在服务器运行时存储。


RDB 的工作方式:默认 Redis 是会以快照”RDB”的形式将数据持久化到磁盘的一个二进制文件 dump.rdb。当 Redis 需要做持久化时,Redis 会 fork 一个子进程,子进程将数据写到磁盘上一个临时 RDB 文件中。当子进程完成写临时文件后,将原来的 RDB 替换掉,这样的好处是可以 copy-on-write。RDB 非常适合灾难恢复。RDB 的缺点是:如果你需要尽量避免在服务器故障时丢失数据,那么 RDB 不合适你。而且 RDB 是做了内存数据的全量同步,数据量大的时候会由于 IO 而严重影响性能


AOF 的工作方式:


appendfsync yes   appendfsync always     #每次有数据修改发生时都会写入AOF文件。appendfsync everysec   #每秒钟同步一次,该策略为AOF的缺省策略。
复制代码

AOF 可以做到全程持久化,只需要在配置中开启 appendonly yes。这样 Redis 每执行一个修改数据的命令,都会把它添加到 AOF 文件中,当 Redis 重启时,将会读取 AOF 文件进行重放,恢复到 Redis 关闭前的最后时刻。使用 AOF 的优点是会让 Redis 变得非常耐久。可以设置不同的 Fsync 策略,AOF 的默认策略是每秒钟 Fsync 一次,在这种配置下,就算发生故障停机,也最多丢失一秒钟的数据。缺点是对于相同的数据集来说,AOF 的文件体积通常要大于 RDB 文件的体积。根据所使用的 Fsync 策略,AOF 的速度可能会慢于 RDB。


两种持久化的比较:


1、如果你非常关心你的数据,但仍然可以承受数分钟内的数据丢失,那么可以只使用 RDB 持久。


2、AOF 将 Redis 执行的每一条命令追加到磁盘中,处理巨大的写入会降低 Redis 的性能,不知道你是否可以接受


数据库备份和灾难恢复:定时生成 RDB 快照非常便于进行数据库备份,并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度快。Redis 支持同时开启 RDB 和 AOF,系统重启后,Redis 会优先使用 AOF 来恢复数据,这样丢失的数据会最少。



上面的配置文件中 save 那一块主要是自动触发备份的条件,主动触发 RDB 持久化的命令:


SAVE 指令: 阻塞 Redis 的服务器进程,直到 RDB 文件被创建完毕


BGSAVE 指令:Fork 出一-个子进程来创建 RDB 文件,不阻塞服务器进程


日志重写解决 AOF 文件大小不断增大的问题,原理如下


  • 调用 fork() ,创建一个子进程

  • 子进程把新的 AOF 写到一个临时文件里,不依赖原来的 AOF 文件

  • 主进程持续将新的变动同时写到内存和原来的 AOF 里

  • 主进程获取子进程重写 AOF 的完成信号,往新 AOF 同步增量变动

  • 使用新的 AOF 文件替换掉旧的 AOF 文件


RDB 和 AOF 文件共存的情况下的恢复流程



Redis 的默认持久化方式:RDB 和 AOF 混合持久化方式的流程


BGSAVE 做全量持久化,AOF 做增量持久化。因为 BGSAVE 会耗费较长时间,不够实时,会导致大量丢失数据的问题,所以呢需要 AOF 来做增量持久化配合使用。


Pipeline 以及主从同步


Pipeline 和 Linux 的管道是类似的,还记得我们之前做的插入 2000 万条数据吗?

cat /tmp/redisTest.txt | redis-cli -h 主机ip -p 端口号 --pipe
复制代码


Redis 基于请求/响应模型,单个请求处理需要一一应答,所以如果需要批量操作数据的时候,每个数据操作都需要请求、应答的流程,那么 IO 负载将会变得非常高,为了提升效率,Pipeline 会批量执行指令,即一次发送多条指令,节省多次 IO 往返时间(但是这样做的前提是批量指令之间没有依赖性)。


主从同步的原理:


首先将 BGSAVE 的镜像文件做同步,在把期间的增量数据做同步。


全同步的过程:


  1. Salve 发送 sync 命令到 Master

  2. Master 启动一个后台进程,将 Redis 中的数据快照保存到文件中

  3. Master 将保存数据快照期间接收到的写命令缓存起来

  4. Master 完成写文件操作后,将该文件发送给 Salve

  5. 使用新的 AOF 文件替换掉旧的 AOF 文件

  6. Master 将这期间收集的增量写命令发送给 Salve 端


增量同步过程:


  1. Master 接收到用户的操作指令,判断是否需要传播到 Slave

  2. 将操作记录追加到 AOF 文件

  3. 将操作传播到其他 Slave : ①对齐主从库;②往响应缓存写入指令

  4. 将缓存中的数据发送给 Slave


Redis Sentinel 用来解决主从同步 Master 宕机后的主从切换问题:


  1. 监控:检查主从服务器是否运行正常

  2. 提醒:通过 API 向管理员或者其他应用程序发送故障通知

  3. 自动故障迁移:主从切换


流言协议 Gossip


在杂乱无章中寻求一致


  • 每个节点都随机地与对方通信,最终所有节点的状态达成一致

  • 种子节点定期随机向其他节点发送节点列表以及需要传播的消息

  • 不保证信息一定会传递给所有节点,但是最终会趋于一致


在区块链的去中心化实现方式中便用到了这种协议。


Redis 集群与一致性 Hash


如何从海量数据里快速找到所需? 分片:按照某种规则去划分数据,分散存储在多个节点上。常规的按照哈希划分无法实现节点的动态增减。


比如 userId 对 2 模,即可把用户数据分散到两台不同的数据库服务器,但是很容易出现数据分布不均匀的问题, 而且很难实现节点的动态增减。


什么是一致性 Hash 呢?其实就是对 2^32 取模,将哈希值空间组织成虚拟的圆环:



使用数据 Key 相同的函数 Hash 计算出 Hash 值


数据只需要进行 Hash 运算,然后顺时针找到最近的节点,就可以找到对应的服务:


那么这样做的好处是什么呢?


现在我们假设 Node C 宕机了,那么如下图:


即使 Node C 宕机了,那么也会找到最近的 Node D 节点,最大化的止损。


如果是增加服务器又会是怎样的情况呢?


如果是新增服务器,只会使一小部分数据发送改变,因为还是只需要找到最近的节点存储即可


接下来说说一致性 Hash 的缺点,Hash 环的数据倾斜问题:


此时,我们会引入虚拟节点来解决数据倾斜的问题



C/C++Linux 服务器开发/架构师免费学习地址:https://ke.qq.com/course/417774?flowToken=1031343


需要 C/C++ Linux 服务器架构师学习资料加群960994558获取(资料包括 C/C++,Linux,golang 技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg 等),免费分享


用户头像

赖猫

关注

C/C++Linux服务器开发学习群960994558 2020.11.28 加入

纸上得来终觉浅,绝知此事要躬行

评论

发布
暂无评论
Redis的常见问题