目录
[Sorted Set(有序集合)](#Sorted Set(有序集合))
Redis
什么是Redis
Remote Dictionary Server
(远程字典服务),是一个开源的使用C语言编写,基于内存,并且支持持久化的一个NoSQL数据库。
Redis解决了什么问题
Redis实现了:
-
高性能高并发缓存:Redis的数据是存放在内存中,所以读写都是非常快速
-
数据结构存储:Redis支持多种数据结构,字符串,哈希,列表等
-
数据持久化:使用AOF持久化,RDB持久化方案
-
发布与订阅:Redis支持发布与订阅模式,可以实现消息的发布和订阅。
Redis的实现原理
1、高性能
将经常访问的数据都放在Redis中,保证用户下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,不用去磁盘中读取,所以速度相当快。
2、高并发
一般像 MySQL 这类的数据库的 QPS 大概都在 w 左右 ,但是使用 Redis 缓存之后很容易达到 10w级别(就单机 Redis 的情况,Redis 集群的话会更高)。
QPS(Query Per Second):服务器每秒可以执行的查询次数;
所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。进而,我们也就提高了系统整体的并发。
数据结构
5 种基础数据类型:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。
String
常用命令
应用场景
-
存储键值对的场景
-
需要计数的场景,使用INCR key进行数字的增减
-
分布式锁
List(列表)
Redis 的 List 的实现为一个 双向链表,即可以支持反向查找和遍历
常用命令
应用场景
保存历史记录,按照日期进行保存
Hash(哈希)
Redis 中的 Hash 是一个 String 类型的 field-value(键值对) 的映射表,特别适合用于存储对象,后续操作的时候,你可以直接修改这个对象中的某些字段的值。
常用命令
应用场景
可以用来对象数据的存储,一个用户下面的各种信息
set(集合)
类似Java中的 HashSet,集合中的元素无序但是唯一,提供了查询元素是否存在的接口
常见命令
应用场景
-
可以做两个集合的交集例如共同好友
-
抽奖系统,能够随机出一个名额
Sorted Set(有序集合)
Sorted Set 类似于 Set,但和 Set 相比,Sorted Set 增加了一个权重参数 score
,使得集合中的元素能够按 score
进行有序排列,还可以通过 score
的范围来获取元素的列表。有点像是 Java 中 HashMap
和 TreeSet
的结合体。
常见命令
应用场景
数据持久化
数据持久化指的是将数据保存到磁盘中,Redis在4.0以后支持了三种持久化方式:
-
快照(snapshotting,RDB)
-
只追加文件(append-only file, AOF)
-
RDB 和 AOF 的混合持久化(Redis 4.0 新增)
数据持久化解决了重启机器或者就是机器故障以后的数据恢复工作
RDB
通过创建快照来获得存储在内存里面的数据在某个时间的副本。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命令创建快照。
是否阻塞主线程
Redis 提供了两个命令来生成 RDB 快照文件:
-
save
: 同步保存操作,会阻塞 Redis 主线程; -
bgsave
: fork 出一个子进程,子进程执行,不会阻塞 Redis 主线程,默认选项。
这里说 Redis 主线程而不是主进程的主要是因为 Redis 启动之后主要是通过单线程的方式完成主要的工作。如果你想将其描述为 Redis 主进程,也没毛病
执行后,会在服务端目录下生成一个dump.rdb文件,而这个文件中就保存了内存中存放的数据,当服务器重启后,会自动加载里面的内容到对应数据库中。
优缺点
优点:恢复快速,保存简单
缺点:
-
可能丢失最新更新的数据
-
性能开销,如果我们数据比较大的时候,子线程进行保存的时候会消耗较多的cpu资源
AOF
与快照持久化相比,AOF 持久化的实时性更好。
开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入到 AOF 缓冲区 server.aof_buf
中,然后再写入到 AOF 文件中(此时还在系统内核缓存区未同步到磁盘),最后再根据持久化方式( fsync
策略)的配置来决定何时将系统内核缓存区的数据同步到硬盘中的。
只有同步到磁盘中才算持久化保存了,否则依然存在数据丢失的风险,比如说:系统内核缓存区的数据还未同步,磁盘机器就宕机了,那这部分数据就算丢失了。
AOF 文件的保存位置和 RDB 文件的位置相同,都是通过 dir
参数设置的,默认的文件名是 appendonly.aof
。
工作流程
AOF 持久化功能的实现可以简单分为 5 步:
-
命令追加(append):所有的写命令会追加到 AOF 缓冲区中。
-
文件写入(write) :将 AOF 缓冲区的数据写入到 AOF 文件中。这一步需要调用
write
函数(系统调用),write
将数据写入到了系统内核缓冲区之后直接返回了(延迟写)。注意!!!此时并没有同步到磁盘。 -
文件同步(fsync) :AOF 缓冲区根据对应的持久化方式(
fsync
策略)向硬盘做同步操作。这一步需要调用fsync
函数(系统调用),fsync
针对单个文件操作,对其进行强制硬盘同步,fsync
将阻塞直到写入磁盘完成后返回,保证了数据持久化。 -
文件重写(rewrite):随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩的目的。
-
重启加载(load):当 Redis 重启时,可以加载 AOF 文件进行数据恢复
AOF持久化的策略
在 Redis 的配置文件中存在三种不同的 AOF 持久化方式( fsync
策略),它们分别是:
-
appendfsync always
:主线程调用write
执行写操作后,后台线程(aof_fsync
线程)立即会调用fsync
函数同步 AOF 文件(刷盘),fsync
完成后线程返回,这样会严重降低 Redis 的性能(write
+fsync
)。 -
appendfsync everysec
:主线程调用write
执行写操作后立即返回,由后台线程(aof_fsync
线程)每秒钟调用fsync
函数(系统调用)同步一次 AOF 文件(write
+fsync
,fsync
间隔为 1 秒) -
appendfsync no
:主线程调用write
执行写操作后立即返回,让操作系统决定何时进行同步,Linux 下一般为 30 秒一次(write
但不fsync
,fsync
的时机由操作系统决定)。
可以看出:这 3 种持久化方式的主要区别在于 fsync
同步 AOF 文件的时机(刷盘)。
为了兼顾数据和写入性能,可以考虑 appendfsync everysec
选项 ,让 Redis 每秒同步一次 AOF 文件,Redis 性能受到的影响较小。而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候,Redis 还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。
AOF重写
当 AOF 变得太大时,Redis 能够在后台自动重写 AOF 产生一个新的 AOF 文件,这个新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小。
AOF 重写
AOF 重写(rewrite) 是一个有歧义的名字,该功能是通过读取数据库中的键值对来实现的,程序无须对现有 AOF 文件进行任何读入、分析或者写入操作。
由于 AOF 重写会进行大量的写入操作,为了避免对 Redis 正常处理命令请求造成影响,Redis 将 AOF 重写程序放到子进程里执行。
AOF 文件重写期间,Redis 还会维护一个 AOF 重写缓冲区,该缓冲区会在子进程创建新 AOF 文件期间,记录服务器执行的所有写命令。当子进程完成创建新 AOF 文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新 AOF 文件的末尾,使得新的 AOF 文件保存的数据库状态与现有的数据库状态一致。最后,服务器用新的 AOF 文件替换旧的 AOF 文件,以此来完成 AOF 文件重写操作。
AOF校验
AOF 校验机制是 Redis 在启动时对 AOF 文件进行检查,以判断文件是否完整,是否有损坏或者丢失的数据。这个机制的原理其实非常简单,就是通过使用一种叫做 校验和(checksum) 的数字来验证 AOF 文件。这个校验和是通过对整个 AOF 文件内容进行 CRC64 算法计算得出的数字。如果文件内容发生了变化,那么校验和也会随之改变。因此,Redis 在启动时会比较计算出的校验和与文件末尾保存的校验和(计算的时候会把最后一行保存校验和的内容给忽略点),从而判断 AOF 文件是否完整。如果发现文件有问题,Redis 就会拒绝启动并提供相应的错误信息。AOF 校验机制十分简单有效,可以提高 Redis 数据的可靠性。
Redis内存管理
一般我们都是需要给内存设置过期时间,首先考虑我们的内存是有限的,其次有些数据也是有时效性的。
如何判断数据过期
Redis 通过一个叫做过期字典(可以看作是 hash 表)来保存数据过期的时间。
过期数据的删除策略
-
惰性删除:只会在取出 key 的时候才对数据进行过期检查。这样对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。
-
定期删除:每隔一段时间抽取一批 key 执行删除过期 key 操作。并且,Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。
定期删除对内存更加友好,惰性删除对 CPU 更加友好。两者各有千秋,所以 Redis 采用的是 定期删除+惰性/懒汉式删除 。
但是,仅仅通过给 key 设置过期时间还是有问题的。因为还是可能存在定期删除和惰性删除漏掉了很多过期 key 的情况。这样就导致大量过期 key 堆积在内存里,然后就 Out of memory 了。
怎么解决这个问题呢?答案就是:Redis 内存淘汰机制。
内存淘汰机制
因为过期时间的删除策略都是具有局限性,惰性删除有时无法及时删除,定期删除会占用cpu大量的时间
相关问题:MySQL 里有 2000w 数据,Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据?
Redis 提供 6 种数据淘汰策略:
-
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:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!
Redis实现分布式锁
Redis缓存问题
缓存穿透
指的是访问一个数据库和缓存中都不存在的数据,每一次都会去访问数据库,也不会存在缓存中
解决方法
布隆过滤
布隆过滤器是将我们数据库中存在的数据都放置在一个二进制向量中,使用N个哈希值,将数据进行hash然后存放在里面。每一次查询的时候都先将我们查询的目标都进行N次hash然后如果有一个位置为0,则表示数据不存在。
缺点:
-
数据越来越多则会越来越不准确。
-
存放在布隆过滤器中的值不容易删除
接口限流
按照我们的用户或者IP对接口进行限流,设置防刷机制。
缓存击穿
比如某条热点数据过期,然后大量的数据去访问数据库,带来巨大的压力
穿透和击穿的区别就是数据库中有或者没有
解决办法
设置热点数据永不过期或者过期时间比较长。
针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期。
请求数据库写数据到缓存之前,先获取互斥锁,保证只有一个请求会落到数据库上,减少数据库的压力。
缓存雪崩
当你的Redis服务器炸了或是大量的Key在同一时间过期,这时相当于缓存直接GG了,那么如果这时又有很多的请求来访问不同的数据,同一时间内缓存服务器就得向数据库大量发起请求来重新建立缓存,很容易把数据库也搞GG。
解决办法
-
采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。
-
限流,避免同时处理大量的请求。
-
多级缓存,例如本地缓存+Redis 缓存的组合,当 Redis 缓存出现问题时,还可以从本地缓存中获取到部分数据。
缓存预热
常见的缓存预热方式有两种:
-
使用定时任务,比如 xxl-job,来定时触发缓存预热的逻辑,将数据库中的热点数据查询出来并存入缓存中。
-
使用消息队列,比如 RabbitMQ,来异步地进行缓存预热,将数据库中的热点数据的主键或者 ID 发送到消息队列中,然后由缓存服务消费消息队列中的数据,根据主键或者 ID 查询数据库并更新缓存。
数据库和缓存一致性
Redis可以将常访问的数据保存起来,新的请求先在Redis中进行查询,能够缓解数据库的压力。
在读的情况下,无论怎样都是不会出现问题的,所以关键就是读写出现不一致的问题。
CAP原理
根据CAP原理,CAP原则又称CAP定理,指的是在一个分布式系统中,存在Consistency(一致性)、Availability(可用性)、Partition tolerance(分区容错性),三者不可同时保证,最多只能保证其中的两者。
一致性(C):在分布式系统中的所有数据备份,在同一时刻都是同样的值(所有的节点无论何时访问都能拿到最新的值)
可用性(A):系统中非故障节点收到的每个请求都必须得到响应(比如我们之前使用的服务降级和熔断,其实就是一种维持可用性的措施,虽然服务返回的是没有什么意义的数据,但是不至于用户的请求会被服务器忽略)
分区容错性(P):一个分布式系统里面,节点之间组成的网络本来应该是连通的,然而可能因为一些故障(比如网络丢包等,这是很难避免的),使得有些节点之间不连通了,整个网络就分成了几块区域,数据就散布在了这些不连通的区域中(这样就可能出现某些被分区节点存放的数据访问失败,我们需要来容忍这些不可靠的情况)
总的来说,数据存放的节点数越多,分区容忍性就越高,但是要复制更新的次数就越多,一致性就越难保证。同时为了保证一致性,更新所有节点数据所需要的时间就越长,那么可用性就会降低。
所以这里我们只能保证最终一致性
解决方法
当我们修改数据库的数据的时候,先更新数据库还是Redis缓存?
- 先删除缓存,再更新数据库
当多线程进行访问的时候,当线程A需要写入X,然后更新数据库的时候,线程2来访问缓存X,缓存中没有消息,进入数据库进行读取,读取旧的值X到缓存中,这个时候线程1再修改好数据库则最终一致性都不能保证。
- 先更新数据库再删除缓存
虽然有可能还是会最终一致性无法满足,但是概率很小,因为读和写请求的并发,读请求会更快的读取并且更新到缓存中,而读更慢。
-
双重删除
-
消息队列
使用消息队列异步删除,因为消息队列采用了确认机制,所以能够确保缓存中的数据被删除,虽然也会读取到脏数据,但是这个可以看作MVcc,读的操作是在更新的操作之前,不能看到更新完成后的失误,最后也能实现数据的最终一致性
常见问题
String 还是 Hash存储对象更好呢?
具体看我们的使用情况,string存储的是已经序列化的数据,存放整个对象。Hash是对每个字段单独的进行存储,可以获取部分的信息,可以修改。所以如果需要经常修改则Hash合适
String 存储更加的节省内存,因为Hash 需要保存更多的结构信息