黑马点评实战笔记:从 Redis 基础到高并发踩坑指南 思维导图黑马点评学习笔记Redis基础MySQL vs Redis核心特性Java客户端:项目实战模块短信登录商户查询缓存:秒杀全局唯一ID乐观锁 vs 悲观锁分布式锁附近商户UV统计用户签到好友关注达人探店:缓存三剑客缓存穿透缓存雪崩缓存击穿:分布式锁MySQL实现Redis实现Zookeeper实现误删问题:工具与踩坑JMeterJDK高版本Bug序列化Bug调试技巧1. 基础概念速记1.1 MySQL vs Redis维度MySQL关系型RedisNoSQL存储磁盘内存→ 快事务ACID原子、一致、隔离、持久无扩展垂直升级硬件水平加节点类型表键值型 / 图型如 Neo4j缩写ACID — Atomicity原子性、Consistency一致性、Isolation隔离性、Durability持久性1.2 Redis 核心特性单线程 命令原子性 → 天然线程安全内存存储→ 低延迟、速度快键值型支持数据持久化RDB / AOF 两种方式写入磁盘支持主从集群、分片集群多语言客户端支持1.3 Java 客户端三剑客客户端特点Jedis老牌客户端API 接近 Redis 命令简单直接Lettuce基于 Netty异步非阻塞线程安全Spring Boot 默认内置Redisson功能最丰富提供分布式锁、队列等高级数据结构封装面试回答Spring Boot 2.x 之后默认使用 Lettuce 作为 Redis 客户端因为它基于 Netty 异步非阻塞连接复用且线程安全适合高并发场景。Redisson 则提供分布式锁、布隆过滤器等高级中间件能力适合分布式场景。 选型速记• 简单读写 →LettuceSpring Boot 默认开箱即用• 需要分布式锁/队列 →Redisson• 老项目兼容 →Jedis2. 缓存策略总述┌──────────┐ 请求 ──▶│ 查缓存 │── 命中 ──▶ 返回 └────┬─────┘ 未命中 │ ▼ ┌──────────┐ │ 查数据库 │── 不存在 ──▶ 404 └────┬─────┘ 存在 │ ▼ ┌────────────────┐ │ 写入缓存 返回 │ └────────────────┘缓存更新策略对比策略原理一致性适用场景内存淘汰内存不足时自动删旧数据差对一致性要求低的展示型数据超时剔除设置 TTLTime To Live生存时间到期自动删一般兜底策略防脏数据永不过期主动更新Cache Aside写数据库时同步更新/删除缓存好推荐绝大部分业务场景生活类比缓存更新就像手机通知栏的消息。内存淘汰 通知栏满了自动清掉最旧的 → 你可能会漏消息超时剔除 24 小时后消息自动消失 → 至少今天能看到主动更新 读完消息主动划掉 → 最及时准确先删缓存再写数据库 vs 先写数据库再删缓存方案并发风险概率先删缓存A 删缓存 → B 查库写旧缓存 → A 写新库⚠️ 大先写库再删B 读旧缓存失败查库 → A 写库删缓存 → B 写旧缓存✅ 小面试回答推荐「先写数据库再删缓存」模式。因为写数据库通常比查数据库慢所以先写后删的并发窗口更小。但无论哪种方案删除缓存优于更新缓存——当并发写发生时更新缓存的值可能被覆盖为旧值而删除缓存让下次请求重新加载天然避免了版本错乱。3. 缓存三大难题3.1 缓存穿透定义请求的数据缓存和数据库都不存在缓存永不生效每次请求直接打到数据库。生活类比有人拿着不存在的「9527 号房间」房卡去前台查询。前台每次都查电脑数据库永远查不到但每次都白费力气。布隆过滤器就像在门口安检时说「我们酒店最大房号就到 5209527 直接拒绝」解决方案方案优点缺点缓存空对象简单维护方便额外内存消耗需设短 TTL可能短期不一致布隆过滤器内存占用少无多余 key实现复杂存在误判不存在可能判为存在面试回答缓存穿透是指查询不存在的数据导致请求直达数据库。解决方案有两层一是缓存空值并设短过期时间二是在缓存前加布隆过滤器用极小的 bitmap 空间提前过滤掉大概率不存在的 key。此外还需做好参数校验和 ID 混淆防止恶意构造请求。3.2 缓存雪崩定义大量缓存 key 同时失效或Redis 宕机瞬时请求全部砸向数据库。生活类比❄️面包店每天 8 点上架新鲜面包顾客都挤在 8 点来买。如果所有面包同时售罄缓存同时过期人群瞬间涌向仓库数据库造成混乱。解决方法给每种面包不同的出炉时间TTL 随机值或者多开几个售货窗口Redis 集群。解决方案给不同 key 的 TTL加随机值如 ±5 分钟使用Redis 集群主从 哨兵 集群提高可用性缓存业务加降级限流Sentinel 限流业务添加多级缓存本地 Caffeine Redis 远程面试回答缓存雪崩指大量 key 在同一时刻过期或 Redis 宕机导致流量直接冲击数据库。核心应对是打散过期时间TTL 随机值降低同时过期的概率同时通过 Redis 集群保证高可用必要时在网关层做限流降级保护数据库。3.3 缓存击穿热点 Key定义一个高并发访问且缓存重建复杂的热点 key 突然失效瞬间大量请求打到数据库。个人理解就像水滴石穿——反复砸同一个点。生活类比某网红奶茶店「招牌波波茶」每天卖出上千杯。如果这条配方纸缓存突然被风吹走所有店员同时跑去仓库翻原始文件数据库仓库瞬间被挤爆。解决方案要么只让一个店员去拿文件其他人等着互斥锁要么每个人都有一份复印件的备份延长有效期逻辑过期。解决方案方案原理优点缺点互斥锁缓存未命中时加锁只让一个线程查库重建一致性好无额外内存线程等待可能死锁逻辑过期缓存不过期加一个逻辑过期时间字段逻辑过期后仍返回旧值异步重建线程无需等待性能好不保证一致性有额外内存消耗面试回答缓存击穿指热点 key 过期瞬间大量并发请求穿透缓存。互斥锁方案用分布式锁保证同一时刻只有一个线程重建缓存属于 CP一致性优先逻辑过期方案不真正删除 key而是标记过期后异步重建属于 AP可用性优先。实际选型需根据业务对一致性的容忍度来决定。4. 分布式锁4.1 为什么需要分布式锁单机synchronized只在同一个 JVM内有效。集群部署时多个 JVM 各自加锁互不干扰无法保证并发安全——必须引入外部共享锁监视器。生活类比synchronized是各自房间里的门锁——每个房间的人锁好自己的门但拦不住别人进他自己的房间。分布式锁是公共厕所的门锁——谁进去谁锁上所有人都能看到「有人」的标记。4.2 实现方式对比实现原理特点MySQL利用行锁 / 唯一索引实现简单性能较差RedisSET lock threadId NX EX 10高性能自动过期释放Zookeeper临时顺序节点 Watch 机制强一致性节点断开自动释放缩写NX — Not eXists不存在时设置EX — EXpire seconds过期秒数4.3 误删问题场景线程 A 获取锁后业务阻塞超时锁自动释放。线程 B 获取锁此时 A 恢复执行并直接释放了 B 的锁 → B 的锁被误删。解决方案释放锁时校验 UUID 线程 ID判断这把锁是否还属于自己的才能删除。释放锁伪代码 if (redis.get(lock) myUUID threadId) { redis.del(lock); // 原子操作用 Lua 脚本保证 get del 原子性 }面试回答分布式锁主要有 RedisSET NX EX和 Zookeeper 临时节点两种实现。Redis 方案性能高、部署简单但默认非阻塞需要客户端实现重试Zookeeper 方案强一致Watcher 机制天然支持阻塞等待。实际使用中需注意锁续期Redisson 的看门狗机制和释放锁时的身份校验防止误删。5. 秒杀核心全局 ID 乐观锁5.1 优惠券 ID 为什么不用自增规律性太明显 → 容易被遍历抢购受单表数据量限制分布式环境自增 ID 冲突5.2 全局 ID 生成器雪花算法 Snowflake┌─┬─────────────────────────┬──────────────────┐ │0│ 时间戳 (31bit) │ 序列号 (32bit) │ │ │ ≈69年 │ 2^32 ≈ 42亿 │ └─┴─────────────────────────┴──────────────────┘符号位 1bit永远为 0正数Long 类型时间戳 31bit从起始时间算起可用约 69 年序列号 32bit同一秒内可生成约 42 亿个 ID面试回答分布式全局 ID 常见方案有UUID无序不适合做主键索引、数据库自增单点瓶颈、雪花算法Snowflake。雪花算法的优点是高性能、趋势递增利于索引、纯本地生成无需网络开销缺点是强依赖机器时钟时钟回拨可能导致 ID 重复。5.3 乐观锁防超卖悲观锁认为冲突一定发生操作前先加锁 → 串行执行性能低乐观锁认为冲突不常发生更新时检查版本 → 有冲突可重试或失败生活类比️演唱会抢票。悲观锁 每次只放一个人进去选座选完一个放下一个慢但安全乐观锁 所有人都能选提交时检查「座位还在不在」→ 还在就成功被抢了重试CASCompare And Swap实现// ❌ 严格版本号判断——可能导致卖不完库存剩1多个请求读到版本1只有第一个更新成功.eq(stock,seckillVoucher.getStock())// ✅ 库存判断只要 stock 0 就能卖.gt(stock,0)经验对于扣减库存这类场景只需判断stock 0即可不适用严格的版本号 CAS。集群场景补充synchronized只对单 JVM 有效。多实例部署时数据库唯一索引兜底是最后防线。同一项目多端口启动Edit Configuration → Modify options → Program arguments 输入--server.port80826. 实战踩坑记录6.1 JDK 高版本兼容 BugJDK 26.0.1导致 Spring Boot 启动报错java.lang.ExceptionInInitializerError com.sun.tools.javac.code.TypeTags解决pom.xml中指定 Lombok 版本version1.18.46/version6.2 BeanUtil 反序列化 Bug现象刷新网页几次后商户信息出现NaN。原因BeanUtil.toBean()是 Bean 属性拷贝不是 JSON 反序列化器对LocalDateTime等复杂类型处理不正确。修复// ❌ 错误ShopshopBeanUtil.toBean(shopJson,Shop.class);// ✅ 正确ShopshopJSONUtil.toBean(shopJson,Shop.class);6.3 B 站学习倍速技巧F12 控制台输入 document.querySelector(video).playbackRate 3;突破网页 2 倍速限制复习用 3 倍速快速过已学内容。6.4 Java 双冒号::方法引用操作符用于 Lambda 表达式的简写。返回类型不确定时应使用泛型接收。7. 面试 FAQQ1Redis 是单线程的为什么这么快答Redis 基于内存操作核心命令采用单线程模型避免上下文切换和锁竞争使用 I/O 多路复用epoll实现高并发网络处理。Redis 6.0 之后引入多线程处理网络 I/O但命令执行仍然是单线程。Q2缓存穿透、击穿、雪崩的区别答穿透查不存在的数据 → 布隆过滤器 / 缓存空值击穿单个热点 key过期 → 互斥锁 / 逻辑过期雪崩大量 key同时过期或 Redis 宕机 → TTL 加随机值 / 集群 / 降级Q3分布式锁怎么实现有什么坑答Redis 用SET NX EXZookeeper 用临时顺序节点。主要坑① 锁续期业务未完成锁过期 → Redisson 看门狗② 误删锁释放时校验 UUID 线程 ID③ 可重入Redisson 用 Hash 计数器实现。Q4乐观锁和悲观锁怎么选答读多写少用乐观锁CAS写冲突频繁用悲观锁。秒杀扣库存场景用stock 0的宽松 CAS 即可不需严格版本号。Q5Cookie 和 Session 的关系分布式 Session 怎么解决答Cookie 是客户端存储Session 是服务端存储。分布式环境下可用 Redis 集中存储 SessionStringRedisTemplate通过 token 作为 key 实现多实例共享。Q6CountDownLatch 的作用答Java 并发工具让一个或多个线程等待其他线程完成操作后再继续执行。常用于模拟并发压测。

本月热点