6. Redis

6.1 Redis 简介

简单来说 Redis 就是一个数据库,不过与传统数据库不同的是 Redis 的数据是存在内存中的,所以存写速度非常快,因此 Redis 被广泛应用于缓存方向。另外,Redis 也经常用来做分布式锁。Redis 提供了多种数据类型来支持不同的业务场景。除此之外,Redis 支持事务 、持久化、LUA脚本、LRU 驱动事件、多种集群方案。


6.2 为什么要用 Redis / 为什么要用缓存

主要从“高性能”和“高并发”这两点来看待这个问题。

1. 高性能

假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!

在这里插入图片描述

2. 高并发

直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。

在这里插入图片描述


6.3 为什么要用 Redis 而不用 map/guava 做缓存?

根据缓存是否与应用进程属于同一进程,可以将内存分为本地缓存和分布式缓存本地缓存是在同一个进程内的内存空间中缓存数据,数据读写都是在同一个进程内完成;而分布式缓存是一个独立部署的进程并且一般都是与应用进程部署在不同的机器,故需要通过网络来完成分布式缓存数据读写操作的数据传输。

本地缓存的缺点:

  • 访问速度快,但无法进行大数据存储
    本地缓存相对于分布式缓存的好处是,由于数据不需要跨网络传输,故性能更好,但是由于占用了应用进程的内存空间,如 Java 进程的 JVM 内存空间,故不能进行大数据量的数据存储。

  • 集群的数据更新问题
    与此同时,本地缓存只支持被该应用进程访问,一般无法被其他应用进程访问,故在应用进程的集群部署当中,如果对应的数据库数据,存在数据更新,则需要同步更新不同部署节点的本地缓存的数据来包保证数据一致性,复杂度较高并且容易出错,如基于 Redis 的发布订阅机制来同步更新各个部署节点。

  • 数据随应用进程的重启而丢失
    由于本地缓存的数据是存储在应用进程的内存空间的,所以当应用进程重启时,本地缓存的数据会丢失。所以对于需要持久化的数据,需要注意及时保存,否则可能会造成数据丢失。

使用场景:

  • 本地缓存一般适合于缓存只读数据,如统计类数据。或者每个部署节点独立的数据,如长连接服务中,每个部署节点由于都是维护了不同的连接,每个连接的数据都是独立的,并且随着连接的断开而删除。

  • 如果数据在集群的不同部署节点需要共享和保持一致,则需要使用分布式缓存来统一存储,实现应用集群的所有应用进程都在该统一的分布式缓存中进行数据存取即可。

以 Java 为例,使用自带的 map 或者 guava 实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着 JVM 的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。

使用 Redis 或 memcached 之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持 redis 或 memcached 服务的高可用,整个程序架构上较为复杂。


6.4 Redis 和 Memcached 的区别

  • Redis 支持更丰富的数据类型(支持更复杂的应用场景):Redis不仅仅支持简单的k/v类型的数据,同时还提供 list,set,zset,hash等数据结构的存储;Memcache 仅支持简单的数据类型,String。

  • Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用;而 Memecached 不支持数据的持久化,把数据全部存在内存之中。

  • 集群模式:memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 Redis 目前是原生支持 cluster 模式的.

  • Memcached 是多线程,非阻塞 IO 复用的网络模型;Redis使用单线程的多路 IO 复用模型。

在这里插入图片描述

补充:关系型和非关系型数据库

  • 关系型数据库:指采用了关系模型来组织数据的数据库。简单来说,关系模式就是二维表格模型。主要代表:SQL Server,Oracle,Mysql,PostgreSQL。

    • 优点:(1)二维表格,容易理解;(2)通用的sql语句,使用方便;(3)数据库拥有ACID属性,易于维护;
    • 缺点:(1)高并发时效率低;(2)横向扩展难度相对较高;
  • 非关系型数据库 :主要指那些非关系型的、分布式的,且一般不保证ACID的数据存储系统,主要代表MongoDB,Redis、CouchDB。

    • 优点
      (1)面向高性能并发读写的key-value数据库。主要特点是具有极高的并发读写性能,例如Redis、Tokyo Cabint等。
      (2)面向海量数据访问的面向文档数据库。特点是,可以在海量的数据库快速的查询数据。例如MongoDB以及CouchDB;
      (3)面向可拓展的分布式数据库。解决的主要问题是传统数据库的扩展性上的缺陷。
    • 缺点 :由于Nosql约束少,所以也不能够像sql那样提供where字段属性的查询。因此适合存储较为简单的数据。有一些不能够持久化数据,所以需要和关系型数据库结合。

