实战篇 03. 达人探店 - 点赞功能学习文档

3 阅读5分钟

太棒了!我们现在正式进入“达人探店”模块中最有意思、也是面试中最常考的互动功能——点赞(Like)

点赞功能看似只要在数据库里做个 +1 的操作,但一旦考虑到**“防刷赞(不能重复点赞)”“前端状态高亮”**,纯靠 MySQL 就会面临巨大的性能瓶颈。这个时候,Redis 就要大显身手了。


📚 实战篇 03. 达人探店 - 点赞功能学习文档

一、 业务需求与痛点分析

1. 核心需求:

  • 同一个用户,对同一篇探店笔记,只能点赞一次
  • 再次点击点赞按钮,应该执行取消点赞操作。
  • 用户在进入笔记详情页或笔记列表页时,如果他已经点过赞了,前端的点赞大拇指图标需要高亮显示

2. 为什么不用纯 MySQL 实现?

如果只用 MySQL,我们需要建一张 tb_blog_like 关联表(记录 blog_iduser_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 数据结构的高性能查找特性(时间复杂度为 O(1)O(1)),完美解决了高并发下的“防重复点赞”和“状态查询”问题。这套逻辑几乎可以直接照搬到任何需要点赞、收藏、关注的业务场景中。


⚠️ 进阶架构拷问:

现在的点赞功能看似完美,但产品经理突然提了一个新需求:

“我们要像微信朋友圈一样,在笔记的详情页下方,按点赞的时间先后顺序,展示最早点赞的 5 个用户的头像 (点赞排行榜) !”

此时,Set 结构面临致命打击!

因为 Set 是无序的!你把 userId 塞进 Set 里,你根本无法知道谁是第一个点的,谁是最后一个点的,完全无法实现时间排序的排行榜。

为了解决这个问题,我们需要对 Redis 的数据结构进行一次“升级”。准备好进入下一节,去看看如何利用 Redis 的 SortedSet (ZSet) 来实现完美的**“点赞排行榜”**了吗?