城食记 (ChengShiJi):Redis 在高并发场景下的 6 大核心应用实战
项目背景:「城食记」是我在深入学习 Java 高并发编程后,设计并实现的一个本地生活服务平台(类似大众点评 / 美团)。在开发过程中,为了解决高并发下的热点数据查询、库存超卖、用户状态管理等问题,我将 Redis 的应用深度融入到了业务架构中。
用户登录与状态管理:基于 Token 的分布式 Session
在单体应用中,我们习惯用 HttpSession 存储用户状态。但在「城食记」的设计中,为了支持分布式部署和水平扩展,我采用了 Redis + Token 的无状态登录方案。
核心流程
发送验证码:用户输入手机号,后端生成验证码存入 Redis(Key:
login:code:{phone}),设置 5 分钟过期。登录校验:
- 校验手机号和 Redis 中的验证码。
- 若用户不存在则自动注册。
- 生成 UUID 作为 Token。
- 将用户信息(UserDTO)转为 Hash 结构存入 Redis(Key:
login:token:{token}),设置 30 分钟过期。
状态保持:
- 前端获取 Token 后存储在 LocalStorage,并在后续请求头(Header)中携带。
- 后端通过拦截器(Interceptor)解析 Header 中的 Token,从 Redis 获取用户信息并存入
ThreadLocal,供后续业务使用。
技术亮点
- 无状态设计:服务端不保存用户状态,完全依赖 Redis,易于水平扩展。
- 安全性:Token 设置过期时间,即使泄露也有有效期限制。
店铺缓存:解决三大缓存问题
店铺详情页是「城食记」的高频访问场景,直接查询数据库压力巨大。我在项目中实现了多级缓存策略,并针对经典的缓存问题设计了对应的解决方案。
缓存穿透 (Cache Penetration)
问题:黑客或恶意用户频繁请求不存在的 ID(如
-1),导致请求直接打穿缓存到数据库。我的解决方案:缓存空对象。
- 当数据库查询为空时,仍将该 Key 存入 Redis,Value 为空字符串
"",并设置较短的过期时间(如 1 分钟)。 - 后续请求该 ID 会直接命中 Redis 的空值,保护数据库。
- 当数据库查询为空时,仍将该 Key 存入 Redis,Value 为空字符串
缓存击穿 (Cache Breakdown)
问题:一个热点 Key(如超级大 V 的店铺)突然过期,此时大量并发请求涌入,瞬间压垮数据库。
我的解决方案 A:互斥锁 (Mutex Lock)
- 利用 Redis 的
SETNX实现分布式锁。 - 只有获取到锁的线程能去查询数据库并重建缓存,其他线程等待重试。
- 利用 Redis 的
我的解决方案 B:逻辑过期 (Logical Expire)
——
项目最终采用方案
- 缓存数据不过期(TTL = -1),但在 Value 中嵌入一个过期时间字段。
- 线程查询时发现逻辑过期,直接返回旧数据,并开启一个独立线程(线程池)去后台重建缓存。
- 优点:无锁,性能极高,不阻塞用户请求。
缓存雪崩 (Cache Avalanche)
- 问题:大量 Key 在同一时间过期。
- 我的解决方案:给缓存过期时间添加 随机值(Random TTL),分散过期时间点。
分布式锁:基于 Redis 的并发控制
在秒杀、库存扣减等场景中,单机锁(Synchronized/ReentrantLock)失效,必须使用分布式锁。
简易版分布式锁
利用 SETNX (Set If Not Exists) 命令:
1 | SET lock:key unique_id NX PX 10000 |
NX:不存在才设置。
PX:自动过期时间(防止死锁)。
完善的分布式锁 (Redisson)
手写 Lua 脚本虽然可行,但维护成本高。在「城食记」中,我引入了 Redisson 框架,它提供了更强大的功能:
- 可重入锁:利用 Hash 记录线程 ID 和重入次数。
- 锁续期 (WatchDog):如果业务执行时间超过锁的过期时间,Redisson 会自动延长锁的有效期,防止锁提前释放。
- 公平锁:保证请求的顺序性。
点赞与排行榜:ZSet 的妙用
点赞功能需要支持:点赞 / 取消、按时间排序显示点赞列表、统计点赞数。
数据结构选择
我选择了 ZSet (Sorted Set) 来实现这一功能:
- Key:
blog:liked:{blogId} - Value (Member):
userId - Score:
System.currentTimeMillis()(时间戳)
核心操作
- 点赞:
ZADD(添加用户 ID 和当前时间戳)。 - 取消点赞:
ZREM(移除用户 ID)。 - 判断是否点赞:
ZSCORE(返回 null 表示未点赞)。 - 点赞排行榜:
ZREVRANGE(按 Score 倒序排列,即最新点赞在前)。 - 点赞数:
ZCARD(获取集合元素数量)。
签到功能:Bitmap 位图
用户签到是一个典型的 “存粹记录状态” 的场景,且数据量极大。
为什么用 Bitmap?
如果用关系型数据库存,一个用户一年 365 条记录,数据量会非常大。
在「城食记」中,我使用了 Bitmap,用一个比特位(bit)表示一天的状态(0 未签,1 已签)。
- 一年 365 天 = 365 bits ≈ 46 Bytes。
- 极大节省内存空间。
核心实现
Key:
sign:{userId}:{yyyyMM}Offset:
dayOfMonth - 1(当月的第几天,从 0 开始)签到:
SETBIT key offset 1统计连续签到天数
:
- 使用
BITFIELD命令读取当月截至今天的所有 bit 位。 - 返回一个十进制数字,通过位运算(
& 1和>> 1)从后往前遍历,统计末尾连续 1 的个数。
- 使用
秒杀与异步下单:Stream 消息队列
秒杀业务的核心挑战是高并发读写和超卖问题。
核心流程
校验:用户资格及库存预判断(Redis)。
扣减库存:利用 Redis 的
DECR原子操作扣减库存。异步下单
:
- 传统做法是直接操作数据库创建订单,容易造成拥堵。
- 优化:将下单信息(用户 ID、商品 ID)发送到 Redis Stream 队列。
- 后台开启一个线程(消费者),从 Stream 中拉取消息,异步写入 MySQL 创建订单。
技术价值
- 削峰填谷:瞬间的高并发写入变成了队列的平稳消费。
- 解耦:秒杀服务只负责资格校验和入队,订单服务负责落库。
项目总结
通过「城食记」这个项目,我不仅仅是完成了业务功能的开发,更重要的是将 Redis 的各种数据结构和特性深度结合到了实际场景中:
- 缓存设计不仅仅是
set和get,更重要的是解决一致性和异常问题(穿透、击穿、雪崩)。 - 数据结构选型的重要性:String 做缓存,Hash 存对象,ZSet 做排序和计数,Bitmap 做海量状态压缩,Stream 做消息队列。
- 分布式思维:在多机部署环境下,锁、Session、事务都需要特殊的处理机制。
说些什么吧!