【面试八股|Redis篇】基于黑马点评项目实现的常见Redis常见八股详解

Redis

由于黑马点评项目对于redis的实战十分专业全面系统。因此这部分面试八股将具体结合黑马点评项目讲解。在面试官询问时,都可以提及这部分的实战操作。如"在我之前的仿小红书项目中曾使用过该功能......"然后再结合具体业务场景,一定能在面试官面前狠狠加分。

Redis基础

简单介绍一下Redis

1.Redis是使用c语言开发的数据库,与传统数据库不同的是redis的数据是存在内存中的,读写速度非常快。

2.redis除了由于缓存方向,还可以实现分布式锁,分布式session,消息队列等。而这些我在之前的项目中都有使用到,通过分布式锁实现秒杀业务,通过消息队列实现异步秒杀。

分布式缓存的技术选型方案有哪些

使用比较多的主要是Memcached与Redis,不过memcached也知识在分布式缓存刚兴起时常用,现在也不常用了。

Memcached相较于redis缺点较多,比如只能支持最简单的k/v数据类型,不支持数据持久化,没有原生的集群模式,过期数据删除策略只有惰性删除等等。

Redis数据类型

常见数据类型

redis以键值形式存储redis,key使用string类型,我们常说的几种数据类型往往指的是value的数据类型。

value有五种常用数据类型,string,list,hash,set,zset。四种新增数据类型bitmap,geo,stream,hyperloglog

详解数据类型使用场景以及特点

String数据类型

特点:字符串类型,redis中最简单的存储类型。可存储普通字符串,整形浮点型,底层都是字节存储,编码方式不同。

使用场景:缓存对象、常规计数、分布式锁、共享 session 信息等。

共享session信息:key:唯一性(业务前缀+随机token) value:用户信息

一般情况业务缓存:如查询商户信息等,key:业务前缀+商户id。对性能,资源消耗非常敏感的话使用。

分布式锁:setnx实现。

List数据类型

特点:类似java的LinkedList,可看作双向链表结构。有序,元素可重复,插入删除快等

使用场景:用作消息队列,评论列表等

消息队列场景:通过左插入右删除模拟队列,但由于无法解决消息缺失,单消费者等问题,因此常用stream类型。

Hash

特点:类似java的HashMap,相较于string结构,相当于把value对象的每个字段单独存储,可针对每个字段做crud。(也就是value里又分出field,value)

使用场景:一般情况业务缓存、购物车等。商品经常调整变动,需要更灵活时很适用。

Set

特点:类似java的HashSet。无序,元素不重复,查找快,支持交,并,差集。

使用场景:共同关注,抽奖活动

共同关注:将两人关注的人放到set集合里去,然后用交集查看共同关注

Zset

特点:类似java的TreeSet,可排序的set集合。

使用场景:排序场景

点赞排序场景:为实现唯一可使用set,但为了顺序就得使用zset。

BitMap

把每一个bit位对应当月的每一天,形成了映射关系。用0和1标示业务状态,这种思路就称为位图 (BitMap)。这样我们就用极小的空间,来实现了大量数据的表示

应用场景:二值状态统计的场景,比如签到、判断用户登陆状态、连续签到用户总数等;甚至可以用来解决缓存穿透。

解决缓存穿透:为节省内存避免id过长可用bitmap+hash实现布隆过滤器替代list

HyperLogLog

Hyperloglog(HLL)是从Loglog算法派生的概率算法,用于确定非常大的集合的基数,而不需要存储其所 有值。相关算法原理大家可以参考: https://juejin.cn/post/6844903785744056333#heading-0。Redis中的HLL是基于string结构实现的,单个HLL的内存永远小于16kb,内存占用低的令人发指!作为 代价,其测量结果是概率性的,有小于0.81%的误差。不过对于UV统计来说,这完全可以忽略。

应用场景:海量数据基数统计的场景,比如百万级网页 UV 计数等;

GEO

GEO就是Geolocation的简写形式,代表地理坐标。Redis在3.2版本中加入了对GEO的支持,允许存储地 理坐标信息,帮助我们根据经纬度来检索数据。

应用场景:存储地理位置信息的场景,比如滴滴叫车;

Stream

可以实现一个功能非常完善的消息队列,支持消息持久化,阻塞读取,确认机制,消息回溯,消费者组等。

应用场景:消息队列的应用场景,例如异步秒杀下单。

不同数据类型的区别

根据上述的数据类型特点即可总结出区别。常见的问法有zset与set的区别,string与hash哪个适合存储数据对象做缓存

