一、Redis基本概念
- 面试官心理: 靠!手上活都没干完又叫我过来面试,这不耽误我事么,今儿又得加班补活了........咦,这小伙子简历不错啊,先考考它
Redis
.......... - 面试官: 谈谈你对
Redis
的理解? - 我:
Redis
是ANSI C
语言编写的一个基于内存的高性能键值对(key-value
)的NoSQL
数据库,一般用于架设在Java程序与数据库之间用作缓存层,为了防止DB磁盘IO效率过低造成的请求阻塞、响应缓慢等问题,用来弥补DB与Java程序之间的性能差距,同时,也可以在DB吞吐跟不上系统并发量时,避免请求直接落入DB从而起到保护DB的作用。 - 而
Redis
一般除了缓存DB数据之外还可以利用它丰富的数据类型及指令来实现一些其他功能,比如:计数器、用户在线状态、排行榜、session
存储等,同时Redis
的性能也非常可观,通过官方给出的数据显示能够达到10w/s的QPS处理,但是在生产环境的实测结果大概读取QPS在7-9w/s,写入QPS在6-8w/s左右(注:与机器性能也有关),同时Redis
也提供事务、持久化、高可用等一些机制的支持。
二、Redis基本数据类型与常用指令
- 面试官: 刚刚听你提到了可以利用它丰富的数据类型及指令来实现一些其他功能,那你跟我讲讲
Redis
的一些常用指令。 - 我:
Redis
常用的一些命令的话一般是都是对于基本数据类型的操作指令以及一些全局指令.....叭啦叭啦叭......,如下:
命令 | 作用 |
---|---|
keys * |
返回所有键(keys 还能用来搜索,比如keys h* :搜索所有以h开头的键) |
dbsize |
返回键数量,如果存在大量键,线上禁止使用此指令 |
exists key |
检查键是否存在,存在返回 1,不存在返回 0 |
del key |
删除键,返回删除键个数,删除不存在键返回 0 |
ttl key |
查看键存活时间,返回键剩余过期时间,不存在返回-1 |
expire key seconds |
设置过期时间(单位:s),成功返回1,失败返回0 |
expireat key timestamp |
设置key 在某个时间戳(精确到秒)之后过期 |
pexpire key milliseconds |
设置过期时间(单位:ms),成功返回1,失败返回0 |
persist key |
去掉过期时间 |
monitor |
实时监听并返回Redis 服务器接收到的所有请求信息 |
shutdown |
把数据同步保存到磁盘上,并关闭Redis 服务 |
info |
查看当前Redis 节点信息 |
....... | ....... |
当然了,一般也是记得一些常用的命令,但是 更多命令参考:Redis命令大全,因为Redis 命令和JVM参数一样,只要记得可以这样做就行了,但是具体的可以去参考相关文档资料。 |
- 面试官: 嗯嗯,不错,那再接着讲讲
Redis
的基本数据类型以及你是在项目中怎么使用它们的吧! - 我:
Redis
数据类型在之前是五种,但是现在的版本中存在九种,分别为:字符串(strings/string
)、散列(hashes/hash
)、列表(lists/list
)、集合(sets/set
)、有序集合(sorted sets/zset
)以及后续的四种数据类型:bitmaps、hyperloglogs
、地理空间(geospatial
)、消息(Streams
),不过无论是哪种数据类型Redis
都不会直接将它放在内存中存储,而是转而内部使用RedisObject
来存储以及表示所有类型的key-value
(说着说着我拿出了纸和笔,给面试官画了一张图):
Redis
内部使用一个RedisObject
对象来表示所有的key
和value
,RedisObject
最主要的信息如上图所示:type
表示一个value
对象具体是何种数据类型,encoding
是不同数据类型在Redis
内部的存储方式。比如:type=string
表示value
存储的是一个普通字符串,那么encoding
可以是raw
或者int
,而关于其他数据类型的内部编码实现我顿时再拿起笔chua~ chua~ chua
:
- 我接着回答: 下面我再简单讲讲
Redis
的基本数据类型以及它们的应用场景:
类型 | 描述 | 特性 | 场景 |
---|---|---|---|
string |
二进制安全 | 可以存储任何元素(数字、字符、音视频、图片、对象.....) | 计数器、分布式锁、字符缓存、分布式ID生成、session 共享、秒杀token 、IP限流等 |
hash |
键值对存储,类似于Map集合 | 适合存储对象,可以将对象属性一个个存储,更新时也可以更新单个属性,操作某一个字段 | 对象缓存、购物车等 |
list |
双向链表 | 增删快 | 栈、队列、有限集合、消息队列、消息推送、阻塞队列等 |
set |
元素不能重复,每次获取无序 | 添加、删除、查找的复杂度都是O(1),提供了求交集、并集、差集的操作 | 抽奖活动、朋友圈点赞、用户(微博好友)关注、相关关注、共同关注、好友推荐(可能认识的人)等 |
sorted set |
有序集合,每个元素有一个对应的分数,不允许元素重复 | 基于分数进行排序,如果分数相等,以key值的 ascii 值进行排序 | 商品评价标签(好评、中评、差评等)、排行榜等 |
bitmaps |
Bitmaps 是一个字节由 8 个二进制位组成 |
在字符串类型上面定义的位操作 | 在线用户统计、用户访问统计、用户点击统计等 |
hyperloglog |
Redis2.8.9 版本添加了 HyperLogLog 结构。Redis HyperLogLog 是用来做基数统计的算法。 |
用于进行基数统计,不是集合,不保存数据,只记录数量而不是具体数据 | 统计独立UV等 |
geospatial |
Redis3.2 版本新增的数据类型:GEO 对地理位置的支持 |
以将用户给定的地理位置信息储存起来, 并对这些信息进行操作 | 地理位置计算 |
stream |
Redis5.0 之后新增的数据类型 |
支持发布订阅,一对多消费 | 消息队列 |
PS:
HyperLogLog
的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的。在Redis
里面,每个HyperLogLog
键只需要花费12 KB
内存,就可以计算接近2^64
个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。但是,因为HyperLogLog
只会根据输入元素来计算基数,而不会储存输入元素本身,所以HyperLogLog
不能像集合那样,返回输入的各个元素(核心是基数估算算法,最终数值存在一定误差误差范围:基数估计的结果是一个带有0.81%
标准错误的近似值,耗空间极小,每个hyperloglog key
占用了12K的内存用于标记基数,pfadd
命令不是一次性分配12K
内存使用,会随着基数的增加内存逐渐增大,Pfmerge
命令合并后占用的存储空间为12K
,无论合并之前数据量多少)
三、Redis缓存及一致性、雪崩、击穿与穿透问题
-
面试官提问: 那么你们在使用
Redis
做为缓存层的时候是怎么通过Java操作Redis
的呢? -
我的心理: 这问题不是送命题吗.....
-
我: Java操作
Redis
的客户端有很多,比如springData
中的RedisTemplate
,也有SpringCache
集成Redis
后的注解形式,当然也会有一些Jedis、Lettuce、Redisson
等等,而我们使用的是Lettuce
以及Redisson........
-
面试官提问: 那你们在使用
Redis
作为缓存的时候有没有遇到什么问题呢? -
我: 咳咳,是的,确实遇到了以及考虑到了一些问题,比如缓存一致性、雪崩、穿透与击穿,关于
Redis
与MySQL
之间的数据一致性问题其实也考虑过很多方案,比如先删后改,延时双删等等很多方案,但是在高并发情况下还是会造成数据的不一致性,所以关于DB与缓存之间的强一致性一定要保证的话那么就对于这部分数据不要做缓存,操作直接走DB,但是如果这个数据比较热点的话那么还是会给DB造成很大的压力,所以在我们的项目中还是采用先删再改+过期的方案来做的,虽然也会存在数据的不一致,但是勉强也能接受,因为毕竟使用缓存访问快的同时也能减轻DB压力,而且本身采用缓存就需要接受一定的数据延迟性和短暂的不一致性,我们只能采取合适的策略来降低缓存和数据库间数据不一致的概率,而无法保证两者间的强一致性。合适的策略包括合适的缓存更新策略,合适的缓存淘汰策略,更新数据库后及时更新缓存、缓存失败时增加重试机制等。 -
面试官话锋一转: 打断一下,你刚刚提到了使用缓存能让访问变快,那么你能不能讲讲
Redis
为什么快呢? -
我的心理: 好家伙,这一手来的我猝不及防......
-
硬着头发回答:
Redis
快的原因嘛其实可以从多个维度来看待:- 一、
Redis
完全基于内存 - 二、
Redis
整个结构类似于HashMap
,查找和操作复杂度为O(1)
,不需要和MySQL
查找数据一样需要产生随机磁盘IO或者全表 - 三、
Redis
对于客户端的处理是单线程的,采用单线程处理所有客户端请求,避免了多线程的上下文切换和线程竞争造成的开销 - 四、底层采用
select/epoll
多路复用的高效非阻塞IO模型 - 五、客户端通信协议采用
RESP
,简单易读,避免了复杂请求的解析开销
- 一、
-
面试官露出姨父般的慈笑: 嗯嗯,还不错,那你继续谈谈刚刚的缓存雪崩、穿透与击穿的问题吧
-
我: 好的,先说缓存雪崩吧,缓存雪崩造成的原因是因为我们在做缓存时为了保证内存利用率,一般在写入数据时都会给定一个过期时间,而就是因为过期时间的设置有可能导致大量的热点key在同一时间内全部失效,此时来了大量请求访问这些key,而
Redis
中却没有这些数据,从而导致所有请求直接落入DB查询,造成DB出现瓶颈或者直接被打宕导致雪崩情况的发生。关于解决方案的的话也可以从多个维度来考虑:- 一、设置热点数据永不过期,避免热点数据的失效导致大量的相同请求落入DB
- 二、错开过期时间的设置,根据业务以及线上情况合理的设置失效时间
- 三、使用分布式锁或者MQ队列使得请求串行化,从而避免同一时间请求大量落入DB(性能会受到很大的影响)
-
面试官: 那缓存穿透呢?指的是什么?又该怎么解决?
-
我喝了口水接着回答: 缓存穿透这个问题是由于请求参数不合理导致的,比如对外暴露了一个接口
getUser?userID=xxx
,而数据库中的userID
是从1开始的,当有黑客通过这个接口携带不存在的ID请求时,比如:getUser?userID=-1
,请求会先来到Redis
中查询缓存,但是发现没有对应的数据从而转向DB查询,但是DB中也无此值, 所以也无法写入数据到缓存,而黑客就通过这一点利用“肉鸡”等手段疯狂请求这个接口,导致出现大量Redis
不存在数据的请求落入DB,从而导致DB出现瓶颈或者直接被打宕机,整个系统陷入瘫痪。 -
面试官: 嗯,那又该如果避免这种情况呢?
-
我: 解决方案也有好几种呢:
- 一、做IP限流与黑名单,避免同一IP一瞬间发送大量请求
- 二、对于请求做非法校验,对于携带非法参数的请求直接过滤
- 三、对于DB中查询不存在的数据写入
Redis
中“Not Data”
并设置短暂的过期时间,下次请求能够直接被拦截在Redis
而不会落入DB - 四、布隆过滤器
-
面试官: 那接下来的缓存击穿呢?又是怎么回事?怎么解决?
-
我: 这个简单,缓存击穿和缓存雪崩有点类似,都是由于请求的key