Redis(非关系型内存键值数据库)
1、Redis在项目中使用的方式
项目中使用Redis缓存信息的时间不同,包括了
验证码,时间为5分钟;…
2、Redis能够存储哪几种类型
五种类型:
String:缓存一般的字符串/数字等数据,项目中通常用来缓存验证码/Json序列化后的字符串/计数操作用户金额增减,十分钟后刷新数据库;一个字符串允许存储的最大容量为512MB
List:具有先进先出的原则,可以实现简单的消息队列
Hash:保存结构体信息,无需序列化对象为字符串,能够单独存储对象中的独立字段
hset user.10002 name monkey
hget user.10002 name 获取单个字段// hgetall user.10002获取所有
Set:键值无序且唯一,一般用在一些去重的场景,用户只能中奖一次去重场景
Zset有序集合:相比set集合,为value赋予了一个score,表示这个value的排序权重。用户评论点赞次数,内容热门,排行榜之类的场景。
3、Redis与Memcached的区别
- 两者都是非关系型内存键值数据库
- Redis 支持五种不同的数据类型,Memcached 仅支持字符串类型
- Redis 支持两种持久化策略:RDB 快照和 AOF 日志,而 Memcached 不支持持久化
- Redis Cluster 实现了分布式的支持,Memcached 不支持分布式
- 在 Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用的 value 交换到磁盘,而 Memcached 的数据则会一直在内存中
4、Redis 的雪崩、穿透和击穿
缓存雪崩
缓存在同⼀时间大面积的失效,后面的请求都直接落到了数据库上,导致服务崩溃。
解决方案
- 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
- 如果缓存数据库是分布式部署,将热点数据均匀分布在不同的缓存数据库中。
- 设置热点数据永远不过期。
缓存穿透
大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这⼀层。
解决方案
- 缓存无效key:将请求的key缓存至数据库,value设置为null,并且给一个随机的过期时间
- 布隆过滤器:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话则判断缓存中是否存在对应数据,存在返回数据,不存在则到数据库中查找,如果数据库也不存在则返回空数据,存在则更新缓存数据并返回。
整合布隆过滤器(redisson依赖中的RBloomFilter) ;参考https://blog.csdn.net/u013658068/article/details/118694064
缓存击穿
大量请求访问同一个热点数据,当某一瞬间这个热点key过期,请求直接落到数据库造成服务崩溃。
解决方案
- 设置热点数据永不过期
- 对相应的代码段加互斥锁
5、Redis 的过期策略
Redis采用的是定期删除 + 懒惰删除策略
定期删除策略
Redis 会将每个设置了过期时间的 key 放入到一个独立的字典中,默认每 100ms 进行一次过期扫描:
- 随机抽取 20 个 key
- 删除这 20 个key中过期的key
- 如果过期的 key 比例超过 1/4,就重复步骤 1,继续删除。
懒惰删除策略
客户端访问一个key的时候,Redis会先检查它的过期时间,如果发现过期就立刻删除这个key。
为什不扫描所有的 key?
Redis 是单线程,全部扫描岂不是卡死了。而且为了防止每次扫描过期的 key 比例都超过 1/4,导致不停循环卡死线程,Redis 为每次扫描添加了上限时间,默认是 25ms。
如果在同一时间出现大面积 key 过期,Redis 循环多次扫描过期词典,直到过期的 key 比例小于 1/4。这会导致卡顿,而且在高并发的情况下,可能会导致缓存雪崩。
6、内存淘汰机制
Redis 的内存占用会越来越高,为了限制最大使用内存,提供了 redis.conf 中的配置参数 maxmemory。当内存超出 maxmemory,Redis 提供了几种内存淘汰机制让用户选择,配置 maxmemory-policy:
noeviction | 直接返回错误; | |
---|---|---|
volatile-ttl | 从设置了过期时间的键中,选择过期时间最小的键,进行淘汰; | |
volatile-random | 从设置了过期时间的键中,随机选择键,进行淘汰; | |
volatile-lru | 从设置了过期时间的键中,使用LRU算法选择键,进行淘汰; | |
volatile-lfu | 从设置了过期时间的键中,使用LFU算法选择键,进行淘汰; | 4.0 |
allleys-random | 从所有的键中,随机选择键,进行淘汰; | |
allkeys-lru | 从所有的键中,使用LRU算法选择键,进行淘汰; | |
allkeys-lfu | 从所有的键中,使用LFU算法选择键,进行淘汰; | 4.0 |
LRU 算法
实现 LRU 算法除了需要 key/value 字典外,还需要附加一个链表,链表中的元素按照一定的顺序进行排列。当空间满的时候,会踢掉链表尾部的元素。当字典的某个元素被访问时,它在链表中的位置会被移动到表头。所以链表的元素排列顺序就是元素最近被访问的时间顺序。
LFU算法
LFU算法是Redis4.0里面新加的一种淘汰策略。它的全称是Least Frequently Used,它的核心思想是根据key的最近被访问的频率进行淘汰,很少被访问的优先被淘汰,被访问的多的则被留下来。
7、Redis的持久化机制
Redis的持久化方式有3种
- 快照方式(RDB Redis DataBase) 将某一时刻的内存数据,以二进制的方式写入到磁盘当中;
- 文件追加方式(AOF Append Only File)记录所有的文件并以文本的形式追加到文件中;
- 混合持久化方式,Redis 4.0之后新增的方式,混合持久化是结合RDB和AOF的优点,在写入的时候先把当前的数据以RDB的形式写入到文件的开头,再将后续的操作以AOF的格式存入文件当中,这样既能保证重启时的速度,又能降低数据丢失的风险
持久化如何触发
RDB的持久化触发方式有手动触发和自动触发
手动触发可以通过save和bgsave两个命令执行,save会阻塞线程,持久化完成后才会响应其他客户端命令。
bgsave则不会,该命令可以后台执行。、
自动触发可以通过.conf配置文件配置生效。
save 900 1 #在900秒(15分钟)之后,如果至少有1个key发生变化,则dump内存快照。
save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,则dump内存快照。
save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,则dump内存快照。
AOF通过配置文件方式触发
appendfsync always #每次有数据修改发生时都会写入AOF文件。
appendfsync everysec #每秒钟同步一次,该策略为AOF的缺省策略。
appendfsync no #从不同步。高效但是数据不会被持久化。
RDB优点
- RDB生成紧凑压缩的二进制文件,体积小,使用该文件恢复数据的速度非常快;
RDB缺点
- 因为Redis只能保存某一时间间隔的数据,所以如果Redis意外宕机的话,就会丢失一段时间的数据;
- BGSAVE每次运行都要执行fork操作创建子进程,属于重量级操作,不宜频繁执行,那么说明RDB快照这种方式不能保证持久化的实时性。
AOF优点
- 可以解决持久化的实时性,默认不开启,需要修改配置
AOF缺点
- 对于相同数量的数据集而言,AOF文件通常要大于RDB文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
二者选择的标准,就是看系统是愿意牺牲一些性能,换取更高的缓存一致性(aof),还是愿意写操作频繁的时候,不启用备份来换取更高的性能,待手动运行bgsave的时候,再做备份(rdb)。
8、如何保证缓存与数据库的双写一致性?
缓存一致性是数据库和缓存保持一致,当修改了数据库的信息缓存的数据也要同时更新和数据库保持一致。去查询数据时先查询缓存,如果缓存有就返回,如果没有就查询数据库,如果查不到则缓存一个null字符串(过期时间设置的小一些,避免缓存穿透),如果查询到了,则缓存到redis具体的信息。
1、先删除缓存,再更新数据库
缺点:如果删除了缓存更新数据库的操作没有成功,此时查询数据的请求会把旧数据存储到缓存中。
2、先更新数据库,再删除缓存。
缺点:如果更新了数据库,删除缓存的操作失败了,此时查询数据的请求查到的数据仍然是旧数据。
3、延迟双删,先删除缓存、再更新数据库,再延迟一定的时间去删除缓存。
为什么要两次删除缓存,因为有可能第一次删除缓存后其它查询请求将旧数据存储到了缓存.为什么要延迟一定的时间去删除缓存,为了给mysql主向从同步的时间,如果立即删除缓存很可能其它请求读到的数据还是旧数据。
延迟的时间不好确定,延迟双删仍然可能导致脏数据所以结论: 以上方案当存在高并发时都无法解决数据库和缓存强一致性的问题。
如何做缓存一致性?
需要根据需求来定:
1、实现强一致性 需要使用分布式锁控制,修改数据和向缓存存储数据使用同一个分布式锁
2、实现最终一致性,缓存数据要加过期时间,即使出现数据不致性当过期时间一到缓存失效又会从数据库查询最新的数据存入缓存。
3、对于实时性要求强的,要实现数据强一致性要尽量避免使用缓存,可以直接操作数据库。使用工具对数据进行同步方案如下:
1、使用任务表加任务调度的方案进行同步
使用Canal基于MySQL的binlog进行同步。
补:
强一致性:即复制是同步的
- 任何一次读都能读到某个数据的最近一次写的数据。
- 系统中的所有进程,看到的操作顺序,都和全局时钟下的顺序一致。(顺序一致性则是操作顺序合理即可,不需要和全局时钟下的顺序一致)
弱一致性(最终一致性):即复制是异步的
- 数据更新后,如果能容忍后续的访问只能访问到部分或者全部访问不到,则是弱一致性。
9、本地锁与分布式锁
本地锁
在单体项目下,使用Redis+本地锁可以有效解决缓存击穿问题;也可以保证多个线程并发访问公共资源时,只能有一个线程在执行。
在微服务项目下,本地锁无法锁住多个服务并发访问的公共资源,因为本地锁锁的只是单个进程中的多个线程,
而微服务中多个服务运行下属于多个进程并发。
显然,在微服务下,本地锁无法锁住多服务共享的资源。
此处即可使用可跨服务、跨虚拟机的分布式锁。
分布式锁
分布式锁能够在分布式环境下对一个对象进行加锁。
分布式锁的三种实现方式:
**基于数据库实现分布式锁:**通过数据库的唯一索引来实现。
加锁时我们在数据库中插入一条锁记录,利用业务id进行防重。当第一个竞争者加锁成功后,第二个竞争者再来加锁就会抛出唯一索引冲突,如果抛出这个异常,我们就判定当前竞争者加锁失败,解锁只需要删除这条记录即可。防重业务id需要我们自己来定义,例如我们的锁对象是一个方法,则我们的业务防重id就是这个方法的名字,如果锁定的对象是一个类,则业务防重id就是这个类名。
**基于Zookeeper:**Zookeeper一般用作配置中心,在Zookeeper中创建瞬时节点,利用节点不能重复创建的特性来保证排他性。
**基于缓存实现分布式锁:**一般使用Redis来实现分布式锁都是利用Redis的SETNX key value这个命令,只有当key不存在时才会执行成功,如果key已经存在则命令执行失败。理论上来说使用缓存来实现分布式锁的效率最高,加锁速度最快,因为Redis几乎都是纯内存操作,而基于数据库的方案和基于Zookeeper的方案都会涉及到磁盘文件IO,效率相对低下。
在实现分布式锁的时候我们需要考虑一些问题,例如:分布式锁是否可重入,分布式锁的释放时机,分布式锁服务端是否有单点问题等。
具体参考博客https://blog.csdn.net/qq_42764269/article/details/122435977
三种方案比较
方案 | 理解难易程度 | 实现的复杂度 | 性能 | 可靠性 | 优点 | 缺点 |
---|---|---|---|---|---|---|
基于数据库 | 容易 | 复杂 | 差 | 不可靠 | ||
基于缓存(Redis) | 一般 | 一般 | 高 | 可靠 | Set和Del指令性能较高 | 1.实现复杂,需要考虑超时,原子性,误删等情形。 2.没有等待锁的队列,只能在客户端自旋来等待,效率低下。 (但是现在有Redisson这两缺点就相当于没有了) |
基于Zookeeper | 难 | 简单 | 一般 | 一般 | 1.有封装好的框架,容易实现 2.有等待锁的队列,大大提升抢锁效率。 | 添加和删除节点性能较低 |
10、如何实现Redis的高可用?
实现Redis的高可用,主要有哨兵和集群两种方案。
**哨兵模式:**在Redis3.0以前的版本要实现集群一般是借助哨兵sentinel工具来监控master节点的状态,如果master节点异常,则会做主从切换,将某一台slave作为master。
**缺点:**哨兵的配置略微复杂,并且性能和高可用性等各方面表现一般,特别是在主从切换的瞬间存在访问瞬断的情况,而且哨兵模式只有一个主节点对外提供服务,没法支持很高的并发,且单个主节点内存也不宜设置得过大,否则会导致持久化文件过大,影响数据恢复或主从同步的效率。
集群模式:Redis集群是一个由多个主从节点群组成的分布式服务器群,它具有复制、高可用和分片特性。
Redis Cluster 将所有数据划分为 16384 个 slots(槽位),每个节点负责其中一部分槽位。槽位的信息存储于每个节点中。当 Redis Cluster 的客户端来连接集群时,它也会得到一份集群的槽位配置信息并将其缓存在客户端本地。这样当客户端要查找某个 key 时,可以直接定位到目标节点。同时因为槽位的信息可能会存在客户端与服务器不一致的情况,还需要纠正机制来实现槽位信息的校验调整。
参考:https://blog.csdn.net/qq_42947952/article/details/127013562