常见数据类型底层结构

这部分内容很多,会先根据小林coding记录,如果不够用再慢慢补充。

Redis的底层数据结构

动态字符串,intset,Dict,ZipList,skiplist,QuickList,RedisObject。

Zset的底层实现

采用ziplist或skiplist。如果元素个数小于128并且每个元素小于64字节时采用ziplist,否则采用skiplist。

什么是ziplist

压缩列表可看作连续内存的双向链表,节点间通过记录本节点和上一节点长度寻址,占用内存低。

存储节点长度的字节数有两种变化,增或删较大数据时可能发生连续更新。

什么是skiplist

跳跃表是一个双向链表,每个节点都有score值(存储分数)与ele值(节点存储值)每个节点则包含多层指针,层数为1-32之间的随机数,不同层到下一节点的跨度不同。

String的底层实现

简单动态字符串(Simple Dynamic String),简称SDS。实现

什么是sds

动态字符串,是一个结构体,包含len,alloc,flags,buf[]。解决了c语言字符串二进制不安全,获取字符串长度需要遍历,不支持动态扩容的局限。

线程模型

Redis为什么这么快

单线程redis吞吐量可达10bps

之所以这么快有以下原因:

1.redis大部分操作是在内存中完成的,并且采用了高效的数据结构。因此它的性能瓶颈往往是网络延迟而不是cpu执行速度。

2.redis主要执行操作采用单线程,避免多线程之间的竞争与上下文切换带来的成本开销

3.redis采用了I/O多路复用机制处理大量的客户端Socket请求,通过epoll_create创建epoll实例,通过红黑树记录被监听的fd,链表记录已就绪的fd。

Redis哪些地方使用多线程

1.Redis在启动时,会启动后台线程,用来关闭文件,AOF刷盘,释放内存等

2.Redis在6.0之后新增I/O线程来处理网络请求,主要是因为Redis的性能瓶颈往往会出现在网络IO处理上。不过因为单线程执行命令,因此不需要关心线程安全问题。

3.Redis6.0的I/O多线程默认是不会处理读请求,需要在配置文件中开启并设置线程数

内存管理

介绍一下Redis的内存淘汰策略

1.Redis的内存淘汰策略共有八种,可分为不进行数据淘汰和进行数据淘汰两类策略

2.不进行数据淘汰策略的有,3.0之后的默认内存淘汰策略,当内存超过最大设置的内存时不淘汰数据,而是写入数据报错

3.进行数据淘汰的又可细分为两类,在设置了过期实践的数据中进行淘汰和在所有数据范围内进行淘汰

4.在设置了过期时间的数据中淘汰的策略有:volatile-random(随机淘汰),volatile-ttl(淘汰最早过期的),volatile-lru(淘汰最久未使用),volatile-lfu(淘汰最少使用的)

5.在所有数据范围内进行淘汰的策略有:allkeys-random,allkeys-lru,allkeys-lfu

过期删除策略有哪些

1.redis中常用的过期删除策略有两种,惰性删除与定期删除

2.惰性删除是指,在取出key时对数据进行过期检查,这样对cpu比较友好,但可能造成大量过期key没有删

3.定期删除是指,每隔一段时间就抽取一批key执行删除过期key的操作,对内存比较友好。

说说Redis持久化

1.Redis支持两种持久化方式,一种是快照(RDB),一种是追加文件(AOF)。

2.Redis默认使用快照模式,通过创建快照来存储某个时间点内存里的数据。Redis通过bgsave,save命令创建快照,bgsave不会阻塞主进程。

3.Redis通过appendonly参数开启AOF持久化,通过将redis执行命令保持到.aof_buf中实现持久化。AOF执行时机有三种,分别是always,everysec,no(操作系统决定时机)。

4.redis4.0版本开始支持aof与rdb混合持久化。

详细说说AOF

1.Redis通过appendonly参数开启AOF持久化,通过将redis执行命令保持到.aof_buf中实现持久化。AOF执行时机有三种,分别是always,everysec,no(操作系统决定时机)。

2.AOF一般在执行命令之后再记录日志,与mysql相反。这样可以避免额外的检查开销,并且不会阻塞当前命令执行。但也有执行完命令redis就宕机丢失记录的风险

3.执行bgrewriteaof时,redis会fork一个子进程重写aof,精简命令来替换旧的aof。

RDB与AOF的区别

1.持久化方式:rdb是对整个内存做快照,aof是记录执行的命令