6.5 redis 常见数据结构以及使用场景分析

1. String

常用命令: set,get,decr,incr,mget 等。

String 数据结构是简单的key-value类型,value其实不仅可以是String,也可以是数字。 常规 key-value 缓存应用;常规计数:微博数,粉丝数等。

2. Hash

常用命令: hget,hset,hgetall 等。

Hash 是一个 string -> (field + value) 的映射表,hash 特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以Hash数据结构来存储用户信息,商品信息等等。比如下面我就用 hash 类型存放了我本人的一些信息:

在这里插入图片描述

3. List

常用命令: lpush,rpush,lpop,rpop,lrange等

list 就是链表,Redis list 的应用场景非常多,也是 Redis 最重要的数据结构之一,比如微博的关注列表,粉丝列表,消息列表等功能都可以用 Redis 的 list 结构来实现。

Redis list 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。

另外可以通过 lrange 命令,就是从某个元素开始读取多少个元素,可以基于 list 实现分页查询,这个很棒的一个功能,基于 redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西(一页一页的往下走),性能高。

4. Set

常用命令: sadd,spop,smembers,sunion 等

set 对外提供的功能与 list 类似是一个列表的功能,特殊之处在于 set 是可以自动去重的。

当你需要存储一个列表数据,又不希望出现重复数据时,set 是一个很好的选择,并且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作。

比如:在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程,具体命令:
sinterstore key1 key2 key3 将交集存在key1内

5. Sorted Set

常用命令: zadd,zrange,zrem,zcard等

和set相比,sorted set 增加了一个权重参数 score ,使得集合中的元素能够按 score 进行有序排列。

举例: 在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息,适合使用 Redis 中的 SortedSet 结构进行存储。


6.6 Redis 设置过期时间

Redis 中有个设置过期时间的功能,即对存储在 Redis 数据库中的值可以设置一个过期时间。作为一个缓存数据库,这是非常实用的。如我们一般项目中的 token 或者一些登录信息,尤其是短信验证码都是有时间限制的,按照传统的数据库处理方式,一般都是自己判断过期,这样无疑会严重影响项目性能。

我们 set key 的时候,都可以给一个 expire time,就是过期时间,通过过期时间我们可以指定这个 key 可以存活的时间。

如果假设你设置了一批 key 只能存活1个小时,那么接下来1小时后,redis 是怎么对这批 key 进行删除的?

定期删除+惰性删除。

  • 定期删除Redis 默认是每隔 100ms 就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。注意这里是随机抽取的。为什么要随机呢?你想一想假如 redis 存了几十万个 key ,每隔100ms就遍历所有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载!

  • 惰性删除 :定期删除可能会导致很多过期 key 到了时间并没有被删除掉,所以就有了惰性删除。假如你的过期 key,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个 key,才会被 Redis 给删除掉。这就是所谓的惰性删除,也是够懒的哈!

但是仅仅通过设置过期时间还是有问题的。我们想一下:如果定期删除漏掉了很多过期 key,然后你也没及时去查,也就没走惰性删除,此时会怎么样?如果大量过期key堆积在内存里,导致redis内存块耗尽了。怎么解决这个问题呢?此时就需要 Redis 内存淘汰机制


6.7 Redis 内存淘汰机制(MySQL 里有2000w数据,Redis 中只存20w的数据,如何保证 Redis 中的数据都是热点数据?)

Redis 提供 6种数据淘汰策略:

  1. volatile-lru:从已设置过期时间的 key 中,使用 LRU 算法淘汰
  2. volatile-lfu:从已设置过期时间的 key 中,使用 LFU 算法淘汰
  3. volatile-ttl:从已设置过期时间的 key 中,挑选将要过期的 key 淘汰
  4. volatile-random:从已设置过期时间的 key 中,随机选择 key 淘汰
  5. allkeys-lru:在所有 key 中,使用 LRU 算法淘汰
  6. allkeys-lfu:在所有 key 中,使用 LFU 算法淘汰
  7. allkeys-random:在所有 key 中随机移除 key
  8. no-eviction:当内存不足以容纳新写入数据时,新写入操作会报错

