大白话说明白Redis及其常见问题

WHAT IS REDIS

简单来说 redis 就是一个数据库,与传统数据库不同的是:

redis 的数据是存在内存中的,所以读写速度非常快。

因此 redis 被广泛应用于缓存方向,这也是它无法传统型SQL数据库的原因,存储在内存的容量注定比硬盘小得多

redis 提供了多种数据类型来支持不同的业务场景。

除此之外,redis 支持事务 、持久化、LUA脚本、LRU驱动事件、多种集群方案

于是,我们可以轻易得出,REDIS是用来当作缓存的

为什么使用缓存?从两点谈

  • 高性能
  • 高并发

看看下图

这是互联网中,用户们访问系统的数据的大概图

  • 前台页面做静态化
  • 后台关系型数据库转非关系型数据库

这样,可以有效减少SQL语句的执行,就减少了跟硬盘打交道,实现了高性能。另外,Redis作为缓存,不会在内部频繁出现数据改动,可以很好得应对高并发

Redis线程模型

Redis是一个单线程工作模型

可是,为什么单线程模型却能很好的应对高并发呢?

redis 内部使用文件事件处理器 file event handler(文件事件分派器),这个文件事件处理器是单线程的,所以 redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进行处理

请看下图

因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型

多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将 socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理

为什么Redis这样的单线程模型却能支撑高并发

其实Redis是用效率弥补了单线程的缺陷

  • 纯内存操作
  • 非阻塞的 IO 多路复用机制同时监听多个 socket
  • 单线程反而避免了多线程的频繁上下文切换问题,切来切去也是需要事件的嗷

Redis数据结构

Redis_Hash

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

hash叫散列(哈希)类型,它提供了字段和字段值的映射。字段值只能是字符串类型,不支持散列类型、集合类型等其它类型。如下:

Redis_List

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

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

列表类型(list)可以存储一个有序的字符串列表,常用的操作是向列表两端添加元素,或者获得列表的某一个片段

列表类型内部是使用双向链表(double linked list)实现的,所以向列表两端添加元素的时间复杂度为0(1),获取越接近两端的元素速度就越快。这意味着即使是一个有几千万个元素的列表,获取头部或尾部的10条记录也是极快的

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

Redis_set

在set集合中的每个元素都是不同的,且没有顺序

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

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

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

sinterstore key1 key2 key3     将交集存在key1内

集合类型和列表类型的对比:

集合类型的常用操作是向集合中加入或删除元素判断某个元素是否存在等,由于集合类型的Redis内部是使用值为空的散列表实现,所有这些操作的时间复杂度都为0(1)

Redis还提供了多个集合之间的交集、并集、差集的运算

Redis_Sorted_Set

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

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

Redis设置过期时间

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

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

redis是怎么对过期的key进行删除的?

Redis采取定期删除+惰性删除处理过期的Key

  • redis默认是每隔 100ms 就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。注意这里是随机抽取的。为什么要随机呢?你想一想假如 redis 存了几十万个 key ,每隔100ms就遍历所有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载
  • 定期删除可能会导致很多过期 key 到了时间并没有被删除掉。所以就有了惰性删除。假如你的过期 key,靠定期删除没有被删除掉,那么当这个key被系统读取到的时候,会被检测到其过期,再将其删除

双保险策略保证了过期失效的可靠性

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

redis 内存淘汰机制

MySQL里有2000w数据,Redis中只存20w的数据,如何保证Redis中的数据都是热点数据?

如果你了解JVM的垃圾回收,可以试着联想对比一下

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:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错,太暴力了,这就是杀敌一千自损一百的损招

Redis持久化

由于Redis的数据是在内存中,那么怎么保证 redis 挂掉之后再重启数据可以进行恢复?

很多时候我们需要持久化数据也就是将内存中的数据写入到硬盘里面,大部分原因是为了之后重用数据(比如重启机器、机器故障之后恢复数据),或者是为了防止系统故障而将数据备份到一个远程位置

