赞
踩
什么是 redis ?
是一种基于键值对的 NoSql 型数据库。与 hashMap 不同的是,Redis 中的 value 支持 string(字符串)、hash(哈希)、 list(列表)、set(集合)、zset(有序集合)、Bitmaps(位图)、GEO(地理信息定位)等多种数据结构。而 HashMap 只是 redis 中的 hash ,他们俩结构是相同的。
redis 最常用的就是作为缓存。而之所以用 redis 作为缓存,是因为 redis 中的数据保存在内存之中,非常快,所以用 Redis 缓存可以极大地提高应用的响应速度和吞吐量。
redis 中的数据结构 zset 适合去做排行榜的功能,同时原子递增可以实现计数器。
可以用来存储任何类型的数据比如字符串、整数、浮点数、图片(图片的 base64 编码或者解码或者图片的路径)、序列化后的对象。
redis 自己构建的一种动态字符串。
有什么用:
字符串主要有以下几个典型的使用场景:
缓存功能
计数
共享 Session
键值对集合,key 是字符串,value 是一个 Map 集合,比如说 value = {name: '沉默王二', age: 18}
,name 和 age 属于字段 field,沉默王二 和 18 属于值 value。
哈希类似于JDK 1.8之前的 HashMap ,底层实现都是数组加链表。
主要用于缓存对象和用户信息。
list 就是链表数据结构的实现,双向链表。
列表主要有以下两个使用场景:
消息队列
文章列表
Set 像 Java 中的 hashSet。是无序集合,集合中的元素是唯一的,不允许重复。
基于 Set 轻易实现交集、并集、差集的操作,所以就可以实现一些共同关注,共同点赞的事。
Zset,有序集合,比 set 多了一个排序属性 score(分值)。也就是说可以排序。
可以做排行榜。
基于内存的数据存储。
redis 中的数据都存储在内存之中,所以数据的读取更快。
单线程模型
Redis 由于单线程模型,所以减少了锁竞争和线程切换带来的消耗。
IO 复路
IO 复路就是内核会一直监听socket的请求,谁有请求,就会将谁交给 redis 处理。
高效数据结构
Redis 中的数据结构种类多,而且都经过优化。
引用知乎上一个高赞的回答来解释什么是 I/O 多路复用。假设你是一个老师,让 30 个学生解答一道题目,然后检查学生做的是否正确,你有下面几个选择:
第一种选择:按顺序逐个检查,先检查 A,然后是 B,之后是 C、D。。。这中间如果有一个学生卡住,全班都会被耽误。这种模式就好比,你用循环挨个处理 socket,根本不具有并发能力。
第二种选择:你创建 30 个分身,每个分身检查一个学生的答案是否正确。 这种类似于为每一个用户创建一个进程或者- 线程处理连接。
第三种选择,你站在讲台上等,谁解答完谁举手。这时 C、D 举手,表示他们解答问题完毕,你下去依次检查 C、D 的答案,然后继续回到讲台上等。此时 E、A 又举手,然后去处理 E 和 A。
第一种就是阻塞 IO 模型,第三种就是 I/O 复用模型。du
Linux 系统有三种方式实现 IO 多路复用:select、poll 和 epoll。
epoll 方式就是将用户的 socket 对应的 fd 注册进 epoll ,这样epoll 就可以监测用户信息是否到达,从而避免大量无效的无用操作。
因为 Redis 是基于内存的,而多线程的目的是,让CPU资源得到充分利用。所以说没必要去利用多线程,本身就已经是基于内存的了。redis 的瓶颈最有可能是内存的大小或者网络限制。
后期多线程是为了提高网络 IO 能力,也就是提高了数据的读写能力,redis 的命令的执行仍然是单线程。/
这样做的⽬的是因为 Redis 的性能瓶颈在于⽹络 IO ⽽⾮ CPU,使⽤多线程能提升 IO 读写的效率,从⽽整体提⾼ Redis 的性能。
防止内存超出范围。
利用的 hash 表,键保存的就是键值对的键值,value 是 longlong 类型的过期时间。
惰性删除:访问 key 的时候,如果发现已过期,那么删除。如果一直不访问,可能一直不删除。对 CPU 友好,但是浪费内存资源
定期删除:定期的随机测试一些建,如果发现过期,那么删除过期的键。
延迟队列:将键都放进队列中,谁过期了,谁出去。这样能保证每个过期的键都能被删除,但是维护队列本身就很麻烦。
定时删除:为每个键都加一个定时器,如果时间到了就删除对应的键,但是这样对CPU 压力是最大的,因为要为每个键设置一个定时器。
Redis 中采用的是定期删除 + 惰性删除。
Redis 的内存是会发生不足的。那我们应该怎么解决?
修改配置文件,redis.conf 中的 maxmemory
也可以通过命令 set maxmemory 动态设置内存上限
修改内存淘汰策略
Redis 集群
当 Redis 所用内存达到 maxmemory 上限时,会触发相应的溢出控制策略。
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):从数据集(server.db[i].dict
)中移除最近最少使用的数据淘汰。
allkeys-random:从数据集(server.db[i].dict
)中任意选择数据淘汰。
no-eviction(默认内存淘汰策略):禁止驱逐数据,当内存不足以容纳新写入数据时,新写入操作会报错。
4.0 版本后增加以下两种:
volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires
)中挑选最不经常使用的数据淘汰。
allkeys-lfu(least frequently used):从数据集(server.db[i].dict
)中移除最不经常使用的数据淘汰。
RDB 是创建快照,每隔指定的一段时间创建快照然后保存到磁盘中的 RDB 文件中。
可以通过两种方式手动创建
save
bgsave、
以下场景会自动触发 RDB 持久化:
①、在 Redis 配置文件(通常是 redis.conf)中,可以通过save
指令配置自动触发 RDB 持久化的条件。这个指令可以设置多次,每个设置定义了一个时间间隔(秒)和该时间内发生的变更次数阈值。
save 900 1 save 300 10 save 60 10000
这意味着:
如果至少有 1 个键被修改,900 秒后自动触发一次 RDB 持久化。
如果至少有 10 个键被修改,300 秒后自动触发一次 RDB 持久化。
如果至少有 10000 个键被修改,60 秒后自动触发一次 RDB 持久化。
满足以上任一条件,RDB 持久化就会被自动触发。
②、当 Redis 服务器通过 SHUTDOWN 命令正常关闭时,如果没有禁用 RDB 持久化,Redis 会自动执行一次 RDB 持久化,以确保数据在下次启动时能够恢复。
③、在 Redis 复制场景中,当一个 Redis 实例被配置为从节点并且与主节点建立连接时,它可能会根据配置接收主节点的 RDB 文件来初始化数据集。这个过程中,主节点会在后台自动触发 RDB 持久化,然后将生成的 RDB 文件发送给从节点。
会同步的将数据集的快照存入磁盘中的 RDB 文件中。这个操作会阻塞所有客户端请求直到 RDB 文件被完全写入磁盘,会阻塞主线程。
会异步的将数据集的快照存入磁盘中的 RDB 文件中。这个命令会立即返回,所以不会耽误Redis 的主线程。会 fork 出一个子线程去创建快照,子线程执行,不会阻塞主线程。
AOF 是通过将操作数据集的命令,追加到 AOF 文件后,如果需要持久化,在重新执行一遍 AOF 的命令。
命令追加:所有命令 append 到 AOF 缓冲区,这个缓存区在内存中,暂时存放命令。
文件写入( write ):系统会将AOF缓冲区中的内容写入到AOF文件中。这个写入操作并不是立即完成的,而是先写入到操作系统的系统内核缓存中。此刻 write 方法被调用。
文件同步:为了保证数据的安全性,需要将文件系统缓存中的数据真正地写入到磁盘上。可以通过修改配置中的 appendfsync 来决定同步策略:
always
:每次写命令执行后都立即同步数据到磁盘。
everysec
:每秒同步一次数据到磁盘。
no
:由操作系统决定何时同步数据到磁盘。
文件重写:随着 AOF 文件越来越大,文件是需要被重写的。AOF 重新会创建一个新的 AOF 文件,并且会遍历整个数据集,这个文件会包含重建数据库的最小数据集。在重写的过程中,命令还是会添加到旧的 AOF 文件中,但同时也会记录到缓冲区中,一旦重写完成,将被写入新的 AOF 文件。
AOF 重写操作由 BGREWRITEAOF 命令触发,它会创建一个子进程来执行重写操作,因此不会阻塞主进程。
AOF 相当于 RDB 来说,更灵活,实时性更好,可以设置不同的 fsync 策略,如每秒同步一次,每次写入命令就同步,或者完全由操作系统来决定何时同步。
RDB 更适合做备份数据。使用 RDB 文件恢复数据,直接解析还原数据即可,不需要一条一条地执行命令,速度非常快。而 AOF 则需要依次执行每个写命令,速度非常慢。也就是说,与 AOF 相比,恢复大数据集的时候,RDB 速度更快,但是可能会丢失最后一次快照之后的数据。
一般来说,要保证数据的安全性,两种方式应该结合使用。
发生故障,可以RDB 或 AOF 恢复,将 RDB 或 AOF 文件拷贝到 Redis 数据目录下。如果使用 AOF 恢复,配置文件开启 AOF,然后启动 redis-server 即可。
具体:
如果都存在优先加载 AOF。
Redis 启动时加载数据的流程:
AOF 持久化开启且存在 AOF 文件时,优先加载 AOF 文件。
AOF 关闭或者 AOF 文件不存在时,加载 RDB 文件。
加载 AOF/RDB 文件成功后,Redis 启动成功。
AOF/RDB 文件存在错误时,Redis 启动失败并打印错误信息
混合持久化是RDB在AOF 重写的时候生成一份快照,在持久化的时候,或者恢复数据时,先加载 RDB快照,然后再执行 AOF 文件中的命令,这样又快又不会丢失数据。
Redis 作为缓存的时候,会发生缓存击穿,缓存穿透,缓存雪崩三种问题。
某个热点数据,在某时刻过期,那么大量请求在缓存中找不到这个数据,那么请求就会直达数据库,数据库压力瞬间增大。
互斥锁:给这个 key 加锁,使只能有一个线程访问 key,加载数据到缓存,其他请求等到缓存有之后,再去访问缓存。
使用异步的方式,不断刷新 key 的过期时间,或者干脆不设置过期时间。
缓存穿透指的是一直查询一个数据库中不存在的数据,导致每次在缓存中都找不到,请求总是直达数据库。
布隆过滤器:查询缓存前,利用布隆过滤器判断数据是否存在,如果不存在,直接返回,不进行查询。
空值缓存:当我们查询数据的时候,如果没在数据库内,那么我们在缓存中添加一个 key ,null 的键值对,并且设置一个较短的过期时间,来避免占用资源。
缓存雪崩是指在某一个时间点,由于大量的缓存数据同时过期或缓存服务器突然宕机了,导致所有的请求都落到了数据库上(比如 MySQL),从而对数据库造成巨大压力,甚至导致数据库崩溃的现象。
总之就是,崩了,崩的非常严重,就叫雪崩了(电影电视里应该看到过,非常夸张)。
集群 Redis
备份缓存
设置不同的过期时间 (在原有的过期时间基础上 添加一个随机值)
限流与降级
可以用一些限流策略,例如令牌桶,从而限制访问流量。
可以降级,例如去关闭一些核心业务
正常逻辑,如果只是读的话并不涉及一致性问题,但是如果写的话,需要考虑一致性问题。
在用 Redis 当缓存的时候,我们去更新数据库,总会造成缓存与数据库不一致的问题。
有两种处理方法:先处理缓存还是先操作数据库。
需要注意的是无论是哪种方法,都推荐删除缓存替代修改缓存,修改缓存成本更高,删缓存业务逻辑更简单。
并发线程会导致数据不一致。
首先线程 1 发布了一个修改数据库的请求,于是他先清除 redis 中的缓存,然后更新数据库,但是更新的过程是需要一定时间,并且有网络延迟的。此时线程2发起查数据的请求,先查 redis 中的数据,redis 中没有,就查数据库,此时数据库还没更新完,还是脏数据。查数据库之后就要返回缓存到redis中,那么redis中还是脏数据,白删了。
redis 中还会出现脏数据,会导致后面的请求如果想查数据,返回的一直是错误的数据。
所以我们可以在数据库更新之后,再删一次 redis 中的缓存。这样只有线程2 一个线程读的是脏数据,其他之后的线程都是正确的数据。(无法保证强一致性)。
又因为更新数据库需要一定的时间,所以要等一段时间再次删除缓存,解决办法叫延迟双删。
如果要保证强一致性,一次都不能错,那就只能加锁了,但是加锁会影响吞吐量,用redis 的目的就是增加吞吐量,所以一般不推荐这么做。
先更新数据库,此时并发线程2查询数据,查询缓存,读出来的是脏数据。等更新完数据库,就删除 redis 缓存,此时如果有数据再读,redis 如果为空,查询数据库,数据库返回缓存到 redis ,之后的线程查询的就都是正确的了。
所以只有线程 2 读的是脏数据。
删除失败怎么办?
如果删除失败的话读的一直是老数据,所以引入删除重试的机制。
通过异步请求的方式,发送消息将需要删除的 key 放入至 mq 之中,然后系统监听 mq,重新删除。
但是这个方法仍然有问题,mq 是放在整个流程内的,代码耦合度高,我们单独将它拿出来,可以利用cannal.
cannal 是阿里研发的,用途是基于 MySQL 增量日志,提供增量数据订阅和消费。cannal 客户端可以是一个 springboot 应用。也就是说我们将删除重试的操作放在 springboot 应用中去解决,解决了代码耦合度问题
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。