# 缓存之 redis
# 简介
Redis 是一个基于 Key-Value 存储结构的开源内存数据库,也是一种 NoSQL 数据库。
它支持多种数据类型,包括 String、Map、Set、ZSet 和 List,以满足不同应用场景的需求。
Redist 以内存存储和优化的数据结构为基础,提供了快速的读写性能和高效的数据访问。常被用作应用与数据库之间的缓存中间件,提升数据 IО效率此外,Redis 支持主从复制、哨兵机制和集群方式,实现高可用性和水平扩展。
总而言之,Redis 是一款功能强大、灵活且可靠的数据库解决方案,适用于各种企业级应用开发场景。
# 为什么 redis 是单线程的,为什么是单线程的也能做到很快呢?
Redis 的全部操作是 基于内存
的,因此 Redis 瓶颈可能是机器的内存或者网络带宽,而并非 CPU。
Redis 因为是单线程的,所以不会出现多线程之间上下文频繁切换并发问题导致死锁。
Redis 是单线程的(6 以前的版本)也就是单线程执行命令,当多个用户同时执行 SET name1 value1
操作时,可能会出现以下情况:
- Redis 是单线程的,每个命令都是按顺序执行的。当多个用户同时执行
SET name1 value1
操作时,Redis 会依次处理每个请求,一个接一个地执行。 - 如果多个用户同时执行
SET name1 value1
操作,由于 Redis 是单线程的,每个请求会依次被处理,不会出现并发写入的情况。每个请求都会被依次执行,保证了数据的一致性。 - 在 Redis 中,如果多个用户同时执行
SET name1 value1
操作,Redis 会根据请求的先后顺序依次处理,后面的请求会等待前面的请求执行完成后再执行。这样可以保证数据的正确性和一致性。
可能在这里会疑惑,单线程为什么还会快呢,这是因为 redis 通过 事件驱动的模型
和 内部的优化
机制,使得 redis 能够高效处理多个并发请求,保证数据的一致性和正确性。
# 事件驱动的模型:
Redis 使用了事件驱动的模型来处理客户端请求和网络 I/O 操作。它基于事件循环机制,通过监听和处理事件来实现高效的并发处理。
比如我执行了 SET name1 value1
命令
- 客户端发送
SET name1 value1
命令到 Redis 服务器。 - Redis 服务器接收到这个命令后,会将其解析并生成对应的事件。
- 生成的事件会被放入事件队列中等待处理。
- Redis 服务器的事件循环会不断地从事件队列中取出事件进行处理。
- 当事件循环处理到
SET name1 value1
这个事件时,会执行相应的操作,即将键名为name1
,值为value1
的键值对存储到内存中。 - 如果设置成功,Redis 会向客户端发送一个响应,通知客户端操作已成功完成。
注意:如果是多个同时执行 SET name1 value1
那么 redis 是依次执行的。
# 内部优化:
数据结构的优化(比如跳跃表)、内存管理器的优化、命令的优化(通过某个事件让命令批量处理之类的)、网络的优化
这里只针对跳跃表查询简单说明一下:
跳跃表是有序集合(zSet)数据类型的实现之一
看到 跳跃
这个词就可以联想到高度、层级,redis 的跳跃表就是一个层级,有最高度的,也就是有多层级的有序链表,那么具体是怎么做到查询快的呢?下面举个例子
首先看普通的链表(只有一层的情况)
2 -> 5 -> 8 -> 15 -> 18 -> 25 -> 38 -> 45 -> null
如果要查询 18 这个节点,那么时间复杂度就是 O (N) 那么怎么处理查询才会更快呢,那就是在某个节点多加一个指针指向下下一个
5 ->-> -> 15 -> -> -> 25 -> -> -> 45 -> null (第一层)
2 -> 5 -> 8 -> 15 -> 18 -> 25 -> 38 -> 45 -> null (第二层)
这个时候我们要查 18,是怎么查的呢
1、首先和 5 比较,18 大于 5,继续往后查找
2、在和 15 比较,18 大于 15,在继续往后找
3、发现 15 的下一个指针指向 25,比 18 大,这时候向下层查找
4、和 18 比较,等于,这就找到了
只需要和 5、15、25 比较
继续在加一层
15 -> -> -> -> -> -> -> 45 -> null (第一层)
5 ->-> -> 15 -> -> -> 25 -> -> -> 45 -> null (第一层)
2 -> 5 -> 8 -> 15 -> 18 -> 25 -> 38 -> 45 -> null (第二层)
这个时候我们要查 18,是怎么查的呢
1、先和 15 比较,18 大于 15,继续往后查找
2、此时下一个节点指向 null,向下查找
3、此时下一个节点指向 15,比 18 小,继续向下查找
4、此时的节点就指向了 18,找到了
只需要和 15、15、25 比较
# 数据结构
# String
字符串 string 是 Redis 最简单的数据结构。Redis 所有的数据结构都是以唯一的 key 字符串作为名称,然后通过这个唯一 key 值来获取相应的 value 数据。不同类型的数据结构的差异就在于 value 的结构不一样。Redis 的 string 可以包含任何数据,比如 jpg 图片或者序列化的对象 (java 中对象序列化函数 serialize)。
String 采用预分配冗余空间的方式来减少内存的频繁分配,内部为当前字符串实际分配的空间 capacity 一般要高于实际字符串长度 len。当字符串长度小于 1M 时,扩容都是加倍现有的空间,如果超过 1M,扩容时一次只会多扩 1M 的空间。需要注意的是字符串最大长度为 512M。
字符串是由多个字节组成,每个字节又是由 8 个 bit 组成,如此便可以将一个字符串看成很多 bit 的组合,这便是 bitmap「位图」数据结构。
例子:
#set key1 value1 | |
> set key1 value1 | |
OK | |
#获取 | |
> get key1 | |
value1 | |
#删除 | |
> DEL key1 | |
1 | |
#再次获取为空 | |
> get key1 | |
null |
# List
Redis 的列表相当于 Java 语言里面的 LinkedList,注意它是链表而不是数组,而且是双向链表。这意味着 list 的插入和删除操作非常快,时间复杂度为 O (1),但是索引定位很慢,时间复杂度为 O (n),这点让人非常意外。
当列表弹出了最后一个元素之后,该数据结构自动被删除,内存被回收。
Redis 的列表结构常用来做异步队列使用。将需要延后处理的任务结构体序列化成字符串塞进 Redis 的列表,另一个线程从这个列表中轮询数据进行处理。
例子:
#向 listKey 这个 key 添加 5 个值 | |
> LPUSH listKey aa bb cc dd ee | |
5 | |
#列出 listKey 从 0 到第十个元素 | |
> LRANGE listKey 0 10 | |
ee | |
dd | |
cc | |
bb | |
aa | |
#弹出头部元素 | |
> LPOP listKey | |
ee | |
#弹出尾部原始 | |
> RPOP listKey | |
aa |
# Hash
hash 的底层存储有两种数据结构
**ziplist:** 如果 hash 对象保存的键和值字符串长度都小于 64 字节且 hash 对象保存的键值对数量小于 512,则采用这种。
dict(字典):其他情况采用这种数据结构。
hash 结构也可以用来存储用户信息,不同于字符串一次性需要全部序列化整个对象,hash 以对用户结构中的每个字段单独存储。这样当我们需要获取用户信息时可以进行部分获取。而以整个符串的形式去保存用户信息的话就只能一次性全部读取,这样就会比较浪费网络流量。(比如一个用户的信息如果用用 string 存储,那么结构就是 username, {"afield":"avalue", "bfield":"bvalue, ...} 这样所有的属性都会保存起来,但是如果用 hash 就是: username, ["afield"="value", ..] 我这里可以只保存重要的信息大大减少了网络传输的数据量)
hash 也有缺点,hash 结构的存储消耗要高于单个字符串,到底该使用 hash 还是字符串,需要根据实际情况再三权衡。
例子:
存储 | |
> HMSET keyMap akey avalue bkey bvalue | |
OK | |
#获取存储在哈希表中指定字段的值。 | |
> HGET keyMap akey | |
avalue |
# Set
Set 是一个无序的、自动去重的集合数据类型(唯一的),Set 底层用两种数据结构存储。
**intset:** 如果元素个数少于默认值 512 且元素可以用整型,则用这种数据结构。
**dict(字典):** 其他情况采用这种数据结构。
当集合中最后一个元素移除之后,数据结构自动删除,内存被回收。 set 结构可以用来存储活动中奖的用户 ID,因为有去重功能,可以保证同一个用户不会中奖两次。
列如:
> SADD setKey aa | |
1 | |
> SADD setKey bb | |
1 | |
> SADD setKey cc | |
1 | |
> SMEMBERS setKey | |
aa | |
bb | |
cc | |
#当存在 aa 在添加会添加失败 | |
> SADD setKey aa | |
0 | |
> SMEMBERS setKey | |
aa | |
bb | |
cc |
# ZSet(有序集合)
zset 可能是 Redis 提供的最为特色的数据结构,它也是在面试中面试官最爱问的数据结构。zset 为有序(有限 score 排序,score 相同则元素字典排序),自动去重的集合数据类型,其底层实现为 字典(dict) + 跳表(skiplist),当数据比较少的时候用 ziplist 编码结构存储。
**ziplist :** 如果有序集合保存的所有元素的长度小于默认值 64 字节且有序集合保存的元素数量小于默认值 128 个,则采用这种数据结构
** 字典(dict) + 跳表(skiplist):** 其他情况采用这种数据结构。
例如:
ZADD key score member [score] [member] | |
> ZADD zsetKey1 1 aa | |
1 | |
> ZADD zsetKey1 2 bb | |
1 | |
> ZADD zsetKey1 3 cc | |
1 | |
> ZRANGE zsetKey1 0 10 WITHSCORES | |
aa | |
1 | |
bb | |
2 | |
cc | |
3 | |
#可以改变顺序 | |
> ZADD zsetKey1 1 cc | |
0 | |
> ZADD zsetKey1 1 dd | |
1 | |
> ZRANGE zsetKey1 0 10 WITHSCORES | |
aa | |
1 | |
cc | |
1 | |
dd | |
1 | |
bb | |
2 |
# 过期策略
Redis 所有的数据结构都可以设置过期时间,时间一到,就会自动删除。
在过期这件事上,Redis 非常小心。
由于执行清除任务的单线程的,那么如何一次性太多过期的就会出现处理不过来堵塞的情况
# 过期 key 集合
redis 会将每个设置了过期时间的 key 放入到一个独立的字典中,以后会定时遍历这个字典来删除到期的 key。除了定时遍历之外,它还会使用惰性策略来删除过期的 key,所谓惰性策略就是在客户端访问这个 key 的时候,redis 对 key 的过期时间进行检查,如果过期了就立即删除。定时删除是集中处理,惰性删除是零散处理。
例如:
#创建一个会过期的 key 过期时间 60 秒 | |
> SETEX exKey 60 exValue | |
OK | |
#查询指定 key 的剩余时间(秒) | |
> TTL exKey | |
33 | |
> TTL exKey | |
31 | |
> TTL exKey | |
30 | |
> TTL exKey | |
28 | |
> TTL exKey | |
25 | |
#获取后被清除了 | |
> TTL exKey | |
-2 | |
> get exKey | |
null |
# 定时扫描策略
Redis 默认会每秒进行十次过期扫描,过期扫描不会遍历过期字典中所有的 key,而是采用了一种简单的贪心策略。
从过期字典中随机 20 个 key;
删除这 20 个 key 中已经过期的 key;
如果过期的 key 比率超过 1/4,那就重复步骤 1;
同时,为了保证过期扫描不会出现循环过度,导致线程卡死现象,算法还增加了扫描时间的上限,默认不会超过 25ms。
设想一个大型的 Redis 实例中所有的 key 在同一时间过期了,会出现怎样的结果?
毫无疑问,Redis 会持续扫描过期字典 (循环多次),直到过期字典中过期的 key 变得稀疏,才会停止 (循环次数明显下降)。这就会导致这期间线上读写 QPS
下降明显。还有另外一种原因是内存管理器需要频繁回收内存页,这也会产生一定的 CPU 消耗。
这里解析一下,假如单台 Redis 读写请求 QPS 是 10w,也就是每个请求需要 0.00001s 来完成,每秒执行十次过期扫描,每次过期扫描都达到上限 25ms,那么每秒过期扫描总花费 0.25s,相当于 QPS 降低了 2.5W。
所以业务开发人员一定要注意过期时间,如果有大批量的 key 过期,要给过期时间设置一个随机范围,而不能全部在同一时间过期。
注:
QPS 是指每秒查询率(Queries Per Second),是衡量系统处理能力的重要指标之一。在计算机系统中,QPS 表示系统每秒能够处理的请求数量,通常用于评估系统的性能和负载能力。
# 从库的过期策略
从库不会进行过期扫描,从库对过期的处理是被动的。主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的 key。
因为指令同步是异步进行的,所以主库过期的 key 的 del 指令没有及时同步到从库的话,会出现主从数据的不一致,主库没有的数据在从库里还存在。
# Redis 内存淘汰机制
Redis 数据库可以通过配置文件来配置最大缓存,当写入的数据发现没有足够的内存可用的时候,Redis 会触发内存淘汰机制。Redis 为了满足多样化场景,提供了八种策略,可以在 redis.config 文件中配置。
volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰
volatile-random:从已设置过期时间的数据集中任意选择数据淘汰
volatile-lfu:从已设置过期时间的数据集中挑选使用频率最低的数据淘汰
allkeys-lru:从所有数据集中挑选最近最少使用的数据淘汰
allkeys-lfu:从所有数据集中挑选使用频率最低的数据淘汰
allkeys-random:从所有数据集中任意选择数据淘汰
no-enviction:不回收任何数据,返回一个写操作的错误信息。这也是默认策略。
特别说明:
LRU(最近离上次使用时间最长):一定时间内,根据使用顺序排队,最前面为刚使用过的,排在最后面的优先淘汰。
LFU(最近访问次数最少):一定时间内,根据使用次数排序,次数最少的优先淘汰。
# Redis 持久化
由于 redis 的数据存在内存中,服务器或 redis 进程一旦挂挂掉,数据就会丢失,所以 redis 就提供了可以持久化数据的机制
Redis 的持久化机制有两种
第一种是快照:
快照是一次全量备份,快照是内存数据的二进制序列化形式,在存储上非常紧凑。
第二种是 AOF 日志:
AOF 日志是连续的增量备份。AOF 日志记录的是内存数据修改的指令记录文本。AOF 日志在长期的运行过程中会变的无比庞大,数据库重启时需要加载 AOF 日志进行指令重放,这个时间就会无比漫长。所以需要定期进行 AOF 重写,给 AOF 日志进行瘦身。
关于 redis 单线程的理解
Redis 单线程指的是「接收客户端请求 -> 解析请求 -> 进行数据读写等操作 -> 发送数据给客户端」这个过程是由一个线程(主线程)来完成的
# RDB(快照)原理
我们知道 Redis 是单线程程序,这个线程要同时负责多个客户端套接字的并发读写操作和内存数据结构的逻辑读写。
在服务线上请求的同时,Redis 还需要进行内存快照,内存快照要求 Redis 必须进行文件 IO 操作,可文件 IO 操作是不能使用多路复用 API。
这意味着单线程同时在服务线上的请求还要进行文件 IO 操作,文件 IO 操作会严重拖垮服务器请求的性能。还有个重要的问题是为了不阻塞线上的业务,就需要边持久化边响应客户端请求。持久化的同时,内存数据结构还在改变,比如一个大型的 hash 字典正在持久化,结果一个请求过来把它给删掉了,还没持久化完呢,这要怎么搞?
Redis 使用操作系统的多进程 COW (Copy On Write) 机制来实现快照持久化,这个机制很有意思,也很少人知道。多进程 COW 也是鉴定程序员知识广度的一个重要指标。
# COW 写复制
COW: COW 允许多个进程共享同一块内存区域,直到其中一个进程试图修改该内存区域时,系统才会将其复制一份,以确保修改不会影响其他进程,也就是在进行写操作时,对被写入的这个内存页进行复制,写的操作在这个被复制出来的内存页上写
# fork (多进程)
Redis 在持久化时会调用 glibc 的函数 fork 产生一个子进程,快照持久化完全交给子进程来处理,父进程继续处理客户端请求。子进程刚刚产生时,它和父进程共享内存里面的代码段和数据段。这是 Linux 操作系统的机制,为了节约内存资源,所以尽可能让它们共享起来。在进程分离的一瞬间,内存的增长几乎没有明显变化。
子进程做数据持久化,它 不会修改现有的内存数据结构
,它只是对数据结构进行遍历读取,然后序列化写到磁盘中。但是父进程不一样,它必须持续服务客户端请求,然后对内存数据结构进行不间断的修改。
这个时候就会使用操作系统的 COW 机制来进行数据段页面的分离。数据段是由很多操作系统的页面组合而成,当父进程对其中一个页面的数据进行修改时,会将被共享的页面复制一份分离出来,然后对这个复制的页面进行修改。这时子进程相应的页面是没有变化的,还是进程产生时那一瞬间的数据。
随着父进程修改操作的持续进行,越来越多的共享页面被分离出来,内存就会持续增长。但是也不会超过原有数据内存的 2 倍大小。另外一个 Redis 实例里冷数据占的比例往往是比较高的,所以很少会出现所有的页面都会被分离,被分离的往往只有其中一部分页面。每个页面的大小只有 4K,一个 Redis 实例里面一般都会有成千上万的页面。
子进程因为数据没有变化,它能看到的内存里的数据在进程产生的一瞬间就凝固了,再也不会改变,这也是为什么 Redis 的持久化叫「快照」的原因。接下来子进程就可以非常安心的遍历数据了进行序列化写磁盘了。
例如:我现在在循环的执行 set name1 value1 命令,执行到一半的时候 redis 开始做快照备份,首先启动子进程备份在内存页 A 做查询操作,然后我后续的 set 操作页在内存页 A,这个时候就会触发操作系统的 COW 机制(即写复制)在复制出一个内存页 A,后续的写操作都在这个复制出来的内存页上,备份用的查询进行在原本的内存页 A 上操作。
# AOF 原理
AOF 日志存储的是 Redis 服务器的顺序指令序列,AOF 日志只记录对内存进行修改的指令记录。
假设 AOF 日志记录了自 Redis 实例创建以来所有的修改性指令序列,那么就可以通过对一个空的 Redis 实例顺序执行所有的指令,也就是「重放」,来恢复 Redis 当前实例的内存数据结构的状态。
Redis 会在收到客户端修改指令后,先进行参数校验,如果没问题,就立即将该指令存储到 AOF 日志缓存中,AOF 日志缓存 copy 到 内核缓存,但还没有刷到磁盘,也就是先写日志,然后再执行指令。这样即使遇到突发宕机,已经存储到 AOF 日志的指令进行重放一下就可以恢复到宕机前的状态。
Redis 在长期运行的过程中,AOF 的日志会越变越长。如果实例宕机重启,重放整个 AOF 日志会非常耗时,导致长时间 Redis 无法对外提供服务。所以需要对 AOF 日志瘦身。
AOF 日志中其实就是记录的类似 set name1 value
命令 不会记录查询命令
# AOF 重写
Redis 提供了 bgrewriteaof 指令用于对 AOF 日志进行瘦身。其原理就是开辟一个子进程对内存进行遍历转换成一系列 Redis 的操作指令,序列化到一个新的 AOF 日志文件中。序列化完毕后再将操作期间发生的增量 AOF 日志追加到这个新的 AOF 日志文件中,追加完毕后就立即替代旧的 AOF 日志文件了,瘦身工作就完成了。
例如:
先执行了 SET name1 value1 ,
SET name2 value2 ,
SET name3 value3 这些命令,如果这个时候触发 AOF,那么 AOF 文件就记录了这些命令。
当启动 AOF 重写时,Redis 会开始记录当前数据库中的写命令,但是不会将这些写命令追加到原始 AOF 文件中,而是暂时保存在内存中。在执行完您的写操作后,Redis 会根据一定的规则重新生成一个新的 AOF 文件,该文件只包含了重写期间的写命令操作,不包含原始 AOF 文件中的写命令,最终,Redis 会将原始 AOF 文件替换为新的 AOF 文件,实现 AOF 文件的瘦身。
# fsync
AOF 日志是以文件的形式存在的,当程序对 AOF 日志文件进行写操作时,实际上是将内容写到了内核为文件描述符分配的一个内存缓存中 OS buffer,然后内核会异步将脏数据刷回到磁盘的。
这就意味着如果机器突然宕机,AOF 日志内容可能还没有来得及完全刷到磁盘中,这个时候就会出现日志丢失。那该怎么办?(不管怎样都会产生数据丢失的情况,只是让数据丢失的概率降到最低)
Linux 的 glibc 提供了 fsync (int fd) 函数可以将指定文件的内容强制从内核缓存刷到磁盘。只要 Redis 进程实时调用 fsync 函数就可以保证 aof 日志不丢失。但是 fsync 是一个磁盘 IO 操作,它很慢!如果 Redis 执行一条指令就要 fsync 一次,那么 Redis 高性能的地位就不保了。
所以在生产环境的服务器中,Redis 通常是每隔 1s 左右执行一次 fsync 操作,周期 1s 是可以配置的。这是在数据安全性和性能之间做了一个折中,在保持高性能的同时,尽可能使得数据少丢失。
# 混合持久化
重启 Redis 时,我们很少使用 rdb 来恢复内存状态,因为会丢失大量数据。我们通常使用 AOF 日志重放,但是重放 AOF 日志性能相对 rdb 来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很长的时间。
Redis 4.0 为了解决这个问题,带来了一个新的持久化选项 —— 混合持久化。将 rdb 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是自持久化开始到持久化结束的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小。
混合持久化的优点包括:
- 数据完整性:AOF 持久化可以确保每个写操作都被记录下来,避免数据丢失。
- 数据恢复速度:RDB 快照可以在恢复数据时提供更快的速度,适用于大规模数据的恢复。
- 磁盘空间占用:AOF 持久化可以提供更小的持久化文件,而 RDB 快照可以降低持久化成本
# 相关配置说明
######################################## | |
#------------Redis 基本配置 --------------# | |
######################################## | |
# 是否以守护进程启动 默认:no | |
daemonize no | |
# 用于设置 Redis 绑定的网络接口(网卡)。如果不配置 bind,默认情况下 Redis 监听所有可用的网卡,redis 只接受来自绑定网络接口的请求。 | |
# Redis 的配置文件中一般默认有 bind 127.0.0.1,只允许本地连接,如果想要被远程访问注释掉 bind 配置或者 bind 外网 ip 即可。 | |
bind 127.0.0.1 | |
# 是否开启保护模式 默认:yes,是否有效会被 bind 和 requirepass 配置影响 | |
## 当 protected-mode 为 yes | |
### 注释 bind 和 requirepass,redis 的保护模式生效,只能通过本地连接 | |
### 只注释 bind,配置 requirepass,redis 的保护模式失效,可以通过密码远程连接 | |
### 只注释 requirepass,redis 的保护模式失效,可以通过 bind 的 ip 无密码连接 | |
## 当 protected-mode 为 no | |
### 无论上面的哪种场景,客户端都可以根据 bind 及 requirepass 实际参数来连接到 redis | |
protected-mode yes | |
# redis 服务端口 默认:6379 | |
port 6379 | |
# 客户端连接空闲时间单位秒,如果在指定时间内没有操作则会断开连接 默认:0(不超时) | |
timeout 0 | |
# tcp 心跳检测时间单位秒,对访问客户端的一种心跳检测,每个 n 秒检测一次 默认:0(不检测),建议设置成 60 | |
tcp-keepalive 0 | |
# 客户端最大连接数,设置 redis 同时可以与多少个客户端进行连接 默认:10000 | |
maxclients 10000 | |
# 日志级别配置 默认:notice | |
## debug:能设置的最高的日志级别,打印所有信息,包括 debug 信息。 | |
## verbose:打印除了 debug 日志之外的所有日志。 | |
## notice:打印除了 debug 和 verbose 级别的所有日志。 | |
## warning:仅打印非常重要的信息。 | |
loglevel notice | |
# 日志文件输出路径配置 | |
## 该路径默认为空。可以根据自己需要把日志文件输出到指定位置。 | |
logfile "" | |
# 数据库数量配置 默认:16 | |
databases 16 | |
# 连接密码配置 默认无密码 | |
requirepass 123456 | |
######################################## | |
#----------RDB 全量持久化配置 -------------# | |
######################################## | |
# 持久化数据存储在本地的文件名称 默认:dump.rdb | |
dbfilename dump.rdb | |
# 持久化数据存储在本地的路径,默认:./(当前工作目录) | |
dir /data | |
# 用于设置 RDB 持久化的时间间隔和条件 | |
## 表示每 900 秒内有至少 1 个写操作就保存一次 RDB 文件 | |
save 900 1 | |
## 表示每 300 秒内有至少 10 个写操作就保存一次 RDB 文件 | |
save 300 10 | |
## 表示每 10 秒内有至少 1000 个写操作就保存一次 RDB 文件 | |
save 60 10000 | |
# 当 RDB 持久化时出现错误无法继续时,是否阻塞客户端变更操作,错误可能因为磁盘已满 / 磁盘故障 / OS 级别异常等 默认:yes | |
stop-writes-on-bgsave-error yes | |
# 是否启用 RDB 文件压缩,默认: yes,压缩往往意味着额外的 cpu 消耗,同时也意味这较小的文件尺寸以及较短的网络传输时间 | |
rdbcompression yes | |
######################################## | |
#----------AOF 增量持久化配置 -------------# | |
######################################## | |
# 是否开启 AOF 默认:no | |
appendonly yes | |
# 指定 AOF 文件名称 | |
appendfilename appendonly.aof | |
# 用于设置 AOF 文件的同步策略 默认:everysec | |
## 可以选择 "always"、"everysec" 或 "no"。always 表示每次写入都同步,everysec 表示每秒同步一次,no 表示由操作系统决定何时同步 | |
appendfsync everysec | |
# 用于设置自动 AOF 重写的阈值。当 AOF 文件的扩展比例超过该值时,Redis 会自动执行重写操作。默认值为 100,表示当 AOF 文件的大小是上一次重写后大小的一倍时触发重写。 | |
auto-aof-rewrite-percentage 100 | |
# 用于设置自动 AOF 重写的最小大小。只有在 AOF 文件的大小大于该值时,才会执行重写操作。默认值为 64MB,建议 512mb | |
auto-aof-rewrite-min-size 64mb | |
# 用于设置在执行 AOF 文件重写时是否禁用同步。如果设置为 yes,则在进行重写时不会进行同步操作,默认:no | |
no-appendfsync-on-rewrite no | |
# 用于设置在加载 AOF 文件时是否允许 Redis 忽略出现错误的命令。如果设置为 "yes",则忽略错误;如果设置为 "no",则不允许加载出现错误的 AOF 文件。默认:yes | |
aof-load-truncated yes | |
# 用于设置 AOF 文件的开头是否包含 RDB 格式的部分。如果设置为 "yes",则在 AOF 文件的开头会先保存一份 RDB 格式的数据,这有助于加速数据加载。默认:no | |
aof-use-rdb-preamble no | |
######################################## | |
#----------Redis key 过期监听配置 -------------# | |
######################################## | |
# key 过期监听 默认:""(关闭),将 notify-keyspace-events 设置为 Ex 代表开启 | |
notify-keyspace-events Ex |
# Redis 整合 springboot 使用
redis 有两种方式整合,第一种是使用 Spring Data Redis
,第二种是使用 redisson
第一种对底层的操作进行了封装,提供了基于模板和注解的方式来操作 Redis 数据库,也就是 Redis template 工具,可以 更简便
的调用接口来使用 redis
第二种提供了丰富的分布式数据结构和工具,在底层实现上对 Redis 进行了性能优化,可以更有效地利用 Redis 的功能
这里我是用第一种种来整合。
添加 maven 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置 application.yml
# Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
spring:
redis:
host: localhost # 地址
port: 6379 # 端口
database: 0 # 数据库索引
password: # 密码
简单使用
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;
@RestController
public class RedisController {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@GetMapping("/set")
public String setKey(@RequestParam String key, @RequestParam Object value) {
//set key value
redisTemplate.opsForValue().set(key, value);
return "success";
}
@GetMapping("/get")
public Object getKey(@RequestParam String key) {
//get key
return redisTemplate.opsForValue().get(key);
}
}
# Redis 使用场景
# 缓存系统
因为 redis 是将数据存储到内存的,对于修改的情况少并且查询情况多的数据存储到 redis,可以大大加快数据访问
# 分布式 SESSION
对于集群项目,需要有一个共同的 session 会话,否则可能在 A 项目登陆,B 项目又要重新登陆,而且不是同一个会话,那么用户的状态就会不一致
# 计数器
Redis 的 INCRBY 命令可以实现计数器功能,非常适合用来做网站的访问统计。
# 分布式锁
redis 是一个中间件,也就也就意味着多个不同的项目可以共同访问,非常适合做分布式锁,由 redis 来管理各个项目的锁,其中 Redlock 就是一种实现分布式锁的机制
Lock4j
框架就由基于 redis 或 redisson 实现的分布式锁。官网
# Redis 缓存问题
# 缓存处理流程
前台请求,后台先从缓存中取数据,取到直接返回结果,取不到时从数据库中取,数据库取到更新缓存,并返回结果,数据库也没取到,那直接返回空结果。注意,缓存是这种处理方式为前提。
# 缓存穿透
描述:缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为 id 为 “-1” 的数据或 id 为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。
解决方案:
1、接口层增加校验,如用户鉴权校验,id 做基础校验,id<=0 的直接拦截;
2、从缓存取不到的数据,在数据库中也没有取到,这时也可以将 key-value 对写为 key-null,缓存有效时间可以设置短点,如 30 秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个 id 暴力攻击。
3、在系统启动或者数据更新时,预先将热门数据加载到缓存中。这样可以减少初次请求时的缓存穿透风险
# 缓存击穿
描述:缓存击穿是指缓存中没有但数据库中有的单条数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。
解决方案:
1、设置热点数据永远不过期。
2、加互斥锁。读数据库时需要获取锁,一条请求拿到锁之后读取数据并更新缓存,为了防止那些抢锁失败线程重新获取到锁后又进行读数据库操作,这里采用双重检验锁方式。
列如:假设一个电商网站的商品详情页是一个 热点数据
,突然失效导致大量用户同时请求该页面。如果缓存中没有该数据,所有请求都会直接访问数据库,导致数据库压力激增。为了解决这个问题,可以在缓存中设置该热点数据永不过期,或者使用互斥锁保证只有一个线程去查询数据库并更新缓存
热点数据:"热点数据" 通常指的是被频繁访问的数据。
# 缓存雪崩
描述:缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至 down 机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决方案:
1、缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
2、设置热点数据永远不过期。
列如:假设一个新闻网站的首页缓存设置了相同的过期时间,当这些缓存同时失效时,大量用户请求首页会直接访问数据库,导致数据库压力激增。为了解决这个问题,可以设置不同的过期时间,分散缓存失效的时间点,或者使用熔断机制,当缓存失效时返回一个默认的数据,避免直接访问数据库。
# 缓存预热
缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
解决方案:
1、直接写个缓存刷新页面,上线时手工操作一下;
2、数据量不大,可以在项目启动的时候自动进行加载;
3、定时刷新缓存;
例如:一个电商网站的热门商品列表可以在系统启动时提前加载到缓存中,这样用户访问热门商品列表时可以直接从缓存中获取数据,减少对数据库的查询请求。
# 缓存降级
当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。或者返回一个默认值。
缓存降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。
在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:
1、一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
2、警告:有些服务在一段时间内成功率有波动(如在 95~100% 之间),可以自动降级或人工降级,并发送告警;
3、错误:比如可用率低于 90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
4、严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。
服务降级的目的,是为了防止 Redis 服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis 出现问题,不去数据库查询,而是直接返回默认值给用户。
例如:一个在线支付系统的订单详情页的缓存失效,为了保证用户体验,可以返回一个默认的订单详情页面或者提示信息,而不是直接报错或者让用户等待缓存重新加载。
# 相关 Redis 连接工具
1、Another Redis Desktop Manager
# 参考资料
- Redis