Redis的一种持久化方式叫快照(snapshotting,RDB),另一种方式是只追加文件(append-only file,AOF)

  • 快照(snapshotting,RDB)

    Redis可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis主从结构,主要用来提高Redis性能),还可以将快照留在原地以便重启服务器的时候使用。

    快照持久化是Redis默认采用的持久化方式,redis.conf配置文件中默认有此下配置:

    save 900 1 #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令创建快照。

    save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创建快照。

    save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创建快照。

  • AOF(append-only file)持久化

    与快照持久化相比,AOF持久化 的实时性更好,因此已成为主流的持久化方案。默认情况下Redis没有开启AOF(append only file)方式的持久化,可以通过appendonly参数开启:

    appendonly yes

    开启AOF持久化后每执行一条会更改Redis中的数据的命令,Redis就会将该命令写入硬盘中的AOF文件。AOF文件的保存位置和RDB文件的位置相同,都是通过dir参数设置的,默认的文件名是appendonly.aof

    在Redis的配置文件中存在三种不同的 AOF 持久化方式,它们分别是:

    appendfsync always #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
    appendfsync everysec #每秒钟同步一次,显示地将多个写命令同步到硬盘
    appendfsync no #让操作系统决定何时进行同步

    为了兼顾数据和写入性能,用户可以考虑 appendfsync everysec选项 ,让Redis每秒同步一次AOF文件,Redis性能几乎没受到任何影响。而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候,Redis还会优雅的放慢自己的速度以便适应硬盘的最大写入速度

Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 aof-use-rdb-preamble 开启)。

如果把混合持久化打开,AOF 重写的时候就直接把 RDB (快照)的内容写到 AOF(文件可追加) 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差

Redis事务

数据库普遍都有事务机制

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

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

与传统关系型数据库不同的是:

redis同一个事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚。

缓存雪崩

缓存雪崩:缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉

如何解决?或预防?

  • 事前:尽量保证整个 redis 集群的高可用性,发现机器宕机尽快补上。选择合适的内存淘汰策略
  • 事中:本地ehcache缓存 + hystrix限流或降级,避免MySQL崩掉
  • 事后:利用 redis 持久化机制保存的数据尽快恢复缓存

具体看下图

缓存穿透

大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。举个例子:某个黑客故意制造我们缓存中不存在的 key 发起大量请求,导致大量请求落到数据库

一般MySQL 默认的最大连接数在 150 左右,这个可以通过 show variables like '%max_connections%';命令来查看。最大连接数一个还只是一个指标,cpu,内存,磁盘,网络等无力条件都是其运行指标,这些指标都会限制其并发能力!所以,一般 3000 个并发请求就能打死大部分数据库了

解决方法:

最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等

  • 缓存无效 key :: 如果缓存和数据库都查不到某个 key 的数据就写一个到 redis 中去并设置过期时间,这种方式可以解决请求的 key 相对稳定的情况,当然,如果黑客恶意攻击,每次构建不同的请求key,会导致 redis 中缓存大量无效的 key ,很明显,这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点

  • 布隆过滤器Bloom Filter:

    布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在与海量数据中。

    把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,我会先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程

    布隆过滤器原理(选看)

    #位数组:https://www.cnblogs.com/silverdark/p/11181492.html

    #基于位数组的布隆过滤器:https://github.com/Snailclimb/JavaGuide/blob/master/docs/dataStructures-algorithms/data-structure/bloom-filter.md

Redis并发竞争Key

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

分布式锁(zookeeper 和 redis 都可以实现分布式锁)可以解决这个问题,如果不存在 Redis 的并发竞争 Key 问题,不要使用分布式锁,这样会影响性能

zookeeper 的分布式锁相比Redis更为可靠,推荐

缓存与数据库不同步问题

只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么如何解决一致性问题?

读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况

具体请参考:

https://blog.csdn.net/Thousa_Ho/article/details/78900563

串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求

发布了182 篇原创文章 · 获赞 121 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/JunSIrhl/article/details/105672945