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解决主节点挂了锁信息却没同步到从节点的问题,它的加锁逻辑需要写到每一个主从节点才算加锁成功。
