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:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
  2. volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
  3. volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
  4. allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的).
  5. allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
  6. 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

猜你喜欢

转载自blog.csdn.net/cys975900334/article/details/115277402
今日推荐