6.8 Redis 持久化机制(怎么保证 Redis 挂掉之后再重启数据可以进行恢复)

1. 快照(snapshotting)持久化(RDB)

RDB 方式,是将 Redis 某一时刻的数据持久化到磁盘中,是一种快照式的持久化方法。Redis 在进行数据持久化的过程中,会先将数据写入到一个临时文件中,待持久化过程都结束了,才会用这个临时文件替换上次持久化好的文件。正是这种特性,让我们可以随时来进行备份,因为快照文件总是完整可用的。

对于 RDB 方式,Redis会单独创建(fork)一个子进程来进行持久化,而主进程是不会进行任何 IO 操作的,这样就确保了 Redis 极高的性能。

快照持久化是 Redis 默认采用的持久化方式,在redis.conf配置文件中默认有此下配置:
在这里插入图片描述

2. AOF(append-only file)持久化

AOF 持久化以 日志 (即 appendonly.aof 文件)的形式来记录每个写操作,将 Redis 执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,Redis 启动之初会读取该文件重新构建数据,换言之,Redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作

默认的AOF持久化策略是每秒钟 fsync 一次( fsync 是指把缓存中的写指令记录到磁盘中),因为在这种情况下,Redis仍然可以保持很好的处理性能,即使 Redis 故障,也只会丢失最近1秒钟的数据。

因为采用了追加方式,如果不做任何处理的话,AOF 文件会变得越来越大,为此,Redis 提供了 AOF 文件重写(rewrite)机制,即当 AOF 文件的大小超过所设定的阈值时,Redis 就会启动 AOF 文件的内容压缩,只保留可以恢复数据的最小指令集。举个例子或许更形象,假如我们调用了100次 INCR 指令,在 AOF 文件中就要存储100条指令,但这明显是很低效的,完全可以把这100条指令合并成一条 SET 指令,这就是重写机制的原理

3. AOF 重写机制的原理

  1. 在重写即将开始之际,Redis 会创建(fork)一个“重写子进程”,这个子进程会首先读取现有的AOF文件,并将其包含的指令进行分析压缩并写入到一个临时文件中。

  2. 与此同时,主工作进程会将新接收到的写指令一边累积到内存缓冲区中,一边继续写入到原有的AOF文件中,这样做是保证原有的AOF文件的可用性,避免在重写过程中出现意外。

  3. 当“重写子进程”完成重写工作后,它会给父进程发一个信号,父进程收到信号后就会将内存中缓存的写指令追加到新AOF文件中。

  4. 当追加结束后,redis就会用新 AOF 文件来代替旧 AOF 文件,之后再有新的写指令,就都会追加到新的AOF文件中了。

4. RDB 和 AOF 的总结

RDB:

  • 优点:如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那 RDB 方式更高效。
  • 缺点:RDB需要定时持久化,风险是可能会丢两次持久之间的数据,量可能很大(如丢失5分钟内的数据)。

AOF:

  • 优点 :AOF 的实时性更好,假如出现问题,也只会丢失1s的数据;
  • 缺点 :在同样数据规模的情况下,AOF 文件要比 RDB 文件的体积大。而且,AOF方式的恢复速度也要慢于RDB方式。

5. RDB 和 AOF 的混合持久化

Redis 4.0 开始支持 RDB 和 AOF 的混合持久化。

如果把混合持久化打开,当 AOF 重写(rewrite) 时,新的AOF文件前半段是RDB格式的全量数据,后半段是AOF格式的增量数据。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。
在这里插入图片描述

数据恢复时,启动 Redis 依然优先加载 aof 文件,aof 文件加载可能有两种情况如下:

  • aof 文件开头是 RDB 的格式, 先加载 RDB 内容再加载剩余的 aof。

  • aof 文件开头不是 RDB 的格式,直接以 aof 格式加载整个文件。


6.9 Redis 事务

Redis 通过 MULTI、EXEC、WATCH 等命令来实现事务(transaction)功能。事务提供了一种将多个命令请求打包,然后一次性、按顺序地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而改去执行其他客户端的命令请求,它会将事务中的所有命令都执行完毕,然后才去处理其他客户端的命令请求。

