Redis面试
1. Redis应用场景
-
缓存(数据查询、短连接、新闻内容、商品内容等等) 全页缓存(FPC)
-
聊天室的在线好友列表
-
任务队列。(秒杀、抢购、12306等等) 队列
-
应用排行榜
-
网站访问统计
-
签到功能
-
数据过期处理(可以精确到毫秒
-
分布式集群架构中的session分离
-
会话缓存(Session Cache)
-
排行榜/计数器
-
发布/订阅
2. Memcache 与 Redis 的区别都有哪些?
- 存储方式不同,Memcache 是把数据全部存在内存中,数据不能超过内存的大小,断电后数据库会挂掉。Redis 有部分存在硬盘上,这样能保证数据的持久性。
- 数据支持的类型不同 memcahe 对数据类型支持相对简单,redis 有复杂的数据类型。
- 使用底层模型不同 它们之间底层实现方式 以及与客户端之间通信的应用协议不一样。Redis 直接自己构建了 VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
- 支持的 value 大小不一样 redis 最大可以达到 1GB,而 memcache 只有 1MB。
3. Redis 用过 RedisNX 吗?Redis 有哪几种数据结构?
反正我是不知道 redisnx 是什么,度娘也不清楚,如果面试中问道自己没有接触过或者没有听过的技术可以直接大胆的告诉他,没有接触过,或者没有听过。
Redis 的数据结构有五种,分别是:
- String——字符串
String 数据结构是简单的 key-value 类型,value 不仅可以是 String,也可以是数字(当数字类型用 Long 可以表示的时候 encoding 就是整型,其他都存储在 sdshdr 当做字符串)。 - Hash——字典
在 Redis中,我们经常将一些结构化的信息打包成 hashmap,在客户端序列化后存储为一个字符串的值(一般是 JSON 格式),比如用户的昵称、年龄、性别、积分等。 - List——列表
List 说白了就是链表(redis 使用双端链表实现的 List),相信学过数据结构知识的人都应该能理解其结构。 - Set——集合
Set 就是一个集合,集合的概念就是一堆不重复值的组合。利用 Redis 提供的 Set 数据结构,可以存储一些集合性的数据。 - Sorted Set——有序集合
**和 Set 相比,Sorted Sets 是将 Set 中的元素增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列,
- 带有权重的元素,比如一个游戏的用户得分排行榜
- 比较复杂的数据结构,一般用到的场景不算太多
4. Redis客户端操作使用方式
- String格式操作方法
存储: set key value
列: set username abc
获取: get key
列: get username
删除:del key
列: del username
- 哈希类型hash操作方法
说白了,就是 HashMap<Key1,HashMap<Key2,Value>>
第一个Key1 是redis去找, 第二个Key2是hash的存储结构中的键, 通过键key2可以去找值Value。或者是通过Key1 查出所有的hash键值对。
map格式
存储:hset key field value
key 主要就是区分不同map, 相当于java对象的意思.不要弄成java中的key了
而域(field)就是map中的 key value就是值了
hset hash_day1 password 123
获取:
hget hash_day1 username 获取指定的field
hgetall hash_day1 获取所有的key 和value
删除:
hdel hash_day1 username //删除指定的field
- list操作方法
只支持linkedlist格式 前后
添加:
1. lpush key value: 从开头插入
lpush linked_day1 a1
2. rpush key value:从最后插入
rpush linked_day1 a3
获取:
lrange key start end :范围获取
lrange linked_day1 0 -1 (从开始到最后 0 到 -1)
删除:
lpop key: 删除列表最左边的元素,并将元素返回
lpop linked_day1
rpop key: 删除列表最右边的元素,并将元素返回
rpop linked_day1
- 集合类型 set 操作方法
存储: sadd key value
sadd set_day1 a
获取:smembers set_day1 (全部value)
删除: srem key value
srem set_day1t abc1
- 有序集合类型 sortedset 操作方法
不允许重复元素,且元素有顺序.每个元素都会关联一个double类型的分数,redis正是通过分数来为集合中的成员进行从小到大的排序。
存储:zadd key score value
zadd sort_list 3 abc3
zadd sort_list 1 abc1
zadd sort_list 2 abc2
获取:zrange key start end [withscores]
zrange mysort 0 -1 (只显示 value)
zrange mysort 0 -1 withscores (分数也显示)
删除:zrem key value
zrem mysort lisi
-
通用命令
-
keys * : 查询数据库中所有的键 (相当于数据库中所有的表)
-
type key : 获取数据库中键对应的类型 (相当于表的类型)
-
del key: 删除指定数据库的key 和此key下所有value (相当于删除表)
-
flushall 清空Redis服务全部数据
5. Redis持久化
redis是一个内存数据库,当redis服务器重启,或者电脑重启,数据会丢失,我们可以将redis内存中的数据持久化保存到硬盘的文件中。这就不会丢失数据了
一般情况下 都不要设置 默认就行了 因为redis 主要是用于数据缓存 的 实际的数据都是存在 数据库中的.保存的内容都保存在appendonly.aof这个文件中 (一开始是没有此文件的 当持久化后会自动生成 )
使用持久化那么 启动服务器的时候必须指定配置文件 否则 无效
语法 redis-server.exe redis.windows.conf
持久化有两种方式
第一种
- RDB:默认方式,不需要进行配置,默认就使用这种机制
在默认配置中,Redis将内存数据库快照保存在名字为dump.rdb的二进制文件中。
可以配置持久化策略:save N M,让redis在“N秒内至少有M个改动”才会触发一次rdb持久化操作。
例如redis的默认策略有:
save 900 1 – 900秒内至少有一个改动 900s后 就会持久化。
save 300 10 – 300秒内至少有10个改动 300s 后就会持久化。
save 60 10000 – 60秒内至少有10000个改动 60s 后 就会持久化。
还可以手动执行命令生成RDB快照,进入redis客户端执行命令save或bgsave可以生成dump.rdb文件,
每次命令执行都会将所有redis内存快照到一个新的rdb文件里,并覆盖原有rdb快照文件。
- bgsave写时复制的(COW)机制
Redis借助了linux系统的写时复制(Copy-On-Write)技术,在生成快照的同时,仍然可以接收命令处理数据。简单来说,bgsave进程是由主进程fork生成的子进程,可以共享进程程所有的内存数据。bgsave进程中的线程运行后,开始读取主进程的内存数据,也就是redis的内存数据,将内存数据写入到dump.rdb文件中。此时,如果主进程处理的命令都是读操作,则bgsave线程不受影响。如果主进程处理了写操作,则会对该命令操作的数据复制一份,生成副本,bgsave进程中的线程会把这个副本写入到dump.rdb文件中,而在这个过程中,主进程仍可执行命令。
save于bgsave对比:
配置自动生成rdb文件后台使用的是bgsave方式。
RDB方式优缺点:
缺点:
RDB每次持久化需要将所有内存数据写入文件,然后替换原有文件,当内存数据量很大的时候,频繁的生成快照会很耗性能。
如果将生成快照的策略设置的时间间隔很大,会导致redis宕机的时候丢失过多的数据。
优点:
应为dump.rdb文件是二进制文件,所以当redis服务崩溃恢复的时候,能很快的将文件数据恢复到内存之中。
第二种:
AOF(Append-Only File)
为解决RDB方式丢失数据的问题,从1.1版本开始,redis增加了一种更加可靠的方式:AOF持久化方式。
在使用AOF方式时,redis每执行一次修改数据命令,都会将该命令追加到appendonly.aof文件中(先写入os cache,然后通过fsync刷盘)。
可以通过修改配置文件来使用AOF:
#appendonly yes
redis重启的时候,会重放appendonly.aof中的命令恢复数据。
AOF可以配置三种刷盘策略:
- appendfsync always:每次执行写命令都会刷盘,非常慢,也非常安全。
- appendfsync everysec:每秒刷盘一次,兼顾性能和安全。
- appendfsync no:将刷盘操作交给系统,很快,不安全。
推荐使用everysec,该策略下,最多会丢1秒的数据。
AOF重写:
应为appendonly.aof文件中存储的是执行命令,所以会产生很多没用的命令,因此,redis会定期根据最新的内存数据生成新的aof文件。
如下两个配置可以控制aop文件重写的频率:
#auto‐aof‐rewrite‐min‐size 64mb: – aof文件至少达到了64m才会触发重写
#auto‐aof‐rewrite‐percentage 100: – 距离上次重写增长了100%才会再次触发重写
AOF也可以手动触发重写:bgrewriteof
注意,AOF重写redis会fork出一个子进程去做(与bgsave命令类似),不会对redis正常命令处理有太多影响。
生产环境可以都启用,redis启动时如果既有rdb文件又有aof文件则优先选择aof文件恢复数据,因为aof一般来说数据更全一点。
6. Redis 4.0混合持久化
重启 Redis 时,我们很少使用 RDB来恢复内存状态,因为会丢失大量数据。我们通常使用 AOF 日志重放,但是重放 AOF 日志性能相对 RDB来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很长的时间。 Redis 4.0 为了解决这个问题,带来了一个新的持久化选项——混合持久化。
通过如下配置可以开启混合持久化(必须先开启aof):
#aof‐use‐rdb‐preamble yes
如果开启了混合持久化,AOF在重写时,不再是单纯将内存数据转换为RESP命令写入AOF文件,而是将重写这一刻之前的内存做RDB快照处理,并且将RDB快照内容和增量的AOF修改内存数据的命令存在一起,都写入新的AOF文件,新的文件一开始不叫appendonly.aof,等到重写完新的AOF文件才会进行改名,覆盖原有的AOF文件,完成新旧两个AOF文件的替换。
于是在 Redis 重启的时候,可以先加载 RDB 的内容,然后再重放增量 AOF 日志就可以完全替代之前的AOF 全量文件重放,因此重启效率大幅得到提升。
混合持久化AOF文件结构如下:
7. Redis数据备份策略
- 写crontab定时调度脚本,每小时都copy一份rdb或aof的备份到一个目录中去,仅仅保留最近48小时的备份。
- 每天都保留一份当日的数据备份到一个目录中去,可以保留最近1个月的备份。
- 每次copy备份的时候,都把太旧的备份给删了。
- 每天晚上将当前机器上的备份复制一份到其他机器上,以防机器损坏。
8. 设置缓存文件清理策略
如果内存到达了指定的上限,还要往redis里面添加更多的缓存内容,需要设置清理内容的策略:
默认为0,没有指定最大缓存,如果有新的数据添加,超过最大内存,则会使redis崩溃,
大概在520~600行左右 # maxmemory 下面配置
maxmemory 314572800 相当于 300mb
设置maxmemory之后,就需要配合 缓存数据回收策略。 当 maxmemory满了就会触发进行清理
下面为redis官网上的几种清理策略:
volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰 (常用)
allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
no-enviction(驱逐):禁止驱逐数据
maxmemory-policy allkeys-lru
9. Redis参数配置
10. Redis 服务器的各种信息和统计数值(优化)
info (打印全部)
info clients (客户端的信息)
info server (服务端的信息)
info Memory (内存使用情况)
以上Memory参数主要看
used_memory_rss_human(当前使用了多少内存) < maxmemory_human (最大Redis内存)
其他的都不最重要
如果 项目上线了一段时间 used_memory_rss_human 离maxmemory_human 比例差距很大的话,那么将 maxmemory调小 比 used_memory_rss_human多三分之一就行。
10.1 Redis优化总结:
- 专注Redis存储和查询, 减少全部查询,减少删除大集合。
- Redis存储 尽量都是 json 格式的字符串, 如果是对象,那么就使用 Json转换工具进行转换,这样方便以后的获取以及使用。
- 在Redis 中, 没有 修改, 想要修改 ,就直接覆盖就行了。
- 在Redis中,提供了部分的删除, 但是实际上只能满足部分需求,很多时候 , 删除其实就是覆盖。
- 尽量使用 字符串结构,就别使用集合结构, 因为字符串配合json无敌, 处理好的话比集合都快。 注意:每个key 的数据量不要太大,如果数据量大的时候,将数据进行分段,将值拆分多个存储到多个key里。
- 避免操作大集合的命令:如果命令处理频率过低导致延迟时间增加,这可能是因为使用了高时间复杂度的命令操作导致 比如: 交集 并集…,这意味着每个命令从集合中获取数据的时间增大。 所以减少使用高时间复杂的命令,能显著的提高的Redis的性能
还有就是 map list set … 这些数据结构 尽量进行分段 就是不要把数据都存在 一个里
比如: 将1000条数据 分别存入 10个map里
每个集合数据量不要大于1000条
在java中是提供批量操作的管道命令 (用法和使用Jedis一样 通过pipe.xxx )
Jedis jedis= JedisPool.Jedis();
Pipeline pipe = jedis.pipelined(); //管道 批量处理
管道命令把几个命令合并一起执行,从而减少因网络开销引起的延迟问题。因为10个命令单独发送到服务端会引起10次网络延迟开销,使用管道会一次性把执行结果返回,仅需要一次网络延迟开销。Redis本身支持管道命令,大多数客户端也支持,倘若当前实例延迟很明显,那么使用管道去降低延迟是非常有效的。
鉴于Pipepining发送命令的特性,Redis服务器是以队列来存储准备执行的命令,而队列是存放在有限的内存中的,所以不宜一次性发送过多的命令。如果需要大量的命令,可分批进行,效率不会相差太远滴,总好过内存溢出嘛~~
11. Redis是单线程的还是多线程的?
采用单线程,有两个方面原因:
如果单单是指业务部分的命令操作, 那么它是单线程的;
如果指的是整个Redis 的话, 那么它是多线程的。
4.0 版本的时候, 引入了多线程异步处理一些耗时较长的任务,例如异步删除命令unlink;
6.0 版本的时候, 针对网络模型引入了多线程,进一步提高了对于多核CPU的利用率。
采用单线程有以下几个原因:
- 抛开持久化不谈,Redis是 纯内存操作, 速度非常快, 采用多线程并不会带来巨大的提升, 因为 Redis 的性能瓶颈是网络延迟而不是速度问题;
- 多线程会导致过多的上下文切换,带来不必要的开销。
- Redis 采用多线程的话, 必然会带来安全问题,必然会引入线程锁这样的安全手段,复杂度高, 性能也会大打折扣。
- 核心是基于非阻塞的IO多路复用机制。
12. Redis事务
12.1 Redis中与事务相关的命令
- 开启事务:MULTI 类似 MySQL 中的 begin transaction。
- 执行事务:EXEC 类似 MySQL 中的 commit。
- 取消事务(回滚事务):DISCARD 类似 MySQL 中的 rowback。
12.2 Redis 事务有哪些特性
-
- Redis 事务中的所有命令都会存放在队列中按序执行。
-
- Redis 事务中的所有命令在没有提交(exec)之前,都不会执行,所以也就不存在关系型数据库中经常出现的脏读,不可重复读,幻读等并发操作的问题。
-
- Redis 事务不保证原子性,命令如果本身的语法没有问题,只是在执行的过程中出错,不影响其他命令的执行。
redis中是用multi开启事务,用exec执行命令。如果在exec之前你不想执行事务了,可以用discard取消当前事务。
12.3 Redis 支不支持事务回滚? 为什么?
总结:
多数事务失败是由语法错误或者数据结构类型错误导致的,语法错误说明在命令入队前就进行检测的,而类型错误是在执行时检测的,Redis为提升性能而采用这种简单的事务,这是不同于关系型数据库的,特别要注意区分
12.4 打破这种不回滚的规则,怎么做?
使用watch 和unwatch 命令实现
上面我们讲到redis是的事务是不支持回滚的,但是我们一定要让它回滚怎么办呢?这就需要用的watch命令了。
watch使用要注意:watch在mutil命令之前使用.
watch的作用是:监控一个值是否发生变化,如果没发生改变,它会执行事务队列中的命令,提交事务;如果发生变化,将不会执行事务中的任何命令,同时事务回滚。 最后无论是否回滚,Redis都会取消执行事务前的WATCH命令。
这么说不太好理解,我们画图表示一下:
案例:
13.缓存雪崩、缓存穿透、缓存击穿
大前提
当我们使用缓存时,目标通常有两个:第一,提升响应效率和并发量;第二,减轻数据库的压力。
而本文中所提到的这三种场景:缓存穿透、缓存雪崩和缓存击穿的发生,都是因为在某些特殊情况下,缓存失去了预期的功能所致。
当缓存失效或没有抵挡住流量,流量直接涌入到数据库,在高并发的情况下,可能直接击垮数据库,导致整个系统崩溃。
这就是我们需要知道的大前提,而缓存穿透、缓存雪崩和缓存击穿,只不过是在这个大前提下的不同场景的细分场景而已。
13.1 缓存穿透(cache penetration)
是用户访问的数据既不在缓存当中,也不在数据库中。出于容错的考虑,如果从底层数据库查询不到数据,则不写入缓存。这就导致每次请求都会到底层数据库进行查询,缓存也失去了意义。当高并发或有人利用不存在的Key频繁攻击时,数据库的压力骤增,甚至崩溃,这就是缓存穿透问题。
缓存穿透发生的场景一般有两类:
原来数据是存在的,但由于某些原因(误删除、主动清理等)在缓存和数据库层面被删除了,但前端或前置的应用程序依旧保有这些数据;
恶意攻击行为,利用不存在的Key或者恶意尝试导致产生大量不存在的业务数据请求。
缓存穿透通常有四种解决方案,我们逐一介绍分析。
方案一:缓存空值(null)或默认值
分析业务请求,如果是正常业务请求时发生缓存穿透现象,可针对相应的业务数据,在数据库查询不存在时,将其缓存为空值(null)或默认值。需要注意的是,针对空值的缓存失效时间不宜过长,一般设置为5分钟之内。当数据库被写入或更新该key的新数据时,缓存必须同时被刷新,避免数据不一致。
方案二:业务逻辑前置校验
在业务请求的入口处进行数据合法性校验,检查请求参数是否合理、是否包含非法值、是否恶意请求等,提前有效阻断非法请求。比如,根据年龄查询时,请求的年龄为-10岁,这显然是不合法的请求参数,直接在参数校验时进行判断返回。
方案三:使用布隆过滤器请求白名单
在写入数据时,使用布隆过滤器进行标记(相当于设置白名单),业务请求发现缓存中无对应数据时,可先通过查询布隆过滤器判断数据是否在白名单内,如果不在白名单内,则直接返回空或失败。
方案四:用户黑名单限制
当发生异常情况时,实时监控访问的对象和数据,分析用户行为,针对故意请求、爬虫或攻击者,进行特定用户的限制;
当然,可能针对缓存穿透的情况,也有可能是其他的原因引起,可以针对具体情况,采用对应的措施。
13.2 缓存雪崩
在使用缓存时,通常会对缓存设置过期时间,一方面目的是保持缓存与数据库数据的一致性,另一方面是减少冷缓存占用过多的内存空间。
当缓存中大量热点缓存采用了相同的失效时间,就会导致缓存在某一个时刻同时失效,请求全部转发到数据库,从而导致数据库压力骤增,甚至宕机。从而形成一系列的连锁反应,造成系统崩溃等情况,这就是缓存雪崩(Cache Avalanche)。
缓存雪崩
上面讲到的是热点key同时失效的场景,另外就是由于某些原因导致缓存服务宕机、挂掉或不响应,也同样会导致流量直接转移到数据库。
所以,缓存雪崩的场景通常有两个:
- 大量热点key同时过期;
- 缓存服务故障;
缓存雪崩的解决方案:
- 通常的解决方案是将key的过期时间后面加上一个随机数(比如随机1-5分钟),让key均匀的失效。
- 考虑用队列或者锁的方式,保证缓存单线程写,但这种方案可能会影响并发量。
- 热点数据可以考虑不失效,后台异步更新缓存,适用于不严格要求缓存一致性的场景。
- 双key策略,主key设置过期时间,备key不设置过期时间,当主key失效时,直接返回备key值。
- 构建缓存高可用集群(针对缓存服务故障情况)。
- 当缓存雪崩发生时,服务熔断、限流、降级等措施保障。
13.3 缓存击穿
缓存雪崩是指大量热点key同时失效的情况,如果是单个热点key,在不停的扛着大并发,在这个key失效的瞬间,持续的大并发请求就会击破缓存,直接请求到数据库,好像蛮力击穿一样。这种情况就是缓存击穿(Cache Breakdown)。
缓存击穿
从定义上可以看出,缓存击穿和缓存雪崩很类似,只不过是缓存击穿是一个热点key失效,而缓存雪崩是大量热点key失效。因此,可以将缓存击穿看作是缓存雪崩的一个子集。
缓存击穿的解决方案:
使用互斥锁(Mutex Key),只让一个线程构建缓存,其他线程等待构建缓存执行完毕,重新从缓存中获取数据。单机通过synchronized或lock来处理,分布式环境采用分布式锁。
热点数据不设置过期时间,后台异步更新缓存,适用于不严格要求缓存一致性的场景。
”提前“使用互斥锁(Mutex Key):在value内部设置一个比缓存(Redis)过期时间短的过期时间标识,当异步线程发现该值快过期时,马上延长内置的这个时间,并重新从数据库加载数据,设置到缓存中去。
小结
本文介绍了在使用缓存时经常会遇到的三种异常情况:缓存穿透、缓存雪崩和缓存击穿。
三种异常情况从根本上来说都是因为本应该访问缓存的,但是缓存不存在或服务异常,导致流量直接进入了数据库层面。
其中缓存雪崩和缓存击穿是因为数据不存在(或服务异常获取不到),导致大量请求访问数据库,从而导致数据库压力骤增,甚至崩溃。
而缓存穿透则是由于数据本身就不存在,导致缓存没有进行数据缓存,流量进入数据库层。
针对不同的缓存异常场景,可选择不同的方案来进行处理。当然,除了上述方案,我们还可以限流、降级、熔断等服务层的措施,也可以考虑数据库层是否可以进行横向扩展,当缓存异常发生时,确保数据库能够抗住流量,不至于让整个系统崩溃。
14. redis中如何保证缓存数据的一致性 ?
方案一:先更新数据库,在删除缓存场景
一改一查场景:
当有两个线程A、B,线程A先去将数据库的值修改为allen,然后需要去删除redis中的缓存,当线程B去读取缓存时,线程A已经完成delete操作时,缓存不命中,需要去查询数据库,然后在更新缓存,数据一致性;如果线程A没有完成delete操作,线程B直接命中,返回的数据与数据库中的数据不一致,可能会短暂出现数据不一致情况,但最终都会一致。推荐
存在的问题:当数据过期或者初始化时,会出现数据不一致情况,也就是线程B从数据库中,查询到数据为tony,然后线程A将tony修改为了allen,然后去删除redis中的数据,然后线程B将读到的tony,更新到了数据库中,出现了数据不一致问题。
解决方案:对于不过期的数据我们要在上线的时候做好数据的预热,保证缓存命中。对于存在过期的数据,因为有过期时间,只会在特定的时间段内数据不一致,下次数据过期后,可以恢复,对于实时性要求不高时,可以接受。
两次修改场景:
当有两个线程A、B,线程A去修改数据库中的值改为allen,然后出现网络波动,线程B将数库中的值修改为了Mike,然后两个线程都会删除缓存,保证数据一致性。
方式二: 延迟双删策略
意思大概就是: 先删除缓存数据, 再更新数据库操作,等待 500 ms,在进行删除缓存操作。
为什么要等待500ms呢?
举个例子:
方式三:最佳实现,数据异步同步
Canal:基于数据库增量日志解析,提供增量数据订阅和消费https://github.com/alibaba/canal
mysql会将操作记录在Binary log日志中,通过canal去监听数据库日志二进制文件,解析log日志,同步到redis中进行增删改操作。
canal的工作原理:canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议;MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal );canal 解析 binary log 对象(原始为 byte 流)。
参考:
原文链接:https://blog.csdn.net/nianqrzhanghw/article/details/121163780
原文链接:https://blog.csdn.net/weixin_45203607/article/details/120235597
原文链接:https://blog.csdn.net/qq_45637260/article/details/125866738