1、简单介绍一下Redis
Redis是一个使用C语言开发的非关系型数据库,不过与其他数据库不同的是,它直接存在于内存当中的,读写非常快美因茨Redis被广泛应用于缓存方向。
作用:
除了做缓存之外,常用于做分布式锁,甚至消息队列
优势:
除了支持多种数据类型的键值对之外,还支持事务、持久化,以及lua脚本语言以及多种集群方案。
2、关系型数据库和非关系型数据库的区别
非关系型数据库:
- 性能:关系型数据库是基于二维表的,而非关系型数据库是基于键值对的,不需要经过SQL层的解析,性能十分高。
- 可扩展性:数据之间没有耦合性,所以非常容易水平扩展
关系型数据库: - 复杂查询:关系型数据库可以在一个表或者多个表之间执行十分复杂的查询
- 事务支持:是的对于安全性能很高的数据访问要求得以实现。redis虽然也支持事务,但是没有回滚等操作,只有操作队列,把要进行的操作放在一起,开始之后直接一步步执行,中间不会受到打扰。
3、分布式数据库的技术选型
分布式环迅的话,使用的比较多的主要有Memcached和Redis,现在基本没有看过还有项目使用 Memcached 来做缓存,都是直接用 Redis。
分布式缓存主要解决的是单机缓存的容量受服务器限制并且无法保存通用的信息。因为,本地缓存只在当前服务里有效,比如你部署了两个相同的服务,他们两者之间的缓存数据是无法共通的。
3.1 Redis和Memcached之间的异同:
共同点:
- 都是基于内存的数据库,一般都用来当做缓存使用。
- 都有过期策略。
- 两者的性能都非常高。
区别(相当于Redis的优势):
- Redis 支持更丰富的数据类型(支持更复杂的应用场景)。Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,棕色梯,hash等数据类型;Memcached 只支持最简单的 k/v 数据类型。
- Redis支持数据的持久化,可以将内存中的数据存储到磁盘中,在重启的时候可以拿出来使用;而 Memecache 把数据全部存在内存之中。
- 灾难恢复机制,和上面的持久化一样
- Redis的服务器内存用完之后,可以将数据放到磁盘上。但是Memcached 在服务器内存使用完之后,就会直接报异常。
- 但是 Redis 目前是原生支持 cluster 模式的
- Memcached 是多线程,非阻塞 IO 复用的网络模型;Redis 多是使用单线程的多路 IO 复用模型。
- Redis支持发布订阅模型、Lua 脚本、事务等功能,Memcached不支持
- Memcached过期数据的删除策略只有惰性删除,而Redis支持定期删除和惰性删除。
这里解释一下删除的方式:
定时删除:创建一个定时器,当key设置有过期时间,且过期时间到达时,由定时器任务立即执行对键的删除操作
优点:节约内存,到时就删除,快速释放掉不必要的内存占用
缺点:CPU压力大,无论CPU此时负载量多高,均占用CPU,会影响redis服务器响应时间和指令吞吐量
总结:拿时间换空间
惰性删除:数据到达过期时间,不做处理。等下次访问该数据时,如果未过期,返回数据 ;发现已过期,删除,返回不存在。
优点:节约CPU性能,发现必须删除的时候才删除
缺点:内存压力很大,出现长期占用内存的数据
总结:拿空间换时间
定期删除:周期性轮询redis库中的时效性数据,采用随机抽取的策略,利用过期数据占比的方式控制删除频度
优点:
- CPU性能占用设置有峰值,检测频度可自定义设置
- 内存压力不是很大,长期占用内存的冷数据会被持续清理
总结:周期性抽查存储空间 (随机抽查,重点抽查)
https://blog.csdn.net/weixin_43230682/article/details/107670911
4、缓存数据的处理流程
- 如果用户请求的数据在缓存中就直接返回。
- 缓存中不存在的话就看数据库中是否存在。
- 数据库中存在的话就更新缓存中的数据。
- 数据库中不存在的话就返回空数据。
当然在Redis中有更加复杂的三种读写策略:
4.1 Cache Aside Pattern 旁路缓存模式
Cache Aside Pattern 是我们平时使用比较多的一个缓存读写模式,比较适合读请求比较多的场景。
下面我们来看一下这个策略模式下的缓存读写步骤。
写:
- 先更新 DB
- 然后直接删除 cache
读: - 从 cache 中读取数据,读取到就直接返回
- cache中读取不到的话,就从 DB 中读取数据返回
- 再把数据放到 cache 中。
所谓的读写模式,主要是在读的时候什么时候去更新cache以及什么时候返回数据,在写的时候什么时候更新DB,什么时候删除cache,他们之间的顺序是什么。
在这里一定要记住在写的时候是先更新数据库再删除缓存,不然会数据不一致性的问题,如果顺序搞反的话:
请求1先把cache中的A数据删除 -> 请求2只能从DB中读取数据->请求1再把DB中的A数据更新。
但即使顺序搞反的话也可能会出现数据不一致的问题,但是概率很小,因为缓存的写入速度是比数据库的写入速度快很多的。
这个模式有着自己的缺陷主要如下:
缺陷1:首次请求数据一定不在 cache 的问题
解决办法:可以将热点数据可以提前放入cache 中。
缺陷2:写操作比较频繁的话导致cache中的数据会被频繁被删除,这样会影响缓存命中率 。
解决办法:
- 数据库和缓存数据强一致场景 :更新DB的时候同样更新cache,不过我们需要加一个锁/分布式锁来保证更新cache的时候不存在线程安全问题。
- 可以短暂地允许数据库和缓存数据不一致的场景 :更新DB的时候同样更新cache,但是给缓存加一个比较短的过期时间,这样的话就可以保证即使数据不一致的话影响也比较小。
4.2 Read/Write Through Pattern(读写穿透)
Read/Write Through Pattern 中服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。cache 服务负责将此数据读取和写入 DB,从而减轻了应用程序的职责。
这种方式使用的比较少,因为我们经常使用的分布式缓存Redis并没有提供cache将数据写入DB的功能。
写:
- 先查 cache,cache 中不存在,直接更新 DB。
- cache 中存在,则先更新 cache,然后 cache 服务自己更新 DB(同步更新 cache 和 DB)。
读: - 从 cache 中读取数据,读取到就直接返回 。
- 读取不到的话,先从 DB 加载,写入到 cache 后返回响应。
4.3 Write Behind Pattern(异步缓存写入)
两个又有很大的不同:Read/Write Through 是同步更新 cache 和 DB,而 Write Behind Caching 则是只更新缓存,不直接更新 DB,而是改为异步批量的方式来更新 DB。
很明显,这种方式对数据一致性带来了更大的挑战,比如cache数据可能还没异步更新DB的话,cache服务可能就就挂掉了。
这种策略在我们平时开发过程中也非常非常少见,但是不代表它的应用场景少,比如消息队列中消息的异步写入磁盘、MySQL 的 InnoDB Buffer Pool 机制都用到了这种策略。
Write Behind Pattern 下 DB 的写性能非常高,非常适合一些数据经常变化又对数据一致性要求没那么高的场景,比如浏览量、点赞量。
5、为何要使用缓存?
高性能:
对照上面 👆 我画的图。我们设想这样的场景:
假如用户第一次访问数据库中的某些数据的话,这个过程是比较慢,毕竟是从硬盘中读取的。但是,如果说,用户访问的数据属于高频数据并且不会经常改变的话,那么我们就可以很放心地将该用户访问的数据存在缓存中。
这样有什么好处呢? 那就是保证用户下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。
不过,要保持数据库和缓存中的数据的一致性。 如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!
高并发:
一般像 MySQL 这类的数据库的 QPS 大概都在 1w 左右(4 核 8g) ,但是使用 Redis 缓存之后很容易达到 10w+,甚至最高能达到 30w+(就单机 redis 的情况,redis 集群的话会更高)。
QPS(Query Per Second):服务器每秒可以执行的查询次数;
所以,直接操作缓存能够承受的数据库请求数量是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。进而,我们也就提高的系统整体的并发。
6、redis中常见的数据结构及使用场景
7、Redis单线程模型详解
Redis基于Reactor模式来设计开发了自己的一台高效的事件处理模型,这套事件处理模型对应的是 Redis 中的文件事件处理器(file event handler)。由于文件事件处理器(file event handler)是单线程方式运行的,所以我们一般都说 Redis 是单线程模型。
如何通过单线程来进行大量的客户端连接的监听:
通过IO 多路复用程序 来监听来自客户端的大量连接(或者说是监听多个 socket),它会将感兴趣的事件及类型(读、写)注册到内核中并监听每个事件是否发生。
这样的好处非常明显: I/O 多路复用技术的使用让 Redis 不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗(和 NIO 中的 Selector 组件很像)。
另外, Redis 服务器是一个事件驱动程序,服务器需要处理两类事件: 1. 文件事件; 2. 时间事件。
时间事件不需要多花时间了解,我们接触最多的还是 文件事件(客户端进行读取写入等操作,涉及一系列网络通信)。
Redis 基于 Reactor 模式开发了自己的网络事件处理器:这个处理器被称为文件事件处理器(file event handler)。文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字,并使用文件事件分配器根据 套接字目前执行的任务来为套接字关联不同的事件处理器。
当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关 闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件事件分配器就会调用套接字之前关联好的事件处理器来处理这些事件。
虽然文件事件处理器以单线程方式运行,但通过使用 I/O 多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接,这保持了 Redis 内部单线程设计的简单性。
四个部分需要注意:
- 套接字
- IO多路复用程序
- 文件事件分配器
- 事件处理器
6.0之前为什么不用多线程?
- 单线程编程容易并且更容易维护;
- Redis 的性能瓶颈不再 CPU ,主要在内存和网络;
- 多线程就会存在死锁、线程上下文切换等问题,甚至会影响性能。
6.0之后为什么使用多线程?
Redis6.0 引入多线程主要是为了提高网络 IO 读写性能,因为这个算是 Redis 中的一个性能瓶颈(Redis 的瓶颈主要受限于内存和网络)。
虽然,Redis6.0 引入了多线程,但是 Redis 的多线程只是在网络数据的读写这类耗时操作上使用了, 执行命令仍然是单线程顺序执行。因此,你也不需要担心线程安全问题。
8、为什么要设置过期时间?
- 很明显的一点是为了缓解内存的消耗,内存就那么大,如果一直保存的话,分分钟直接 Out of memory。
- 还有就是有一些应用场景,比如发送给手机的验证码短信,有效时间有的时候只有几分钟,几分钟之后就直接失效。
可以通过setx还有expire来设置过期时间。
如何设置判断数据是否过期?
Redis 通过一个叫做过期字典(可以看作是 hash 表)来保存数据过期的时间。过期字典的键指向 Redis 数据库中的某个 key(键),过期字典的值是一个 long long 类型的整数,这个整数保存了 key 所指向的数据库键的过期时间(毫秒精度的 UNIX 时间戳)。
9、Redis内存淘汰机制
所谓内存淘汰机制,是在redis大小有限的情况下装尽量多的热点数据
- volatile-lru(least recently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
- allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
- no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!
4.0 版本后增加以下两种:
volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰。
allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key。
10、Redis持久化机制(怎么保证Redis挂掉之后再重启数据可以进行恢复)
很多时候我们需要持久化数据也就是将内存中的数据写入到硬盘里面,大部分原因是为了之后重用数据(比如重启机器、机器故障之后恢复数据),或者是为了防止系统故障而将数据备份到一个远程位置。
Redis 不同于 Memcached 的很重要一点就是,Redis 支持持久化,而且支持两种不同的持久化操作。Redis 的一种持久化方式叫快照(snapshotting,RDB),另一种方式是只追加文件**(append-only file, AOF)**。这两种方法各有千秋,下面我会详细这两种持久化方法是什么,怎么用,如何选择适合自己的持久化方法。
快照(snapshotting)持久化(RDB)
Redis 可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用。
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)方式的持久化。
appendfsync always #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
appendfsync everysec #每秒钟同步一次,显示地将多个写命令同步到硬盘
appendfsync no #让操作系统决定何时进行同步
还有混合持久化机制与AOF重写,看不太懂
11、Redis的事务
Redis 可以通过 MULTI,EXEC,DISCARD
和 WATCH
等命令来实现事务(transaction)功能
Redis是不能全部满足关系型数据库的四大特性的。不符合原子性和持久性不支持回滚
12、缓存穿透
12.1 什么是缓存穿透?
缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。举个例子:某个黑客故意制造我们缓存中不存在的 key 发起大量请求,导致大量请求落到数据库。
12.2 解决方案
- 最直接的就是通过参数校验避免哪些不合法的输入传入服务层进行查询
- 如果缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去并设置过期时间,具体命令如下: SET key value EX 10086,尽量将无效的 key 的过期时间设置短一点比如 1 分钟。
- 布隆过滤器:
布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在于海量数据中。我们需要的就是判断 key 是否合法,有没有感觉布隆过滤器就是我们想要找的那个“人”。
具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。
布隆滤波器的原理:
https://github.com/Snailclimb/JavaGuide/blob/master/docs/dataStructures-algorithms/data-structure/bloom-filter.md
基本上就是将布隆滤波器就像一个很长很长的数组,里面的元素之后0和1,我们有几个hash函数,通过hash函数计算出对应的hash值。就是数组的下标,如果所有哈希函数对应的下标中的值为1的话就说明这个数一定是在数据库中的。
13、缓存雪崩
我发现缓存雪崩这名字起的有点意思,哈哈。
实际上,缓存雪崩描述的就是这样一个简单的场景:缓存在同一时间大面积的失效,后面的请求都直接落到了数据库上,造成数据库短时间内承受大量请求。 这就好比雪崩一样,摧枯拉朽之势,数据库的压力可想而知,可能直接就被这么多请求弄宕机了。
第一种场景:系统的缓存模块出了问题比如宕机导致不可用。造成系统的所有访问,都要走数据库。
第二种场景:有一些被大量访问数据(热点缓存)在某一时刻大面积失效,导致对应的请求直接落到了数据库上。
举个例子 :秒杀开始 12 个小时之前,我们统一存放了一批商品到 Redis 中,设置的缓存过期时间也是 12 个小时,那么秒杀开始的时候,这些秒杀的商品的访问直接就失效了。导致的情况就是,相应的请求直接就落到了数据库上,就像雪崩一样可怕。
解决方案:
针对Redis服务不可用的情况:
- 采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。限流,避免同时处理大量的请求。
- 限流,避免同时处理大量的请求。
针对热点缓存失效的情况:
- 设置不同的失效时间比如随机设置缓存的失效时间。
- 缓存永不失效。
14、如何保证缓存和数据库数据的一致性?
针对旁路缓存模式:
如果更新数据库成功,而删除缓存这一步失败的情况的话,简单说两个解决方案:
缓存失效时间变短(不推荐,治标不治本) :我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操作缓存后操作数据库的场景不适用。
增加 cache 更新重试机制(常用): 如果 cache 服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。如果多次重试还是失败的话,我们可以把当前更新失败的 key 存入队列中,等缓存服务可用之后,再将 缓存中对应的 key 删除即可。