在传统的关系式数据库中,常常用 ACID 性质来检验事务功能的可靠性和安全性。在 Redis 中,事务总是具有原子性(Atomicity)、一致性(Consistency)和隔离性(Isolation),并且当 Redis 运行在某种特定的持久化模式下时,事务也具有持久性(Durability)。


6.10 缓存雪崩和缓存穿透问题解决方案

1. 缓存雪崩

缓存在某一个时刻出现大规模的 key 失效(到了过期时间),那么就会导致大量的请求发给了数据库,造成数据库短时间内承受大量请求而崩掉。

解决办法:

  • 1) 事前

    • 均匀过期 :设置不同的过期时间,让缓存失效的时间尽量均匀,避免相同的过期时间导致缓存雪崩,造成大量数据库的访问。
    • 分级缓存 :第一级缓存失效的基础上,访问二级缓存,每一级缓存的失效时间都不同。
    • 热点数据缓存永远不过期。
    • 保证Redis缓存的高可用,防止Redis宕机导致缓存雪崩的问题。可以使用 主从+ 哨兵,Redis集群来避免 Redis 全盘崩溃的情况。
  • 2) 事中

    • 互斥锁 :在缓存失效后,通过互斥锁或者队列来控制读数据写缓存的线程数量,比如某个 key 只允许一个线程查询数据和写缓存,其他线程等待。这种方式会阻塞其他的线程,此时系统的吞吐量会下降。
    • 使用熔断机制,限流降级 :当流量达到一定的阈值,直接返回“系统拥挤”之类的提示,防止过多的请求打在数据库上将数据库击垮,至少能保证一部分用户是可以正常使用,其他用户多刷新几次也能得到结果。
  • 3) 事后

    • Redis 持久化机制,尽快恢复缓存数据,一旦重启,就能从磁盘上自动加载数据恢复内存中的数据。

在这里插入图片描述

2. 缓存穿透

缓存穿透是指用户请求的数据在缓存中不存在,即没有命中,同时在数据库中也不存在,导致用户每次请求该数据都要去数据库中查询一遍。如果有恶意攻击者不断请求系统中不存在的数据,会导致短时间大量请求落在数据库上,造成数据库压力过大,甚至导致数据库承受不住而宕机崩溃。

在这里插入图片描述

解决办法:

  • 1) 将无效的key存放进Redis中
    当出现 Redis 查不到数据,且数据库也查不到数据的情况,我们就把这个key保存到Redis中,设置 value=“null” ,并设置其过期时间极短,后面再出现查询这个 key 的请求的时候,直接返回 null ,就不需要再查询数据库了。但这种处理方式是有问题的,假如传进来的这个不存在的 key 值每次都是随机的,那存进 Redis 也没有意义

  • 2) 使用布隆过滤器
    如果布隆过滤器判定某个 key 不存在布隆过滤器中,那么就一定不存在,如果判定某个 key 存在,那么很大可能是存在(存在一定的误判率)。于是我们可以在缓存之前再加一个布隆过滤器,将数据库中的所有 key 都存储在布隆过滤器中,在查询 Redis 前先去布隆过滤器查询 key 是否存在,如果不存在就直接返回,不让其访问数据库,从而避免了对底层存储系统的查询压力


6.11 如何解决 Redis 的并发竞争 Key 问题

所谓 Redis 的并发竞争 Key 的问题也就是多个系统同时对一个 key 进行操作,但是最后执行的顺序和我们期望的顺序不同,这样也就导致了结果的不同

推荐一种方案:分布式锁(zookeeper 和 redis 都可以实现分布式锁)。(如果不存在 Redis 的并发竞争 Key 问题,不要使用分布式锁,这样会影响性能)

基于 zookeeper 临时有序节点可以实现的分布式锁。大致思想为:每个客户端对某个方法加锁时,在 zookeeper 上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁。

在实践中,当然是从以可靠性为主。所以首推Zookeeper。


6.12 如何保证缓存与数据库双写时的数据一致性?

掘金:https://juejin.cn/post/6850418121754050567
知乎:https://zhuanlan.zhihu.com/p/59167071

从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。这种方案下,我们可以对存入缓存的数据设置过期时间,所有的写操作以数据库为准,对缓存操作只是尽最大努力即可。也就是说如果数据库写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存。因此,接下来讨论的思路不依赖于给缓存设置过期时间这个方案。 在这里,我们讨论三种更新策略:

  1. 先更新数据库,再更新缓存
  2. 先删除缓存,再更新数据库
  3. 先更新数据库,再删除缓存