2.文件大小:rdb有压缩文件更小,aof文件更大

3.数据完整性:rdb不完整,两次备份之间数据丢失。aof相对完整,取决于刷盘策略

4.宕机恢复速度:rdb很快,aof较慢

事务

说说Redis的事务

1.Redis的事务主要通过multi,exec,discard,watch四个命令实现

2.multi命令之后输入的命令会被放到事务队列中,通过exec执行所有命令。discard可以清空事务队列中所有命令。watch用于监听某个指定键,如果在exec执行事务时,watch监听的键被修改,那么整个事务都会执行失败

说说Redis事务的原子性

1.如果事务正常执行,则原子性是可以被保证的

2.如果事务没有执行那么将不能保证原子性,因为除了执行过程中出错的命令,其它命令都能正常执行,redis的事务是不支持回滚的。

3.可以通过lua脚本实现原子性,redis会将lua脚本作为一个整体执行。而这在我之前的项目中秒杀业务中也是有实现的。

集群实现

主从节点怎么实现数据同步

1.在第一次连接时,master节点将完整内存数据生成rdb发送给slave,slava清空本地数据加载master的rdb实现全量同步。

2.在其它阶段,master通过repl_baklog获取offset后的数据,将这些命令发送给slave实现增量同步

3.如果未同步部分过多,导致repl_baklogg写满覆盖了最早的数据,那就会再进行全量同步

说一说哨兵机制

1.redis的哨兵机制通过监控,自动故障恢复,通知来保证主从集群的高可用性

2.监控原理:对集群发送ping命令,若某sentinel发现某实例在规定时间未响应则认为主观下线,如果超过指定数量sentinel都认为该实例主观下线则认为其客观下线。

3.故障恢复原理:根据与主节点断开时间长短,slave-priority值,offset值,节点id选取从节点。然后向该从节点发送slaveof no one命令让其成为master,给其它slave发送slave of 新master让其它从节点成为新master的从节点,最后把故障节点标记为slave。

说一说redis的分片集群

1.通过redis的分片集群解决海量数据存储,高并发写问题

2.分片集群有多个master节点和多个slave节点,每个master之间保存不同数据,master之间互相ping监测健康状态

3.redis cluster通过16384个哈希槽来处理数据与节点之间的映射关系

redis主从和集群可以保证数据一致性吗 ?

redis主从和集群在cap理论中数据ap模型,即选择了可用性和分区容错性而牺牲了强一致性。

生产问题

说一说redis中的big key

1.如果一个key对于的value很大,则称这个key为big key。一般来说当string类型value大于10kb,复合类型大于5000个则称为big key。

2.bigkey会占用较高内存,导致性能下降,网络拥塞等

3.可使用--bigkeys参数查找bigkey,或通过分析rdb文件分析bigkey 然后进行拆分

redis与数据库的一致性

1.在我的项目中通过Cache Aside Pattern旁路缓存策略实现

2.比如在写数据的情况,通过考虑是修改缓存还是删除缓存(为避免无效修改缓存操作),是先删缓存还是先修改数据库(一读一写线程情况下考虑),最终决定先修改数据库然后删除缓存,在下一次查询时将数据库的值再存入缓存。

3.不过先操作数据库仍然有极小概率出现问题。比如缓存原来就没有值,线程a查询数据库,在写入缓存之前线程b修改完数据库,导致线程a把旧值写入缓存。

4.因此这里实现了延迟双删,在操作数据库删除缓存后,过一段时间再删一次缓存,避免旧值存储。

讲讲缓存穿透

1.这个问题在我之前的项目中有具体的落地实现方案

2.缓存穿透是指客户端请求的数据在缓存中和数据库都不存在,这样会直接打到数据库上,增大数据库压力

3.可通过非法请求限制,缓存空对象,布隆过滤的方案解决

4.缓存空对象是指,由于redis能承受的并发度高于数据库,因此可以直接将不存在的数据存入缓存中,下次访问就不会打到数据上了。但这样会造成额外内存消耗和短期不一致

5.布隆过滤是在redis与客户端之间再加一层布隆过滤器去判断当前访问数据是否存在,如果不存在则直接返回,可通过bitmap实现。由于是通过哈希思想实现,有误判可能。

讲讲缓存雪崩

1.这个问题在我之前的项目中有具体的落地实现方案

2.缓存雪崩是指在同一时段大量缓存key失效或redis宕机导致大量用户请求打到数据库上

