面试必问!Redis核心场景与原理一网打尽 面试必问Redis核心场景与原理一网打尽Redis作为后端开发的必备技能在面试中出现的频率极高。本文将结合黑马点评项目中的经典案例为你梳理Redis的核心应用场景、底层原理及最佳实践助你从容应对面试。文章目录一、 核心数据结构与实战应用如何用ZSet实现点赞排行榜如何用Bitmap实现用户签到与统计如何用GEO实现“附近的人/商铺”如何用HyperLogLog实现UV统计二、 缓存三大问题与解决方案什么是缓存穿透如何解决什么是缓存击穿如何解决什么是缓存雪崩如何解决三、 分布式锁与秒杀场景如何实现一个可靠的分布式锁如何解决秒杀场景下的超卖问题如何优化秒杀性能四、 底层原理与高级特性Redis为什么这么快Redis的持久化机制有哪些Redis的内存淘汰策略有哪些Redis的集群方案有哪些1.用户登录2.查询缓存一、 核心数据结构与实战应用如何用ZSet实现点赞排行榜问题 实现一个博客点赞功能并展示点赞用户排行榜按点赞时间排序。解答核心是使用Redis的 ZSet有序集合。ZSet中的每个元素都关联一个score分数可以基于分数进行排序。数据结构Key: blog:liked:{blogId}Member: userId (点赞用户的ID)Score: System.currentTimeMillis() (点赞的时间戳)实现流程点赞 使用 ZADD 命令将用户ID和时间戳添加到ZSet中。取消点赞 使用 ZREM 命令移除指定用户ID。判断是否点赞 使用 ZSCORE 命令查询用户ID的分数若存在则表示已点赞。获取排行榜 使用 ZREVRANGE 命令按分数时间戳倒序获取前N个用户ID再根据ID查询用户信息。如何用Bitmap实现用户签到与统计问题 实现用户每日签到功能并能统计连续签到天数。解答核心是利用Redis的 Bitmap位图它本质上是String类型但可以按位操作极其节省内存。实现流程签到 使用 SETBIT key offset 1 命令。Key通常为 sign:{userId}:{yyyyMM}offset 为当月第几天减1因为Bitmap从0开始。统计连续签到天数使用 BITFIELD key GET u{N} 0 命令一次性获取从本月1号到今天为止的所有签到记录并将其作为一个无符号整数返回。将这个十进制数与 1 进行按位与运算判断最后一位是否为1即今天是否签到。如果签到了将数字右移一位 1继续判断下一位直到遇到0未签到为止循环次数即为连续签到天数。如何用GEO实现“附近的人/商铺”问题 根据用户当前位置查找附近5公里内的商铺并按距离排序。解答核心是使用Redis 3.2引入的 GEO地理位置 数据结构。实现流程存储数据 使用 GEOADD 命令将商铺的经度、纬度和ID存入Redis。通常会按商铺类型分组Key为 shop:geo:{typeId}。搜索附近 使用 GEOSEARCH 命令。指定中心点坐标用户当前位置。指定搜索半径如5公里。使用 WITHDIST 参数返回距离信息。使用 COUNT 参数限制返回数量。分页处理 Redis GEO本身不支持OFFSET分页。通常的做法是先查询出前N页的数据如COUNT为pageSize * pageNum然后在应用层Java代码中通过subList进行截取。如何用HyperLogLog实现UV统计问题 统计一个网页的独立访客数UV要求内存占用极小。解答核心是使用 HyperLogLog它是一种用于基数估算的概率数据结构。优点 无论统计多少元素内存占用固定约12KB误差率低于0.81%。缺点 只能统计数量无法获取具体元素。实现流程添加访客 用户访问时使用 PFADD key userId 命令。获取UV 使用 PFCOUNT key 命令获取估算的基数。二、 缓存三大问题与解决方案什么是缓存穿透如何解决问题 查询一个在缓存和数据库中都不存在的数据导致每次请求都打到数据库。解答缓存空对象 当数据库查询结果为空时也将一个空值如存入缓存并设置一个较短的过期时间。布隆过滤器 在缓存层之前加一层布隆过滤器用于判断数据是否存在。如果布隆过滤器判断不存在则直接返回不查询缓存和数据库。什么是缓存击穿如何解决问题 一个热点Key在缓存中过期的瞬间大量并发请求同时涌入直接击穿缓存访问数据库。解答互斥锁Mutex Lock 当缓存失效时不立即去加载数据库而是先尝试获取一个分布式锁如使用SETNX。只有获取锁成功的线程才能去查询数据库并重建缓存其他线程等待并重试获取缓存。逻辑过期 不在Redis中设置物理过期时间而是在Value中保存一个逻辑过期时间。查询时判断逻辑时间是否过期。如果过期则尝试获取锁由获取锁成功的线程异步重建缓存当前线程直接返回旧数据。什么是缓存雪崩如何解决问题 在同一时间段大量Key集中过期或Redis节点宕机导致大量请求直接打到数据库。解答设置随机过期时间 在原有的TTL基础上增加一个随机值避免大量Key同时过期。构建高可用集群 使用Redis主从、哨兵或Cluster集群避免单点故障。服务降级与限流 当缓存和数据库压力过大时对非核心服务进行降级或限流。三、 分布式锁与秒杀场景如何实现一个可靠的分布式锁问题 在集群环境下如何保证“一人一单”等业务的线程安全解答一个可靠的分布式锁需要满足互斥、可重入、防死锁、防误删等特性。获取锁 使用 SET key value NX EX timeout 命令。NX保证互斥EX设置超时时间防止死锁value使用唯一标识如UUID线程ID防止误删。释放锁 释放锁时需要先判断value是否为自己的唯一标识再执行删除。为保证“判断删除”的原子性必须使用 Lua脚本。使用Redisson 生产环境强烈推荐使用Redisson客户端它封装了分布式锁的复杂逻辑提供了可重入、锁续期WatchDog机制等高级功能。如何解决秒杀场景下的超卖问题问题 高并发下多个线程同时查询到库存充足导致扣减库存后出现负数。解答乐观锁推荐 在数据库层面解决。更新库存时增加一个WHERE stock 0的条件。UPDATE seckill_voucher SET stock stock - 1 WHERE voucher_id ? AND stock 0通过判断SQL执行后影响的行数是否大于0来确定扣减是否成功。分布式锁 在业务代码层面加锁保证同一时间只有一个线程能执行扣减库存的逻辑。但性能较差。如何优化秒杀性能问题 秒杀请求直接打到数据库造成巨大压力。解答采用 异步秒杀 方案将同步操作转为异步。Redis预减库存 秒杀开始前将库存信息加载到Redis中。Lua脚本校验 使用Lua脚本原子性地判断库存是否充足、用户是否已下单。如果校验通过则在Redis中扣减库存并将订单信息用户ID、优惠券ID放入一个 阻塞队列BlockingQueue 中。异步下单 开启一个独立的线程不断从阻塞队列中获取订单信息然后异步地创建数据库订单。这样可以将瞬时的高并发请求平滑地处理掉。四、 底层原理与高级特性Redis为什么这么快基于内存操作 数据存储在内存中读写速度极快。高效的数据结构 如SDS、跳表、压缩列表等为不同场景做了极致优化。IO多路复用 采用 epoll 模型使用单线程即可高效处理大量并发连接避免了多线程的上下文切换开销。单线程模型 核心命令处理是单线程的避免了锁竞争保证了命令的原子性。Redis的持久化机制有哪些RDB快照 在指定时间间隔内将内存中的数据快照写入磁盘。优点 恢复速度快文件紧凑。缺点 可能会丢失最后一次快照后的数据。AOF追加文件 记录每一次写操作命令重启时重新执行这些命令来恢复数据。优点 数据安全性更高。缺点 文件体积大恢复速度慢。生产建议 通常结合使用以AOF为主RDB为辅做冷备。Redis的内存淘汰策略有哪些当内存使用达到maxmemory上限时Redis会根据配置的策略淘汰部分Key。noeviction 默认策略不淘汰写操作报错。allkeys-lru 在所有Key中淘汰最近最少使用的Key生产环境最常用。volatile-lru 在设置了过期时间的Key中淘汰最近最少使用的Key。volatile-ttl 在设置了过期时间的Key中淘汰即将过期的Key。Redis的集群方案有哪些主从复制 实现读写分离和数据备份。哨兵Sentinel 在主从基础上实现自动故障转移和高可用。Cluster分片集群 官方提供的分布式方案通过哈希槽16384个将数据分片存储在多个主节点上支持水平扩展。1.用户登录1.为啥把生成的验证码存入Redis里验证码是临时数据需要快速存取、自动过期且必须支持跨请求/跨服务器共享而 Redis 恰好完美满足这些需求stringRedisTemplate.opsForValue().set(login:code:phone,code,2,TimeUnit.MINUTES);方案问题存数据库MySQL太重了验证码是临时数据频繁读写数据库性能差存 Session单机可以但集群环境下 Session 不共享用户请求可能被分发到不同服务器存内存Map单机可以但集群不共享且服务重启数据丢失还需自己写过期逻辑存 Redis天然支持过期时间、读写极快内存操作、集群共享2.mybatisplus使用过程// 1. 实体类 - 用 TableName 标识对应的表DataTableName(t_user)publicclassUser{TableId(typeIdType.AUTO)privateLongid;privateStringname;privateIntegerage;}// 2. Mapper 接口 - 继承 BaseMapperMapperpublicinterfaceUserMapperextendsBaseMapperUser{// 无需写任何方法直接拥有 CRUD 能力}// 3. Service 接口 - 继承 IServicepublicinterfaceUserServiceextendsIServiceUser{// 可以在此定义自定义业务方法}// 4. Service 实现类 - 继承 ServiceImplServicepublicclassUserServiceImplextendsServiceImplUserMapper,UserimplementsUserService{// 无需写任何代码直接拥有 CRUD 能力}3.为啥不用session而用Redis做token校验多态tomcat服务器不共享session存储空间不是用户主动登录多台服务器而是负载均衡会自动把用户的请求分发到不同服务器所以必须用 Redis 做共享存储让任何一台服务器都能查到用户的登录状态4.为啥每次检验完token都要把用户DTO信息存入ThreadLocalThreadLocal用于在后续的业务代码中在同一个请求线程中内共享当前登录用户的信息不然后续要用到登录用户的信息时都要反复从请求头取token查Redis取用户信息再执行业务代码5.优化拦截器时为啥要整两个拦截器为了解决一个拦截器只拦截部分请求进行不被拦截的请求时就不刷新token的有效期用户还需要反复登录前置拦截器拦截拦截所有请求刷新token过期时间、存信息到ThreadLocal、放行第二个拦截器拦截需要登录的请求判断ThreadLocal是否有数据有就放行总结一下登录的过程发送验证码用户输入手机号校验手机号格式生成六位随机验证码把验证码存入Redis设置有效期返回给用户验证码校验登录用户输入手机号验证码验证手机号格式从Redis取验证码根据手机号查数据库决定是否注册新用户生成token把部分用户信息存入Redis把token返回给前端前端把token存起来每次请求在请求头加上token请求校验前端发起的请求被前置拦截器拦截有token就查Redis得到用户信息存到ThreadLocal刷新Redis有效期放行第二个拦截器拦截需要登录的请求从ThreadLocal取信息有信息就放行2.查询缓存1.实现商铺缓存与数据库的双写一致给查询商铺的缓存添加超时剔除和主动更新的策略根据id查询店铺时若缓存未命中则查询数据库将数据库结果写入缓存并设置超时期限根据id修改店铺时先修改数据库再删除缓存2.商铺查询时的缓存穿透因为Redis和数据库都没有要查的数据后续请求就会反复查询Redis和数据库使性能降低解决方法缓存空对象优点实现简单维护方便缺点额外的内存消耗可能造成短期的不一致布隆过滤优点内存占用少没有多余key缺点实现复杂存在误判可能布隆过滤器挡在前面过滤掉绝大部分不存在的 key海量不同的假 key 场景回填空值 短 TTL兜底防止同一个假 key 反复穿透单个 key 重复攻击场景