需求背景
最近有个 APP 功能迭代,产品经理提了个看似简单却容易踩坑的需求:用户首次进入'程序员树洞'模块时,必须展示一份隐私协议,勾选后才能进入;后续再次访问则直接跳过。这个逻辑听起来 trivial,但一旦涉及高并发或海量用户,简单的数据库方案就会暴露出性能瓶颈。
需求分析
先把业务拆解一下,核心关注点其实就三个:
- 状态持久化:用户的勾选状态需要被记录,不能因为重启服务就丢失。
- 读取高频:每次打开功能都要判断是否显示弹窗,读操作远多于写操作。
- 写入低频:绝大多数用户只勾选一次,重复写入是浪费资源。
如果直接用 MySQL 存一张 user_agreement 表,虽然稳妥,但每次请求都要查库。当用户量上来后,这张表的查询压力会迅速增大,而且为了加锁防止并发冲突,数据库的吞吐量可能会成为瓶颈。这时候,Redis 的优势就体现出来了。
为什么选 Redis?
Redis 作为内存数据库,读写速度极快,天然适合这种'读多写少'且对一致性要求不是绝对强一致的场景。更重要的是,它提供了一些原子性命令和过期策略,能帮我们省去很多代码层面的判断。
方案对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| MySQL | 数据持久化强,事务支持好 | 高并发下 IO 压力大,查询慢 |
| Redis | 速度极快,支持原子操作 | 内存成本高,需处理持久化 |
在这个场景下,我们不需要像订单系统那样保证强一致性,只要保证用户不重复看协议即可。Redis 完全够用。
实战实现
Key 的设计
Key 的命名要清晰且唯一。建议采用 feature:agreement:{userId} 这样的格式。这样既能隔离不同功能,又方便后续按前缀批量删除或统计。
核心逻辑
这里有两个关键点:一是如何确保只写一次,二是如何处理过期(比如协议更新后需要重新展示)。
1. 原子写入
使用 SETNX (Set if Not Exists) 是最经典的做法。如果 Key 不存在,设置成功并返回 1;如果已存在,说明用户已经看过,直接忽略。
// 伪代码示例
String key = "feature:agreement:" + userId;
Boolean success = redisTemplate.opsForValue().setIfAbsent(key, "accepted", 365, TimeUnit.DAYS);
if (!success) {
// 用户已同意,直接放行
return showFeaturePage();
}
注意这里的 setIfAbsent 配合 TTL 一起用,既保证了原子性,又设置了有效期。如果协议版本更新了,我们可以把 Key 删掉或者修改 Value 里的版本号,下次再进来就是新的了。