3.这里的解决思路则是避免大量缓存key同时失效,或者提升redis的高可用性避免宕机

4.具体可以是给不同key的ttl增添随机值,搭建redis集群提高可用性,给业务添加多级缓存等。

讲讲缓存击穿问题

1.这个问题在我之前的项目中有具体的落地实现方案

2.缓存击穿也叫热点key问题,是指一个被高并发访问并且重建业务复杂的key突然失效,大量请求给数据库带来巨大压力

3.这里有互斥锁或逻辑过期两种解决方案

4.互斥锁是指当某线程在redis没访问到这个热点key就会获得锁,使业务变成串行,等改线程重建缓存后其它线程再正常执行。具体使用setnx分布式锁。

5.逻辑过期是指将热点key设置逻辑过期,每次访问都判断一下是否过期,如果过期就单开一个线程去实现缓存重建,而主线程返回过期key。

6.互斥锁用性能换取一致性,逻辑过期用短暂的不一致换取性能。

业务

这里是实战重点,上面的任何问题都往这里靠近

可通过redis实现哪些业务功能

1.实现集群模式登录,通过redis的session共享实现。

2.实现查询缓存,解决了缓存数据库的一致性,缓存穿透,缓存雪崩,缓存击穿一系列问题

3.实现秒杀场景,解决了一人一单,超卖的问题

详细说说秒杀场景的实现思路

1.为每个商品生成全局唯一id,可利用时间戳+业务前缀生成

2.利用乐观锁解决超卖,执行sql时判断库存容量是否大于0

3.避免多线程用户同时下单造成一人多单,需要实现一人一单功能。通过加锁实现,但这里改进的策略思路相对复杂。

4.解耦秒杀资格校验与下单逻辑,用redis的stream充当消息队列实现异步

一人一单的实现

1.加synchronized锁方法,但粒度太粗,影响其它用户线程。因此通过用户id+intern单独锁用户。

2.在方法内加锁可能存在当前事务没提交,锁已经释放的问题。因此在其它方法里调用该方法,对调用语句加锁(相当于锁套住整个方法,但是以用户id为粒度)

3.通过this方法调用,事务失效。因此需要利用代理类来调用方法。

4.以上思路只适用于单体架构,因此利用redis的setnx实现分布式锁,添加过期时间避免死锁。

5.通过线程标识判断锁归属避免阻塞删除别人锁,但由于操作不具备原子性,可能导致验证完阻塞然后又删除别人锁。因此又采用lua脚本实现原子性。

6.但由于上述实现不能重入,不可重试,主从不一致,超时释放(可能导致一人多单)等的问题,采用的redisson。

介绍一下redission

1.redission是在redis基础上实现的Java客户端库,提供了分布式锁的多种多样的功能。

2.它同样用 Redis 的 Hash 结构存储锁信息、用 Lua 脚本保证原子性、靠 "线程标识 + 重入次数" 实现可重入,只是在细节上补充了更多工业级特性 3.看门狗机制实现自动续期,当调用无参的lock将触发看门狗机制。在业务未完成时,默认开启后台线程监控锁的ttl,超过1/3重置ttl。

4.通过mutilock解决主节点挂了锁信息却没同步到从节点的问题,它的加锁逻辑需要写到每一个主从节点才算加锁成功。

相关推荐
m0_607076606 小时前
CSS3 转换,快手前端面试经验,隔壁都馋哭了
前端·面试·css3
NEXT067 小时前
二叉搜索树(BST)
前端·数据结构·面试
NEXT067 小时前
JavaScript进阶:深度剖析函数柯里化及其在面试中的底层逻辑
前端·javascript·面试
夏鹏今天学习了吗8 小时前
【LeetCode热题100(100/100)】数据流的中位数
算法·leetcode·职场和发展
愚者游世10 小时前
brace-or-equal initializers(花括号或等号初始化器)各版本异同
开发语言·c++·程序人生·面试·visual studio
元亓亓亓11 小时前
LeetCode热题100--42. 接雨水--困难
算法·leetcode·职场和发展
学到头秃的suhian11 小时前
Redis缓存
数据库·redis·缓存
苏渡苇11 小时前
Java + Redis + MySQL:工业时序数据缓存与持久化实战(适配高频采集场景)
java·spring boot·redis·后端·spring·缓存·架构
源代码•宸12 小时前
Leetcode—200. 岛屿数量【中等】
经验分享·后端·算法·leetcode·面试·golang·dfs
用户1252055970812 小时前
后端Python+Django面试题
后端·面试