redis秋招面试总结
引言
在Web应用发展的初期,那时关系型数据库受到了较为广泛的关注和应用,原因是因为那时候Web站点基本上访问和并发不高、交互也较少。而在后来,随着访问量的提升,使用关系型数据库的Web站点多多少少都开始在性能上出现了一些瓶颈,而瓶颈的源头一般是在磁盘的I/O上。而随着互联网技术的进一步发展,各种类型的应用层出不穷,这导致在当今云计算、大数据盛行的时代,对性能有了更多的需求,主要体现在以下四个方面:
- 低延迟的读写速度:应用快速地反应能极大地提升用户的满意度
- 支撑海量的数据和流量:对于搜索这样大型应用而言,需要利用PB级别的数据和能应对百万级的流量
- 大规模集群的管理:系统管理员希望分布式应用能更简单的部署和管理
- 庞大运营成本的考量:IT部门希望在硬件成本、软件成本和人力成本能够有大幅度地降低
为了克服这一问题,NoSQL应运而生,它同时具备了高性能、可扩展性强、高可用等优点,受到广泛开发人员和仓库管理人员的青睐。
Redis是什么
Redis是现在最受欢迎的NoSQL数据库之一,Redis是一个使用ANSI C编写的开源、包含多种数据结构、支持网络、基于内存、可选持久性的键值对存储数据库,其具备如下特性:
- 基于内存运行,性能高效
- 支持分布式,理论上可以无限扩展
- key-value存储系统
- 开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API
相比于其他数据库类型,Redis具备的特点是:
- C/S通讯模型
- 单进程单线程模型
- 丰富的数据类型
- 操作具有原子性
- 持久化
- 高并发读写
- 支持lua脚本
哪些大厂在使用Redis?
Redis的应用场景有哪些?
Redis 的应用场景包括:缓存系统(“热点”数据:高频读、低频写)、计数器、消息队列系统、排行榜、社交网络和实时系统。
Redis的数据类型及主要特性
Redis提供的数据类型主要分为5种自有类型和一种自定义类型,这5种自有类型包括:String类型、哈希类型、列表类型、集合类型和顺序集合类型。
了解这5类数据类型用法和背后的数据结构
string类型:
它是一个二进制安全的字符串,意味着它不仅能够存储字符串、还能存储图片、视频等多种类型, 最大长度支持512M。
对每种数据类型,Redis都提供了丰富的操作命令,如:
- GET/MGET
- SET/SETEX/MSET/MSETNX
- INCR/DECR
- GETSET
- DEL
哈希类型:
该类型是由field和关联的value组成的map。其中,field和value都是字符串类型的。
Hash的操作命令如下:
- HGET/HMGET/HGETALL
- HSET/HMSET/HSETNX
- HEXISTS/HLEN
- HKEYS/HDEL
- HVALS
列表类型:
该类型是一个插入顺序排序的字符串元素集合, 基于双链表实现。
List的操作命令如下:
- LPUSH/LPUSHX/LPOP/RPUSH/RPUSHX/RPOP/LINSERT/LSET
- LINDEX/LRANGE
- LLEN/LTRIM
集合类型:
Set类型是一种无顺序集合, 它和List类型最大的区别是:集合中的元素没有顺序, 且元素是唯一的。
Set类型的底层是通过哈希表实现的,其操作命令为:
- SADD/SPOP/SMOVE/SCARD
- SINTER/SDIFF/SDIFFSTORE/SUNION
Set类型主要应用于:在某些场景,如社交场景中,通过交集、并集和差集运算,通过Set类型可以非常方便地查找共同好友、共同关注和共同偏好等社交关系
顺序集合类型:
ZSet是一种有序集合类型,每个元素都会关联一个double类型的分数权值,通过这个权值来为集合中的成员进行从小到大的排序。与Set类型一样,其底层也是通过哈希表实现的。
ZSet命令:
- ZADD/ZPOP/ZMOVE/ZCARD/ZCOUNT
- ZINTER/ZDIFF/ZDIFFSTORE/ZUNION
Redis的数据结构
Redis的数据结构如下图所示:
关于上表中的部分释义:
- 压缩列表是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表项,并且每个列表项要么就是小整数,要么就是长度比较短的字符串,Redis就会使用压缩列表来做列表键的底层实现
- 整数集合是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现
简单动态字符串SDS (Simple Dynamic String)
基于C语言中传统字符串的缺陷,Redis自己构建了一种名为简单动态字符串的抽象类型,简称SDS,其结构如下
SDS的特点
和C字符串相比,SDS的特点如下
- 常数复杂度获取字符串长度
Redis中利用SDS字符串的len属性可以直接获取到所保存的字符串的长
度,直接将获取字符串长度所需的复杂度从C字符串的O(N)降低到了O(1)。
2. 减少修改字符串时导致的内存重新分配次数
通过C字符串的特性,我们知道对于一个包含了N个字符的C字符串来说,其底层实现总是N+1个字符长的数组(额外一个空字符结尾)
那么如果这个时候需要对字符串进行修改,程序就需要提前对这个C字符串数组进行一次内存重分配(可能是扩展或者释放)
而内存重分配就意味着是一个耗时的操作。
Redis巧妙的使用了SDS避免了C字符串的缺陷。在SDS中,buf数组的长度不一定就是字符串的字符数量加一,buf数组里面可以包含未使用的字节,而这些未使用的字节由free属性记录。
与此同时,SDS采用了空间预分配的策略,避免C字符串每一次修改时都需要进行内存重分配的耗时操作,将内存重分配从原来的每修改N次就分配N次——>降低到了修改N次最多分配N次。
如下是Redis对SDS的简单定义:
Redis特性1:事务
- 命令序列化,按顺序执行
- 原子性
- 三阶段: 开始事务 - 命令入队 - 执行事务
- 命令:MULTI/EXEC/DISCARD
Redis特性2:发布订阅(Pub/Sub)
- Pub/sub是一种消息通讯模式
- Pub发送消息, Sub接受消息
- Redis客户端可以订阅任意数量的频道
- “fire and forgot”, 发送即遗忘
- 命令:Publish/Subscribe/Psubscribe/UnSub
Redis特性3:Stream
- Redis 5.0新增
- 等待消费
- 消费组(组内竞争)
- 消费历史数据
- FIFO
以上就是Redis的基本概念,下面我们将介绍在开发过程中可能会踩到的“坑”。
Redis常见问题解析:击穿
概念:在Redis获取某一key时, 由于key不存在, 而必须向DB发起一次请求的行为, 称为“Redis击穿”。
引发击穿的原因:
- 第一次访问
- 恶意访问不存在的key
- Key过期
合理的规避方案:
- 服务器启动时, 提前写入
- 规范key的命名, 通过中间件拦截
- 对某些高频访问的Key,设置合理的TTL或永不过期
Redis常见问题解析:雪崩
概念:Redis缓存层由于某种原因宕机后,所有的请求会涌向存储层,短时间内的高并发请求可能会导致存储层挂机,称之为“Redis雪崩”。
合理的规避方案:
- 使用Redis集群
- 限流
Redis在产品开发中的应用实践
为此,我很高兴的为大家介绍,葡萄城架构师Jim将在2019-11-27 14:00 为大家带来一场公开课,其中 Jim除了为大家讲解Redis的基础,同时也会实际演示他所在的项目组使用Redis时碰到的问题以及解决方案,对于刚接触Redis的同学来说,更具参考意义和学习价值,欢迎大家届时参加,公开课地址:https://live.vhall.com/661463644。
- 后端采用nodeJS
- 使用Azure的Redis服务
- Redis的使用场景
- token缓存, 用于令牌验证
- IP白名单
碰到的问题
- “网络抖动”或者Redis服务异常导致Redis访问超时
- Redis客户端驱动稳定性问题
- 连接池 “Broken connection” 问题
- JS的Promise引出的Redis重置问题
下面我们来简单了解一下Redis的进阶知识。
进阶之Redis协议简介
Redis客户端通讯协议:RESP(Redis Serialization Protocol),其特点是:
- 简单
- 解析速度快
- 可读性好
Redis集群内部通讯协议:RECP(Redis Cluster Protocol ) ,其特点是:
- 每一个node两个tcp 连接
- 一个负责client-server通讯(P: 6379)
- 一个负责node之间通讯(P: 10000 + 6379)
Redis协议支持的数据类型:
-
简单字符(首字节: “+”)
“+OK\r\n”
-
错误(首字节: “-”)
“-error msg\r\n”
-
数字(首字节: “:”)
“:123\r\n”
-
批量字符(首字节: “$”)
“&hello\r\nWhoa re you\r\n”
-
数组(首字节: “*”)
“*0\r\n”
“*-1\r\n”
除了Redis,还有什么NoSQL型数据库
市面上类似于Redis,同样是NoSQL型的数据库有很多,如下图所示,除了Redis,还有MemCache、Cassadra和Mongo。下面,我们就分别对这几个数据库做一下简要的介绍:
Memcache**:**这是一个和Redis非常相似的数据库,但是它的数据类型没有Redis丰富。Memcache由LiveJournal的Brad Fitzpatrick开发,作为一套分布式的高速缓存系统,被许多网站使用以提升网站的访问速度,对于一些大型的、需要频繁访问数据库的网站访问速度的提升效果十分显著。
Apache Cassandra**:**(社区内一般简称为C*)这是一套开源分布式NoSQL数据库系统。它最初由Facebook开发,用于储存收件箱等简单格式数据,集Google BigTable的数据模型与Amazon Dynamo的完全分布式架构于一身。Facebook于2008将 Cassandra 开源,由于其良好的可扩展性和性能,被 Apple、Comcast、Instagram、Spotify、eBay、Rackspace、Netflix等知名网站所采用,成为了一种流行的分布式结构化数据存储方案。
MongoDB:是一个基于分布式文件存储、面向文档的NoSQL数据库,由C++编写,旨在为WEB应用提供可扩展的高性能数据存储解决方案。MongoDB是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系型数据库的,它支持的数据结构非常松散,是一种类似json的BSON格式。
总结
Redis简介和优缺点
redis本质上是一个Key- Value 类型的内存数据库,是纯内存操作。定期 通过异步操作把数据库数据flush 到硬盘上进行保存。
优点: - 性能出色 - 支持保存多种数据结构,且单个value最大限制是1GB - 可以设置一个时效时间
应用:List做FIFO双向链表就可以实现一个轻量级的高性能消息队列服务。而用Set就可以做一个高性能的tag系统
缺点:数据库容量收到物理内存限制,不好做海量数据的高性能读写。
Redis 与memcached 相比有哪些优势?
1 .memcached 所有的值均是简单的字符串, redis 作为其替代者, 支持更为丰富的数据类型 2.redis 的速度比memcached 快很多 3.redis可以持久化数据。
redis数据类型
String 、List 、Set、Zset 、hash
Redis 主要消耗什么物理资源?
内存
数据淘汰策略
作为一个内存数据库,redis在内存空间不足的时候,为了保证命中率,就会选择一定的数据淘汰策略
-
volatile-lru(推荐使用):从设置了过期时间的数据集中,选择最近最久未使用的数据释放;
-
allkeys-lru:从数据集中(包括设置过期时间以及未设置过期时间的数据集中),选择最近最久未使用的数据释放;
-
volatile-random:从设置了过期时间的数据集中,随机选择一个数据进行释放;
-
allkeys-random:从数据集中(包括了设置过期时间以及未设置过期时间)随机选择一个数据进行入释放;
-
volatile-ttl:从设置了过期时间的数据集中,选择马上就要过期的数据进行释放操作;
-
noeviction(默认):不删除任意数据(但redis还会根据引用计数器进行释放),这时如果内存不够时,会直接返回错误。
一个字符串类型的值能存储最大容量是多少?
512M
MySQL里有2000w 数据,redis中只存20w的数据,如何保证redis 中的数据都是热点数据?
限定redis占用的内存,redis会根据自身数据淘汰策略,留下热数据到内存。所以可以计算100w数据大约占用的内存,
然后设置一下redis内存限制即可,并将淘汰策略设置为allkeys-lru或者volatile-lru.
设置redis最大占用内存:
打开redis配置文件,设置maxmemory参数,maxmemory是bytes字节类型哦!
maxmemory 268435456
设置过期策略:
https://www.cnblogs.com/ysocean/p/9074787.html redis配置文件介绍
maxmemory-policy volatile-lru
过期策略
redis会把设置了过期时间的key放入一个独立的字典里,在key过期时并不会立刻删除它。而是通过两种策略:
- 惰性删除:客户端访问某个key,redis检查key是否过期,过期了就删掉
- 定期扫描:redis默认每秒执行10次过期扫描,扫描策略如下:
- 从过期字典中随机选择20个key
- 删除20个key中已过期的key
- 如果过期的key比例超过25%就会重复第一步
LRU算法
维护一个链表,用于顺序存放被访问过的key,在访问数据是,最新访问过的key将被移动到表头,这样最近访问的key在表头,最少访问的key在表尾
为什么Redis 需要把所有数据放到内存中?
Redis为了达到最快的读写速度将数据都读到内存中,并通过异步的方式将数据写 入磁盘。所以redis 具有快速和数据待久化的特征,如果不将数据放在内存中, 磁盘I/O速度为严重影响redis的性能。
Redis 如何做内存优化?
尽可能使用散列表(hashes) , 散列表(是说散列表里面存储的数少) 使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。 比如你的web系统中有一个用户对象,不要为这个用户的名称, 姓氏, 邮箱, 密码设罢单独的key, 而是应该把这个用户的所有信息存储到一张散列表里面。
Redis 集群会有写操作丢失吗?为什么?
Redis 并不能保证数据的强一致性, 这意味这在实际中集群在特定的条件下可能会丢失写操作。
Redis 集群的主从复制模型是怎样的?
为了使在部分节点在失败或者大部分节点无法通信的情况下集群仍然可用, 所以集群使用了主从复制模型, 每个节点都会有N - 1 个复制品.
Redis 中的管道有什么用?
一次清求/响应服务器能实现处理新的清求即使旧的清求还未被响应, 这样就可以将多个命令发送到服务器, 而不用等待回复, 最后在一个步骤中读取该答复。这就是管道( pi pelinin g ) , 比如我在论坛系统项目中邮件发送了解到的POP3 协议已经实现支待这个功能,可以大大加快了从服务器下载新邮件的过程。
缓存
缓存雪崩
对于系统 A,假设每天高峰期每秒 5000 个请求,本来缓存在高峰期可以扛住每秒 4000 个请求,但是缓存机器意外发生了全盘宕机。缓存挂了,此时 1 秒 5000 个请求全部落数据库,数据库必然扛不住,它会报一下警,然后就挂了。此时,如果没有采用什么特别的方案来处理这个故障,DBA 很着急,重启数据库,但是数据库立马又被新的流量给打死了。
这就是缓存雪崩。
缓存雪崩的事前事中事后的解决方案如下。
事前:redis 高可用,主从+哨兵,redis cluster,避免全盘崩溃。 事中:本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死。 事后:redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
用户发送一个请求,系统 A 收到请求后,先查本地 ehcache 缓存,如果没查到再查 redis。如果 ehcache 和 redis 都没有,再查数据库,将数据库中的结果,写入 ehcache 和 redis 中。
限流组件,可以设置每秒的请求,有多少能通过组件,剩余的未通过的请求,怎么办?走降级!可以返回一些默认的值,或者友情提示,或者空白的值。
好处:
- 数据库绝对不会死,限流组件确保了每秒只有多少个请求能通过。
- 只要数据库不死,就是说,对用户来说,部分的请求都是可以被处理的。
- 只要有部分的请求可以被处理,就意味着你的系统没死,对用户来说,可能就是点击几次刷不出来页面,但是多点几次,就可以刷出来一次。
缓存穿透
对于系统A,假设一秒 5000 个请求,结果其中 4000 个请求是找不到的
比如发出的那 4000 个请求,缓存中查不到,每次你去数据库里查,也查不到。
举个栗子。数据库 id 是从 1 开始的,结果黑客发过来的请求 id 全部都是负数。这样的话,缓存中不会有,请求每次都“视缓存于无物”,直接查询数据库。这种恶意攻击场景的缓存穿透就会直接把数据库给打死。
解决方式很简单,每次系统 A 从数据库中只要没查到,就写一个空值到缓存里去,比如 set -999 UNKNOWN。然后设置一个过期时间,这样的话,下次有相同的 key 来访问的时候,在缓存失效之前,都可以直接从缓存中取数据。
缓存击穿
缓存击穿,就是说某个 key 非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。
假设一秒 5000 个请求,结果其中 4000 个请求是找不到的
比如发出的那 4000 个请求,缓存中查不到,每次你去数据库里查,也查不到。
举个栗子。数据库 id 是从 1 开始的,结果黑客发过来的请求 id 全部都是负数。这样的话,缓存中不会有,请求每次都“视缓存于无物”,直接查询数据库。这种恶意攻击场景的缓存穿透就会直接把数据库给打死。
解决方式很简单,每次系统 A 从数据库中只要没查到,就写一个空值到缓存里去,比如 set -999 UNKNOWN。然后设置一个过期时间,这样的话,下次有相同的 key 来访问的时候,在缓存失效之前,都可以直接从缓存中取数据。
缓存击穿
缓存击穿,就是说某个 key 非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。
解决方式也很简单,可以将热点数据设置为永远不过期;或者基于 redis or zookeeper 实现互斥锁,等待第一个请求构建完缓存之后,再释放锁,进而其它请求才能通过该 key 访问数据。