文章中只是部分摘要,建议大家去看原文档,笔记文档地址:https://wwzdcv5m5b4.feishu.cn/wiki/Djy3wb2XNiZiMRkKlA4cxYr9nkb?from=from_copylink
一、登录状态管理:从“单店会员卡”到“全国一卡通”
在传统的单体应用中,我们习惯把用户登录状态存在 Tomcat 的 Session 里。但这就像办了一张“单店会员卡”,一旦你的服务扩展成集群,用户被负载均衡路由到另一台机器,状态就丢失了,导致用户频繁掉线。
解决方案与落地痛点:为了解决集群下的状态失效,我们把登录信息抽离出来,存到独立的 Redis 空间中。这就像把单店系统升级成了云端的“全国一卡通”。
第一步是发送验证码:校验手机号后生成6位随机码,以 phone 为 key 存入 Redis 并设置有效期。
第二步是登录校验:拿用户输入的 code 与 Redis 中的 cacheCode 比对,一致则通过,并将用户信息存入 Redis 辅助后续权限校验。
工程实战中的巧思:双层拦截器这里有个极易踩坑的点:并非所有接口都需要登录拦截,但未登录的访问(比如逛首页)也需要刷新 Session 存活时间。因此,我们不能一刀切,而是设计了双层拦截器:
TokenRefreshInterceptor:拦截所有路径
/,专门负责给已经携带有效 Token 的用户续期。LoginInterceptor:只拦截特定需要权限的路径(如发帖、点赞),检查是否真的登录了,没登录则拦截。 最后,通过这两层拦截器协同,就能优雅地对不同请求进行精准处理。
二、缓存设计与“三大皆空”的防线
为了提高商户信息的查询速度,我们会把数据存进 Redis。但数据库是活的,随时会改,这就带来了数据不一致的风险。实战经验:更新数据库时,一定要先更新数据库,再删除 Redis 缓存。下次访问自然会去 DB 查最新数据并重建缓存。
但引入缓存后,我们还得面对面试官最爱问的“缓存三大坑”。我把它们总结在一张表里:
| 异常场景 | 痛点描述 (工程体感) | 应对方案 |
| 缓存穿透 | 攻击者用大量数据库根本不存在的恶意 ID 狂刷接口,请求直接“穿透”缓存打到 DB。 | 方案 A:缓存短期的空值,拦截在 Redis 层,但大量随机攻击会耗费 Redis 内存。 方案 B:布隆过滤器,预计算合法 key 指纹,但有误判率。 |
| 缓存击穿 | 某个超级爆款的“热点 Key”突然失效,导致海量并发请求同时去查 DB 试图重建缓存。 | 方案 A:互斥锁,只让第一个拿到锁的线程去查库,其余等待,适合一致性要求高的场景。 方案 B:逻辑过期,数据永不主动过期,发现逻辑时间超时后,尝试获取分布式锁异步更新。 |
| 缓存雪崩 | 大量缓存 Key 同时过期,或者 Redis 直接宕机,请求如雪崩般压垮 DB。 | 基础 TTL 上加上一个 random(0, 300) 秒的随机时间,避免扎堆。如果是分布式架构,还需配合 Sentinel 限流、Hystrix 熔断以及多级缓存。 |
三、秒杀架构优化:把“作坊手工”变成“流水线作业”
爆款优惠券秒杀,是检验架构功底的试金石。
优化前的朴素方案(作坊手工): 一个简单的秒杀逻辑通常包含:查库存 -> 判重复 -> 扣库存 -> 插订单。每一次请求需要进行 3 次 DB 读和 2 次 DB 写,总共 5 次 DB 操作。如果来了 10000 并发,那就是 50000 次操作,MySQL 会直接崩溃。此外,异步并发读写还会导致库存出现负数(超卖现象)。
优化后的四层异步架构(流水线作业): 怎么解?咱们得把同步阻塞变成异步解耦。
第一道防线:Lua 脚本原子校验。把最关键的“判库存 + 判重复 + 扣库存 + 记用户”打包成一个原子操作,在 Redis 内存里极速执行,拦截无效请求。优化后,响应时间从 ~200ms 降到了 ~0.5ms,快了成百上千倍。
第二道防线:全局 ID 生成器。由于不能阻塞等待 MySQL 返回自增 ID,我们用 Redis 的自增特性拼接秒级时间戳,预先生成一个 64 位的高性能全局唯一订单 ID。
第三层解耦:立即返回响应。一旦 Lua 校验通过,不管后台怎么处理,接口层面立刻给用户返回“秒杀成功”,绝不让用户干等 DB 写入。
第四层削峰:Kafka 队列与幂等校验。把订单发往 Kafka,前端的海量请求化作队列里匀速滴下的水滴,保护了 DB 永远不被冲垮。写 DB 时,利用乐观锁(
WHERE stock > 0)做最终兜底防超卖。同时,利用count(user_id, voucher_id)做幂等检查,防止 Kafka 重复投递导致的一人多单。
四、灵魂拷问:你真的懂分布式锁吗?
在处理缓存击穿时,我们引入了 Redis 分布式锁(setIfAbsent)。但在复杂的工程环境中,这把锁其实危机四伏。
危机 1:锁被误删。你抢到锁,但业务执行太慢,锁到期自动释放了。别人拿到了新锁,结果你业务执行完了,随手一个
del把别人的锁给删了。解法:用 Lua 脚本,把“判断是不是我的锁”和“删除”变成原子操作。
危机 2:不可重入死锁。同一个线程里的方法 A 加了锁,调用方法 B 时又想加同一把锁,结果自己把自己锁死了。
解法:利用 Redis 的 Hash 结构,不仅存线程标识,还存一个重入次数,用 Lua 脚本增加锁的深度。
危机 3:业务没跑完,锁没了。这就好比上厕所,规定只能用 10 分钟,但你拉肚子需要 15 分钟,10 分钟一到门开了,全乱套了。
解法:引入看门狗(Watchdog)机制。开启一个定时任务,只要你的线程还活着,它就每隔一段时间(比如 TTL/3)自动帮你把锁的有效期续满。如果你的服务器宕机挂了,看门狗自然也就停止续期,锁到期自动释放,完美避免死锁。
灵魂伪代码:看看看门狗是如何运作的
Java// 每 timeout/3 秒续期一次 renewalTask = WATCHDOG.scheduleAtFixedRate(() -> { if (unlocked) return; // 已经释放就停止 // 执行 Lua 脚本:如果这把锁还是我的,就重置过期时间 (EXPIRE) Long result = stringRedisTemplate.execute(RENEWAL_SCRIPT, ...); if (执行失败) { renewalTask.cancel(false); } // 锁已掉,取消续期任务 }, interval, interval, TimeUnit.SECONDS);
架构设计从来都不是纸上谈兵,每一个精妙的方案背后,都是无数次系统崩溃带来的血泪教训。希望这份干货总结能帮你少走一些弯路。