1. 先更新数据库,再更新缓存

这种场景一般是没有人使用的。

  • 如果你是一个写数据库场景比较多,而读数据场景比较少的业务需求,采用这种方案就会导致,数据压根还没读到,缓存就被频繁的更新,浪费性能。

  • 如果你写入数据库的值,并不是直接写入缓存的,而是要经过一系列复杂的计算再写入缓存。那么,每次写入数据库后,都再次计算写入缓存的值,无疑是浪费性能的。显然,删除缓存更为适合。
    举个例子,比如在数据库中有一个值为 1 的值,此时我们有 10 个请求对其每次加一的操作,但是这期间并没有读操作进来,如果用了先更新数据库的办法,那么此时就会有十个请求对缓存进行更新,会有大量的冷数据产生


2. 先删缓存,再更新数据库

在这里插入图片描述
此时来了两个请求,请求 A(更新操作) 和请求 B(查询操作)

  • A 会先删除 Redis 中的数据,然后去数据库进行更新操作
  • 此时 B 看到 Redis 中的数据时空的,会去数据库中查询该值,并将该值写到 Redis 中
  • 但是此时 A 并没有更新成功,或者事务还未提交

那么这时候就会产生数据库和 Redis 数据不一致的问题。如何解决呢?其实最简单的解决办法就是延时双删的策略。

在这里插入图片描述

但是上述的保证事务提交完以后再进行删除缓存还有一个问题,就是如果你使用的是 Mysql 的读写分离的架构的话,那么其实主从同步之间也会有时间差

在这里插入图片描述

此时来了两个请求,请求 A(更新操作) 和请求 B(查询操作)

  • A 删除了 Redis,并更新数据库
  • 主库与从库进行同步数据的操作
  • B 查询 Redis ,发现 Redis 中没有数据
  • B 去从库中拿数据
  • 此时同步数据还未完成,B 拿到的数据是旧数据,B 将旧数据写入 Redis
  • 数据库完成主从同步,从库变为新值

此时的解决办法:

1)如果是需要写数据到 Redis 的查询数据库操作,那么就强制将其指向主库进行查询。

2)还是使用双删延时策略。只是,睡眠时间修改为在主从同步的延时时间基础上,加几百ms。


3. 先更新数据库,后删除缓存

首先,先说一下。老外提出了一个缓存更新套路,名为《Cache-Aside pattern》。其中就指出

  • 失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
  • 命中:应用程序从cache中取数据,取到后返回。
  • 更新:先把数据存到数据库中,成功后,再让缓存失效。

这种情况不存在并发问题么?

不是的。假设这会有两个请求,一个请求A做查询操作,一个请求B做更新操作,那么会有如下情形产生

(1)缓存刚好失效

(2)请求A查询数据库,得一个旧值

(3)请求B将新值写入数据库

(4)请求B删除缓存

(5)请求A将查到的旧值写入缓存 ok,如果发生上述情况,确实是会发生脏数据。

但实际上,发生这种情况的概率是很小的 。因为数据库的读操作的速度是远快于写操作的,所以(4)发生在(5)之前的概率很小。

在这里插入图片描述

还有一种情况也会出现问题,比如更新数据库成功了,但是在删除缓存的阶段出错了没有删除成功,那么此时再读取缓存的时候每次都是错误的数据了。

此时解决方案就是利用消息队列进行删除的补偿。具体的业务逻辑用语言描述如下:

  • A 先对数据库进行更新操作
  • 在对 Redis 进行删除操作的时候发现报错,删除失败
  • 此时将 Redis 的 key 作为消息体发送到消息队列中
  • 系统接收到消息队列发送的消息后再次对 Redis 进行删除操作

在这里插入图片描述

但是这个方案会有一个缺点就是会对业务代码造成大量的侵入,深深的耦合在一起,所以这时会有一个优化的方案,我们知道对 Mysql 数据库更新操作后再 binlog 日志中我们都能够找到相应的操作,那么我们可以订阅 Mysql 数据库的 binlog 日志对缓存进行操作

在这里插入图片描述

参考博客:
https://zhuanlan.zhihu.com/p/59167071
https://juejin.cn/post/6850418121754050567

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值