缓存的基本思想
我们为了避免用户在请求数据的时候获取速度过于缓慢,所以我们在数据库之上增加了缓存这一层来弥补。
使用缓存为系统带来了什么问题
- 系统复杂性增加:引入缓存之后,需要维护缓存和数据库的数据一致性、维护热点缓存等
- 系统开发成本增加
本地缓存解决方案
JDK 自带的 HashMap
和 ConcurrentHashMap
ConcurrentHashMap
可以看作是线程安全版本的 HashMap
,两者都是存放 key/value 形式的键值对。但是,大部分场景来说不会使用这两者当做缓存,因为只提供了缓存的功能,并没有提供其他诸如过期时间之类的功能。一个稍微完善一点的缓存框架至少要提供:过期时间、淘汰机制、命中率统计这三点。
Ehcache
、 Guava Cache
、 Spring Cache
这三者是使用的比较多的本地缓存框架
Ehcache
的话相比于其他两者更加重量。不过,相比于 Guava Cache
、 Spring Cache
来说, Ehcache
支持可以嵌入到 hibernate 和 mybatis 作为多级缓存,并且可以将缓存的数据持久化到本地磁盘中、同时也提供了集群方案(比较鸡肋,可忽略)。
Guava Cache
和 Spring Cache
两者的话比较像。
Guava
相比于 Spring Cache
的话使用的更多一点,它提供了 API 非常方便我们使用,同时也提供了设置缓存有效时间等功能。它的内部实现也比较干净,很多地方都和 ConcurrentHashMap
的思想有异曲同工之妙。
使用 Spring Cache
的注解实现缓存的话,代码会看着很干净和优雅,但是很容易出现问题比如缓存穿透、内存溢出。
后起之秀 Caffeine
相比于 Guava
来说 Caffeine
在各个方面比如性能要更加优秀,一般建议使用其来替代 Guava
。并且, Guava
和 Caffeine
的使用方式很像!本地缓存固然好,但是缺陷也很明显,比如多个相同服务之间的本地缓存的数据无法共享。
为什么要有分布式缓存?/为什么不直接用本地缓存?
- 本地缓存对分布式架构支持不友好,比如同一个相同的服务部署在多台机器上的时候,各个服务之间的缓存是无法共享的,因为本地缓存只在当前机器上有。
- 本地缓存容量受服务部署所在的机器限制明显。 如果当前系统服务所耗费的内存多,那么本地缓存可用的容量就很少。
Redis基本介绍
Redis是一个使用C语言开发的内存数据库,Redis除了做缓存之外,Redis也经常用来做分布式锁,消息队列。
Redis提供了多种数据类型来支持不同的业务场景。Redis还支持事务、持久化、Lua脚本、多种集群方案。
分布式缓存常见的技术选型方案有哪些?
Redis或者Memcached
Redis 和 Memcached 的区别和共同点
- Redis支持更丰富的数据类型:Redis支持五种数据类型,Memcached只支持最简单的k/v数据类型
- Redis支持数据的持久化
- Redis有灾难恢复机制
- Memcached没有原生的集群模式,需要依赖客户端来实现往集群中分片写入数据
- Memcached是多线程,非阻塞IO复用的网络模型;Redis使用单线程的多路IO复用模型
- Redis支持发布订阅模式、Lua脚本、事务等功能,而Memcached不支持,并且Redis支持更多的编程语言
- Memcached过期数据的删除策略只用了惰性删除,而Redis同时使用了惰性删除与定期删除
缓存数据的处理流程是怎样的?
- 如果用户请求的数据在缓存中就直接返回
- 缓存中不存在就看数据库中是否存在
- 数据库中存在的话就更新缓存中的数据
- 数据库中不存在就返回空的数据
为什么要用缓存?
- 提高系统响应速度
- 提高系统承载并发量
Redis数据结构
- string 一般用于需要计数的场景,比如用户的访问次数、热点文章的点赞转发数量等
- list 发布与订阅或者慢查询
- hash 系统中对象的存储
- set 不能重复的数据
- zset 根据某个权重进行排序的场景
Redis单线程模型
文件事件处理器
-
Redis基于Reactor模式开发了文件事件处理器。这个处理器是单线程的,所以Redis才叫单线程的模型。文件事件处理器采用了IO多路复用机制同时监听多个socket,根据socket上的事件选择对应的事件处理器来进行处理
-
如果被监听的scoket准备好执行accept、read、write、close等事件/操作的时候,跟事件/操作对应的文件时间就会产生,这个时候文件事件处理器就会调用之前关联好的事件处理器来处理这个事件
-
文件事件处理器是单线程模式运行的,但是通过IO多路复用机制监听多个socket,可以实现高性能的网络通信模型,又可以跟内部其他单线程的模块进行对接,保证了Redis内部的线程模型的简单性
-
文件事件处理器包含4个部分
多个socket
IO多路复用程序
文件事件分派器
事件处理器(命令请求处理器,命令回复处理器,连接应答处理器等等)
-
多个socket可能并发的产生不同的操作,每个操作对应不同的文件,但是IO多路复用会监听多个socket,会将socket放入到一个队列中排队,然后每次从队列中取出一个socket给事件分派器,事件分派器再把socket分派给对应的事件处理器去处理
-
当一个socket的事件被处理完之后,IO多路复用程序才会将队列中的下一个socket取出交给事件分派器,文件事件分派器再根据socket当前产生的事件来选择对应的事件处理器来处理
常用的文件事件处理器
- 如果是客户端要连接Redis,那么会为socket关联连接应答处理器
- 如果是客户端要写数据到Redis,那么会为socket关联命令请求处理器
- 如果是客户端要从Redis中读取数据(Redis发送数据给客户端),那么会为socket关联命令回复处理器
客户端与Redis通信的一次流程
- 在Redis启动及初始化的时候,Redis会(预先)将连接应答处理器跟“AE_READABLE”事件关联起来,接着如果一个客户端向Redis发起连接,此时就会产生一个“AE_READABLE”事件,然后由连接应答处理器来处理跟客户端建立连接,创建客户端对应的socket,同时将这个socket的“AE_READABLE”事件跟命令请求处理器关联起来;
- 当客户端向Redis发起请求的时候(不管是读请求还是写请求),首先就会在之前创建的客户端对应的socket产生一个“AE_READABLE”事件,然后IO多路复用程序会监听到在之前创建的客户端对应的socket上产生了一个“AE_READABLE”事件,接着把这个socket放入到一个队列中排队,然后由文件事件分派器从队列中获取socket交给对应的命令请求处理器来处理(因为之前在Redis启动并进行初始化的时候就已经预先将“AE——READABLE”事件跟命令请求处理器关联起来了),之后命令请求处理器就会从之前创建的客户端对应的socket中读取请求相关的数据,然后在自己的内存中进行执行和处理;
- 当客户端请求处理完成,Redis这边也准备好了给客户端的响应数据之后,就会(预先)将socket的“AE_WRITABLE”事件跟命令回复处理器关联起来,当客户端这边准备好读取响应数据时,就会在之前创建的客户端对应的socket上产生一个“AE_WRITABLE”事件,然后IO多路复用程序会监听到在之前创建的客户端对应的socket上产生了一个“AE_WRITABLE”事件,接着把这个socket放入到一个队列中进行排队,然后由文件时间分派器从队列中获取socket交给对应的命令回复处理器来处理(因为之前在Redis这边准备好给客户端的响应数据之后就已经预先将“AE_WRITABLE”时间跟命令回复处理器关联起来了),之后命令回复处理器就会向之前创建的客户端对应的socket输出/写入准备好的响应数据,最终返回给客户端,供客户端来读取;
- 当命令回复处理器将准备好的响应数据写完之后,就会删除之前创建的客户端对应的socket上的“AE_WRITABLE”事件和命令回复处理器的关联关系
为什么Redis单线程模型也能效率这么高?
- 纯内存操作;
- 核心是基于非阻塞的IO多路复用机制;
- 单线程同时也避免了多线程的上下文频繁切换问题,预防了多线程可能产生的竞争问题。
Redis6.0之后为何引入了多线程?
Redis6.0引入了多线程主要是为了提高网络IO读写性能,因为这个算是Redis中的一个性能瓶颈(Redis的瓶颈主要受限于内存和网络)。Redis的多线程只是在网络数据的读写这类耗时操作上使用了,执行命令仍然是单线程顺序执行,因此不用担心线程安全问题。
Redis过期数据删除策略
Redis采用的是定期删除+惰性删除
惰性删除
只会在取出key的时候才会对数据进行过期检查,这样对CPU最友好,但是可能会造成太多过期的key没有被删除
定期删除
每隔一段时间抽取一批key执行删除过期key操作,并且,Redis底层会通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响
Redis内存淘汰机制
Redis提供了6种数据淘汰策略:
-
volatile-lru:从已设置过期时间的数据及中挑选最近最少使用的数据淘汰
-
volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰
-
volatile-random:从已设置过期时间的数据集中任意选择数据淘汰
-
allkeys-lru:当内存不足以容纳写入新数据时,淘汰最近最少使用的key
-
allkeys-random:当内存不足以写入新数据时,随机选择key进行淘汰
-
no-eviction:禁止驱逐数据,就是内存不足时不允许写
4.0版本后增加了以下两种:
-
volatile-lfu:从已设置过期时间的数据集中移除最不经常使用的key
-
allkeys-lfu:当内存不足写入新数据时,移除最不经常使用的key
Redis持久化机制
快照(snapshotting,RDB)
可以通过创建快照来获得存储在内存里面的数据在某个时间节点上的副本。Redis创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis主从结构,主要用来提高Redis性能),还可以将快照留在原地以便重启服务器的时候使用。
快照持久化是Redis默认采用的持久化方式,在Redis.conf配置文件中默认有此下配置:
save 900 1 #在900秒之后,如果至少有一个key发生变化,Redis就会自动触发BGSAVE命令创建快照
save 300 10 #在300秒之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创建快照
save 60 10000 #在60秒之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创建快照
优点
- 对Redis对外提供的读写服务,影响非常小,可以让Redis保持高性能
- 相对于AOF持久化机制来说,直接基于RDB数据文件来重启和恢复Redis进程,更加快速
缺点
- 相对于AOF,实时性较差
追加文件(append-only file,AOF)
与快照持久化相比,AOF持久化的实时性更好,因此成为主流的持久化方案。默认情况下Redis没有开启AOF方式的持久化,可以通过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还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。
优点
1.实时性更好
缺点
- 由于AOF一般每秒都在存储,对于相同数量的数据集而言,AOF文件通常要大于RDB。
- 恢复大数据集时速度要比RDB慢
- 对于读写性能影响更大
Redis4.0开始支持RDB和AOF的混合持久化(默认关闭,可以通过配置项aof-use-rdb-preamble开启)。
如果把混合持久化打开,AOF重写的时候就会直接把RDB的内容写到AOF文件开头。这样做的好处是可以结合RDB和AOF的优点,快速加载同时避免丢失过多的数据。当然缺点也是有的,AOF里面的RDB部分是压缩格式不再是AOF格式,可读性较差。
AOF重写
AOF持久化是通过保存被执行的写命令来记录数据库状态的,所以AOF文件的大小会越来越大,为了解决AOF文件体积膨胀的问题,Redis提供了AOF重写功能。
AOF重写可以产生一个新的AOF文件,这个新的AOF文件和原有的AOF文件所保存的数据库状态一样,但是新的AOF文件不会包含任何浪费空间的冗余命令,通常体积较就得AOF文件小很多。
AOF重写是通过读取数据库中的键值对来实现的,程序无须对现有的AOF文件进行任何读入、分析或者写入操作。
在执行BGREWRITEAOF命令时,Redis服务器会维护一个AOF重写缓冲区,该缓冲区会在子线程创建新AOF文件期间,记录服务器执行的所有写命令。当子进程完成创建新AOF文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新AOF文件的末尾,使得新旧两个AOF文件所保存的数据库状态一直。最后,服务器用新的AOF文件替换就得AOF文件,一次来完成AOF文件重写操作。
Redis事务
Redis可以通过MULTI,EXEC,DISCARD和WATCH命令来实现事务功能。
使用MULTI命令后可以输入多个命令。Redis不会立即执行这些命令,而是将它们放到队列,当调用了EXEC命令将会执行所有命令。
Redis事务是不支持回滚的,因而不满足原子性和持久性。(Redis开发者们觉得没有必要支持回滚,这样更简单便捷并且性能更好。Redis开发者觉得即使命令执行错误,也应该在开发过程中被发现,而不是生产过程中)。
可以将Redis中的事务理解为:Redis事务提供了一种将多个命令请求打包的功能。然后,再按照顺序执行打包的所有命令,并且不会被中途打断。
缓存穿透
什么是缓存穿透
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,这时的用户很可能是攻击者,会导致数据库压力过大。
有哪些解决方法
- 做好参数校验,不合法的请求直接抛出异常信息给客户端
- 缓存无效key,尽量将无效的key的过期时间设置短一点,比如一分钟
- 布隆过滤器
什么是布隆过滤器
布隆过滤器是一种比较巧妙的概率型数据结构,它可以告诉你某种东西一定不存在或者可能存在。
布隆过滤器相对于Set、Map等数据结构来说,它可以更高效的插入和查询,并且占用空间更少,它也有缺点,就是判断某种东西是否存在时,可能会误判。但是只要参数设置的合理,它的精确度也可以控制的相对精确,只会有小小的误判概率。
Redis中的布隆过滤器
之前的布隆过滤器可以使用Redis的位图操作实现,直到Redis4.0版本提供了插件功能,Redis官方提供的布隆过滤器才正式登场。布隆过滤器作为一个插件加载到Redis Server中,就会给Redis提供了强大的布隆去重功能。
布隆过滤器的基本使用
在Redis中,布隆过滤器有两个基本命令,分别是:
bf.add
:添加一个元素到布隆过滤器中
bf.madd
:添加多个元素到布隆过滤器中
bf.exists
:判断某个元素是否在过滤器中
bf.mexists
:判断多个元素是否在过滤器中
布隆过滤器的高级使用
Redis还提供了自定义参数的布隆过滤器,想要尽量减少布隆过滤器的误判,就要设置合理的参数。在使用bf.add
添加元素之前,使用bf.reserve
创建一个自定义的布隆过滤器。bf.reserve
命令有三个参数,分别是:
key
:键
error_rate
:期望错误率,期望错误率越低,需要的空间就越大
capacity
:初始容量,当实际元素的数量超过这个初始化容量时,误判率上升。如果对应的key已经存在时,在执行bf.reserve
命令就会报错。如果不使用bf.reserve
命令创建,而是使用Redis自动创建的布隆过滤器,默认的error_rate
是 0.01,capacity
是 100。
布隆过滤器的error_rate
越小,需要的存储空间就越大,对于不需要过于精确的场景,error_rate
设置稍大一点也可以。布隆过滤器的capacity
设置的过大,会浪费存储空间,设置的过小,就会影响准确率,所以在使用之前一定要尽可能地精确估计好元素数量,还需要加上一定的冗余空间以避免实际元素可能会意外高出设置值很多。总之,error_rate
和 capacity
都需要设置一个合适的数值。
布隆过滤器的原理简介
Redis中布隆过滤器的数据结构就是一个很大的数组和几个不一样的无偏哈希函数(能把元素的哈希值算的比较平均,能让元素被哈希到位数组中的位置比较随机)。
向布隆过滤器中添加 元素时,会使用多个无偏哈希函数对元素进行哈希,算出一个整数索引值,然后对位数组长度进行取模运算得到一个位置,每个无偏哈希函数都会得到一个不同的位置。再把位数组的这几个位置都设置为1,这就完成了bf.add
命令的操作。
向布隆过滤器查询元素是否存在时,和添加元素一样,也会把哈希的几个位置算出来,然后看看位数组中对应的几个位置是否都为1,只要有一个位位0,那么就说明布隆过滤器里不存在这个元素。如果这几个位置都为1,并不能完全说明这个元素就一定存在其中,有可能这些位置为1是因为其他元素的存在,这就是布隆过滤器会出现误判的原因。
缓存击穿
缓存击穿是指缓存中没有但是数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库中取数据,引起数据库压力瞬间增大。
解决方案
- 设置热点数据永不过期
- 加互斥锁
缓存雪崩
缓存在同一事件大面积的失效,导致请求都落到了数据库上。
解决方案
- 缓存数据的过期时间设置随机,防止同一时间出现大量数据过期现象
- 设置热点数据永不过期
- 使用Redis集群