太棒了!我们现在正式进入“达人探店”模块中最有意思、也是面试中最常考的互动功能——点赞(Like) 。
点赞功能看似只要在数据库里做个 +1 的操作,但一旦考虑到**“防刷赞(不能重复点赞)”和“前端状态高亮”**,纯靠 MySQL 就会面临巨大的性能瓶颈。这个时候,Redis 就要大显身手了。
📚 实战篇 03. 达人探店 - 点赞功能学习文档
一、 业务需求与痛点分析
1. 核心需求:
- 同一个用户,对同一篇探店笔记,只能点赞一次。
- 再次点击点赞按钮,应该执行取消点赞操作。
- 用户在进入笔记详情页或笔记列表页时,如果他已经点过赞了,前端的点赞大拇指图标需要高亮显示。
2. 为什么不用纯 MySQL 实现?
如果只用 MySQL,我们需要建一张 tb_blog_like 关联表(记录 blog_id 和 user_id)。
- 点赞时: 每次点赞都要去查这张表,判断有没有记录。如果有则删除并让
blog表的点赞数-1,如果没有则插入并让blog表点赞数+1。 - 查看笔记时: 用户每看一篇笔记,都要去这张巨大的关联表里
SELECT * WHERE blog_id = ? AND user_id = ?来判断是否需要高亮图标。 - 痛点: 这种高频的读写操作,会给关系型数据库带来极大的压力,极易成为系统瓶颈。
二、 破局方案:引入 Redis 的 Set 数据结构
为了解决高频读写和去重问题,我们可以将“谁点赞了这篇笔记”的信息存入 Redis。
Redis 的 Set(集合) 数据结构天然具备元素唯一性,非常适合用来做防重复校验。
-
Key 的设计:
blog:liked:{blogId}(例如:blog:liked:1001) -
Value 的设计: 存放所有给这篇笔记点过赞的
userId的集合。 -
核心命令:
- 判断是否点过赞:
SISMEMBER key userId - 点赞操作:
SADD key userId - 取消赞操作:
SREM key userId
- 判断是否点过赞:
三、 代码落地:核心逻辑改造
1. 实体类改造 (准备接收前端高亮状态)
首先,我们需要在 Blog 实体类中新增一个布尔类型的字段,用来告诉前端当前登录用户是否已经点过赞。
同样,这个字段在数据库中不存在,必须加上 @TableField(exist = false)。
Java
/**
* 当前用户是否点赞了该笔记 (用于前端图标高亮)
*/
@TableField(exist = false)
private Boolean isLike;
2. 核心接口:点赞 / 取消赞 (likeBlog)
当用户点击大拇指时,前端会调用这个接口。我们要利用 Redis 的 isMember 来决定是执行 +1 还是 -1。
Java
@Override
public Result likeBlog(Long id) {
// 1. 获取当前登录用户
Long userId = UserHolder.getUser().getId();
// 2. 判断当前用户是否已经点赞
String key = "blog:liked:" + id;
Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
if (Boolean.FALSE.equals(isMember)) {
// 3. 如果未点赞,可以点赞
// 3.1. 数据库点赞数 +1 (update tb_blog set liked = liked + 1 where id = ?)
boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update();
// 3.2. 保存用户到 Redis 的 Set 集合 (sadd key userId)
if (isSuccess) {
stringRedisTemplate.opsForSet().add(key, userId.toString());
}
} else {
// 4. 如果已点赞,取消点赞
// 4.1. 数据库点赞数 -1 (update tb_blog set liked = liked - 1 where id = ?)
boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();
// 4.2. 把用户从 Redis 的 Set 集合中移除 (srem key userId)
if (isSuccess) {
stringRedisTemplate.opsForSet().remove(key, userId.toString());
}
}
return Result.ok();
}
3. 查看详情接口改造 (判断并返回高亮状态)
上一节我们写了 queryBlogById,现在我们需要在返回给前端之前,加上查 Redis 判断是否高亮的逻辑。
Java
@Override
public Result queryBlogById(Long id) {
// 1. 查询探店笔记
Blog blog = getById(id);
if (blog == null) {
return Result.fail("笔记不存在!");
}
// 2. 查询并封装发布这篇笔记的用户信息 (上一节的逻辑)
queryBlogUser(blog);
// 3. 【核心新增】:查询当前登录用户是否点赞了该笔记
isBlogLiked(blog);
return Result.ok(blog);
}
/**
* 判断当前用户是否点赞
*/
private void isBlogLiked(Blog blog) {
// 注意:如果是未登录用户在随便逛,获取 User 会是 null,直接返回即可,不用查 Redis
UserDTO user = UserHolder.getUser();
if (user == null) {
return;
}
Long userId = user.getId();
String key = "blog:liked:" + blog.getId();
// 查询 Set 集合中是否包含该 userId
Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
// 将结果赋值给 blog 对象的 isLike 字段,传给前端
blog.setIsLike(Boolean.TRUE.equals(isMember));
}
四、 学习总结与架构进阶伏笔
通过这节的学习,你掌握了利用 Redis Set 数据结构的高性能查找特性(时间复杂度为 ),完美解决了高并发下的“防重复点赞”和“状态查询”问题。这套逻辑几乎可以直接照搬到任何需要点赞、收藏、关注的业务场景中。
⚠️ 进阶架构拷问:
现在的点赞功能看似完美,但产品经理突然提了一个新需求:
“我们要像微信朋友圈一样,在笔记的详情页下方,按点赞的时间先后顺序,展示最早点赞的 5 个用户的头像 (点赞排行榜) !”
此时,Set 结构面临致命打击!
因为 Set 是无序的!你把 userId 塞进 Set 里,你根本无法知道谁是第一个点的,谁是最后一个点的,完全无法实现时间排序的排行榜。
为了解决这个问题,我们需要对 Redis 的数据结构进行一次“升级”。准备好进入下一节,去看看如何利用 Redis 的 SortedSet (ZSet) 来实现完美的**“点赞排行榜”**了吗?