ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

从零构建高并发点赞系统:Spring Boot + Vue 3 全栈实战

2026/9/2 5:57:14 拓冰建站 浏览量
从零构建高并发点赞系统:Spring Boot + Vue 3 全栈实战 最近在开发一个社交类应用时遇到了一个看似简单却影响用户体验的“小”需求如何优雅地实现一个“喜欢/点赞”功能并让用户感受到即时、友好的互动反馈这个功能几乎是所有内容型产品的标配从微博、知乎到抖音无处不在。然而实现它不仅仅是前端加个爱心图标、后端加个计数那么简单。它涉及到前后端状态同步、高并发下的数据一致性、防刷机制、以及最重要的——如何通过技术手段传递出“喜欢吗我也喜欢”这种温暖、即时的情感共鸣。本文将从一个全栈开发者的视角手把手带你从零构建一个健壮、可扩展的“喜欢”功能。我们将覆盖从数据库设计、后端API开发、到前端交互与状态管理的完整闭环。无论你是刚入门全栈的新手还是希望优化现有点赞逻辑的开发者都能从中获得一套可直接复用于生产环境的解决方案。1. 功能核心概念与设计思路在动手写代码之前我们必须明确“喜欢”功能的业务边界和技术挑战。1.1 什么是“喜欢”功能“喜欢”Like在不同产品中也被称为“点赞”、“爱心”、“收藏”弱化版本。其核心业务逻辑是一个用户主体对一条内容客体表达一次正向的情感反馈。这次反馈通常会被记录并可能带来以下影响计数显示内容上会展示收到“喜欢”的总数。状态回显当前用户如果已经点过喜欢界面会高亮显示如红色爱心表明“已喜欢”。社交互动可能触发通知告诉内容创作者“有人喜欢了你的内容”。排序权重喜欢数可能作为内容热度因子影响其在列表中的排序。1.2 关键设计考量与挑战幂等性用户连续点击两次“喜欢”按钮结果应该是“取消喜欢”而不是重复计数。这是一个典型的“切换”Toggle操作必须保证幂等。实时性点击后前端状态爱心颜色、计数需要立刻更新给用户即时反馈即使后端请求可能稍有延迟乐观更新。高并发与数据一致性热门内容可能在瞬间收到成千上万个“喜欢”如何保证计数准确不发生少计或超计防刷与安全如何防止恶意脚本无限刷喜欢数需要引入用户认证和频率限制。扩展性未来是否要支持“踩”Dislike是否要区分“喜欢”的类型数据库和API设计需要预留扩展空间。基于以上分析我们将采用以下技术方案后端使用 Spring Boot 构建 RESTful API配合 MySQL 数据库。数据库使用两张表一张存储内容一张专门记录用户-喜欢的关联关系计数通过查询或缓存获取避免直接更新计数字段。并发控制利用数据库唯一索引防止重复插入使用 Redis 缓存热门内容的计数并配合异步落库应对高并发。前端使用 Vue 3 组合式 API 实现一个具有乐观更新功能的“喜欢”组件。2. 环境准备与版本说明在开始编码前请确保你的开发环境已就绪。以下是本文示例所使用的环境你可以根据实际情况调整。后端环境JDK: 17 或以上版本本文使用 Amazon Corretto 17构建工具: Maven 3.8IDE: IntelliJ IDEA 或 VS Code数据库: MySQL 8.0缓存: Redis 6.2前端环境Node.js: 18.x 或以上版本包管理器: npm 或 yarn框架: Vue 3.3配合 Vite 构建工具关键依赖版本在pom.xml中定义parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.1.5/version /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies3. 数据库设计与核心模型良好的数据结构是功能的基石。我们设计两张核心表。3.1 内容表 (post)存储被点赞的对象可以是文章、视频、评论等。CREATE TABLE post ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键ID, title varchar(255) DEFAULT NULL COMMENT 标题, content text COMMENT 内容, author_id bigint NOT NULL COMMENT 作者ID, created_at datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), KEY idx_author (author_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci COMMENT内容表;3.2 喜欢关系表 (user_like)这是核心表记录“谁”喜欢了“什么”。唯一索引是保证幂等性的关键。CREATE TABLE user_like ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键, user_id bigint NOT NULL COMMENT 用户ID, target_id bigint NOT NULL COMMENT 点赞目标ID (对应post.id), target_type varchar(50) NOT NULL DEFAULT POST COMMENT 点赞目标类型 (POST, COMMENT...), status tinyint NOT NULL DEFAULT 1 COMMENT 状态: 1-有效, 0-取消, created_at datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_user_target (user_id,target_id,target_type), -- 唯一索引防止重复点赞 KEY idx_target (target_id,target_type) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci COMMENT用户点赞关系表;设计解释uk_user_target唯一索引确保同一个用户对同一个目标只能有一条有效记录。当用户重复点击时我们使用ON DUPLICATE KEY UPDATE或先查后改的逻辑来切换status字段从而实现“点赞/取消”的切换。target_type字段为未来扩展预留可以支持对评论、视频等其他实体点赞。status字段采用软删除逻辑便于数据统计和恢复。4. 后端API实现Spring Boot 实战我们创建一个简单的 Spring Boot 应用实现点赞的核心 API。4.1 项目结构与实体类首先创建对应的 JPA 实体。1. Post 实体// 文件路径src/main/java/com/example/like/demo/entity/Post.java package com.example.like.demo.entity; import jakarta.persistence.*; import lombok.Data; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; import java.time.LocalDateTime; Entity Table(name post) Data public class Post { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; private String title; Lob private String content; Column(name author_id, nullable false) private Long authorId; CreationTimestamp Column(name created_at, updatable false) private LocalDateTime createdAt; UpdateTimestamp Column(name updated_at) private LocalDateTime updatedAt; }2. UserLike 实体// 文件路径src/main/java/com/example/like/demo/entity/UserLike.java package com.example.like.demo.entity; import jakarta.persistence.*; import lombok.Data; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; import java.time.LocalDateTime; Entity Table(name user_like, uniqueConstraints { UniqueConstraint(columnNames {userId, targetId, targetType}) }) // JPA方式声明唯一约束 Data public class UserLike { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(name user_id, nullable false) private Long userId; Column(name target_id, nullable false) private Long targetId; Column(name target_type, nullable false) private String targetType POST; // 默认类型为POST Column(nullable false) private Integer status 1; // 1-有效0-取消 CreationTimestamp Column(name created_at, updatable false) private LocalDateTime createdAt; UpdateTimestamp Column(name updated_at) private LocalDateTime updatedAt; }4.2 数据访问层Repository创建 Spring Data JPA 仓库接口。// 文件路径src/main/java/com/example/like/demo/repository/UserLikeRepository.java package com.example.like.demo.repository; import com.example.like.demo.entity.UserLike; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; import java.util.Optional; public interface UserLikeRepository extends JpaRepositoryUserLike, Long { // 根据用户、目标查找点赞记录 OptionalUserLike findByUserIdAndTargetIdAndTargetType(Long userId, Long targetId, String targetType); // 统计某个目标的有效点赞数 Query(SELECT COUNT(ul) FROM UserLike ul WHERE ul.targetId :targetId AND ul.targetType :targetType AND ul.status 1) Long countByTargetIdAndTargetType(Param(targetId) Long targetId, Param(targetType) String targetType); // 更新点赞状态取消或重新点赞 Modifying Transactional Query(UPDATE UserLike ul SET ul.status :status, ul.updatedAt CURRENT_TIMESTAMP WHERE ul.userId :userId AND ul.targetId :targetId AND ul.targetType :targetType) int updateStatus(Param(userId) Long userId, Param(targetId) Long targetId, Param(targetType) String targetType, Param(status) Integer status); }4.3 业务逻辑层ServiceService 层封装核心的点赞/取消逻辑并处理并发和数据一致性问题。// 文件路径src/main/java/com/example/like/demo/service/LikeService.java package com.example.like.demo.service; import com.example.like.demo.entity.UserLike; import com.example.like.demo.repository.UserLikeRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Optional; import java.util.concurrent.TimeUnit; Service RequiredArgsConstructor Slf4j public class LikeService { private final UserLikeRepository userLikeRepository; private final StringRedisTemplate redisTemplate; // Redis Key 模板 private static final String LIKE_COUNT_KEY like:count:%s:%s; // like:count:POST:123 /** * 点赞或取消点赞 * param userId 用户ID * param targetId 目标ID * param targetType 目标类型 * return 操作后的状态true-已点赞false-未点赞 */ Transactional public boolean toggleLike(Long userId, Long targetId, String targetType) { // 1. 先尝试查询现有记录 OptionalUserLike existingLikeOpt userLikeRepository.findByUserIdAndTargetIdAndTargetType(userId, targetId, targetType); if (existingLikeOpt.isPresent()) { // 2. 记录存在切换状态 UserLike existingLike existingLikeOpt.get(); Integer newStatus existingLike.getStatus() 1 ? 0 : 1; int updatedRows userLikeRepository.updateStatus(userId, targetId, targetType, newStatus); if (updatedRows 0) { // 3. 更新Redis缓存中的计数 updateLikeCountInRedis(targetId, targetType, newStatus 1 ? 1 : -1); return newStatus 1; } } else { // 4. 记录不存在创建新的点赞记录 try { UserLike newLike new UserLike(); newLike.setUserId(userId); newLike.setTargetId(targetId); newLike.setTargetType(targetType); newLike.setStatus(1); userLikeRepository.save(newLike); // 5. 更新Redis缓存中的计数 updateLikeCountInRedis(targetId, targetType, 1); return true; // 点赞成功 } catch (DataIntegrityViolationException e) { // 6. 捕获唯一约束冲突极短时间内重复请求 log.warn(Duplicate like request detected for user {} on target {}., userId, targetId); // 冲突意味着记录已存在重新查询状态 return userLikeRepository.findByUserIdAndTargetIdAndTargetType(userId, targetId, targetType) .map(ul - ul.getStatus() 1) .orElse(false); } } return false; } /** * 获取点赞总数优先从Redis读取 */ public Long getLikeCount(Long targetId, String targetType) { String key String.format(LIKE_COUNT_KEY, targetType, targetId); String countStr redisTemplate.opsForValue().get(key); if (countStr ! null) { // 缓存命中 return Long.parseLong(countStr); } else { // 缓存未命中从数据库查询并写入缓存 Long countFromDB userLikeRepository.countByTargetIdAndTargetType(targetId, targetType); redisTemplate.opsForValue().set(key, String.valueOf(countFromDB), 1, TimeUnit.HOURS); // 缓存1小时 return countFromDB; } } /** * 获取用户对某个目标的点赞状态 */ public boolean getLikeStatus(Long userId, Long targetId, String targetType) { return userLikeRepository.findByUserIdAndTargetIdAndTargetType(userId, targetId, targetType) .map(ul - ul.getStatus() 1) .orElse(false); } /** * 原子化地更新Redis中的点赞计数 */ private void updateLikeCountInRedis(Long targetId, String targetType, long delta) { String key String.format(LIKE_COUNT_KEY, targetType, targetId); redisTemplate.opsForValue().increment(key, delta); // 可以设置一个较短的过期时间或者不设置由定时任务同步到DB redisTemplate.expire(key, 1, TimeUnit.HOURS); } }4.4 控制层REST API提供简洁的 API 接口供前端调用。// 文件路径src/main/java/com/example/like/demo/controller/LikeController.java package com.example.like.demo.controller; import com.example.like.demo.service.LikeService; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.Map; RestController RequestMapping(/api/like) RequiredArgsConstructor public class LikeController { private final LikeService likeService; /** * 点赞/取消点赞 * POST /api/like/toggle?targetId123targetTypePOST * 请求头需携带用户认证信息如JWT Token这里简化处理从请求参数获取userId */ PostMapping(/toggle) public MapString, Object toggleLike(RequestParam Long targetId, RequestParam(defaultValue POST) String targetType, RequestHeader(X-User-Id) Long userId) { // 实际应从Token解析 boolean liked likeService.toggleLike(userId, targetId, targetType); Long currentCount likeService.getLikeCount(targetId, targetType); MapString, Object result new HashMap(); result.put(liked, liked); result.put(count, currentCount); result.put(targetId, targetId); return result; } /** * 获取点赞状态和总数 * GET /api/like/status?targetId123targetTypePOST */ GetMapping(/status) public MapString, Object getLikeStatus(RequestParam Long targetId, RequestParam(defaultValue POST) String targetType, RequestHeader(X-User-Id) Long userId) { boolean liked likeService.getLikeStatus(userId, targetId, targetType); Long count likeService.getLikeCount(targetId, targetType); MapString, Object result new HashMap(); result.put(liked, liked); result.put(count, count); return result; } }注意在实际项目中用户ID应从认证信息如JWT Token中安全获取而不是通过请求头传递。此处为简化演示。4.5 应用配置配置数据库和Redis连接。# 文件路径src/main/resources/application.yml spring: datasource: url: jdbc:mysql://localhost:3306/like_demo?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update # 开发环境生产环境应使用none或validate并通过SQL脚本管理表结构 show-sql: true properties: hibernate: format_sql: true redis: host: localhost port: 6379 database: 0 timeout: 2000ms server: port: 80805. 前端实现Vue 3 交互组件后端API准备好后我们创建一个具有良好用户体验的前端“喜欢”按钮组件。5.1 项目初始化与依赖使用 Vite 快速创建一个 Vue 3 项目。npm create vuelatest like-frontend # 按照提示选择项目配置本文使用 TypeScript Pinia cd like-frontend npm install npm install axios # 用于HTTP请求5.2 喜欢按钮组件 (LikeButton.vue)这是一个独立的、可复用的组件。!-- 文件路径src/components/LikeButton.vue -- template button clickhandleLikeClick :disabledloading :class[like-button, { liked, loading }] aria-label点赞 span classicon-wrapper !-- 使用SVG图标更具灵活性 -- svg v-ifliked classicon liked-icon viewBox0 0 24 24 fillcurrentColor path dM12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z/ /svg svg v-else classicon viewBox0 0 24 24 fillnone strokecurrentColor stroke-width2 path dM12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z/ /svg /span span classcount :class{ count-updated: countUpdated } {{ formattedCount }} /span /button /template script setup langts import { ref, computed, onMounted } from vue; import axios from axios; const props defineProps{ targetId: number | string; targetType?: string; initialLiked?: boolean; initialCount?: number; }(); const emit defineEmits{ (e: likeToggled, payload: { liked: boolean; count: number }): void; }(); const liked ref(props.initialLiked || false); const count ref(props.initialCount || 0); const loading ref(false); const countUpdated ref(false); // 格式化计数显示超过1000显示为1k const formattedCount computed(() { if (count.value 1000) { return (count.value / 1000).toFixed(1).replace(/\.0$/, ) k; } return count.value.toString(); }); // 处理点击事件乐观更新 请求 const handleLikeClick async () { if (loading.value) return; loading.value true; const oldLiked liked.value; const oldCount count.value; // 1. 乐观更新立即更新UI给用户即时反馈 liked.value !oldLiked; count.value oldLiked ? oldCount - 1 : oldCount 1; countUpdated.value true; setTimeout(() (countUpdated.value false), 300); // 动画效果 try { // 2. 发送请求到后端 const response await axios.post( http://localhost:8080/api/like/toggle, null, // POST body为空 { params: { targetId: props.targetId, targetType: props.targetType || POST, }, headers: { X-User-Id: 1, // 模拟用户ID实际应从登录状态获取 }, } ); const data response.data; // 3. 请求成功用后端返回的真实数据同步一次防止乐观更新与后端不一致 liked.value data.liked; count.value data.count; // 4. 触发自定义事件通知父组件 emit(likeToggled, { liked: liked.value, count: count.value }); } catch (error) { console.error(点赞操作失败:, error); // 5. 请求失败回滚乐观更新 liked.value oldLiked; count.value oldCount; // 可以在这里添加用户提示例如使用Toast alert(操作失败请稍后重试); } finally { loading.value false; } }; // 组件挂载时可以主动获取一次状态 onMounted(async () { try { const response await axios.get(http://localhost:8080/api/like/status, { params: { targetId: props.targetId, targetType: props.targetType || POST, }, headers: { X-User-Id: 1, }, }); const data response.data; liked.value data.liked; count.value data.count; } catch (error) { console.error(获取点赞状态失败:, error); } }); /script style scoped .like-button { display: inline-flex; align-items: center; gap: 6px; padding: 8px 16px; border: 1px solid #ddd; border-radius: 20px; background-color: #fff; color: #555; font-size: 14px; cursor: pointer; transition: all 0.2s ease; user-select: none; } .like-button:hover:not(.loading) { border-color: #f08; background-color: #fef5f9; } .like-button.liked { border-color: #f08; color: #f08; background-color: #fef5f9; } .like-button.loading { opacity: 0.7; cursor: not-allowed; } .icon-wrapper { display: flex; align-items: center; } .icon { width: 18px; height: 18px; } .liked-icon { fill: #f08; /* 点赞后的填充色 */ } .count { font-weight: 600; transition: transform 0.3s ease; } .count-updated { transform: scale(1.2); color: #f08; } /style5.3 在页面中使用组件在App.vue或任何页面组件中引入并使用。!-- 文件路径src/App.vue -- template div classapp h1“喜欢吗我也喜欢” - 功能演示/h1 div classpost-card h2一篇很棒的技术文章/h2 p这里是文章的内容摘要.../p div classactions !-- 使用LikeButton组件 -- LikeButton :target-idpostId target-typePOST :initial-likedfalse :initial-count42 like-toggledhandleLikeToggled / span classhint试试点击它感受即时反馈/span /div div v-iflastAction classfeedback 你刚刚 {{ lastAction.liked ? 点赞了 : 取消了点赞 }}当前总数{{ lastAction.count }} /div /div /div /template script setup langts import { ref } from vue; import LikeButton from ./components/LikeButton.vue; const postId ref(123); // 模拟的文章ID const lastAction ref{ liked: boolean; count: number } | null(null); const handleLikeToggled (payload: { liked: boolean; count: number }) { lastAction.value payload; // 在实际应用中这里可以触发更多逻辑如显示通知、更新列表等 console.log(点赞状态变化:, payload); }; /script style .app { font-family: Arial, sans-serif; max-width: 600px; margin: 40px auto; padding: 20px; } .post-card { border: 1px solid #eee; border-radius: 12px; padding: 24px; margin-top: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); } .actions { display: flex; align-items: center; gap: 15px; margin-top: 20px; } .hint { font-size: 12px; color: #888; } .feedback { margin-top: 15px; padding: 10px; background-color: #f0f9ff; border-radius: 6px; color: #0066cc; } /style6. 运行与验证6.1 启动后端服务确保 MySQL 和 Redis 服务已启动并配置正确。在 IDE 中运行 Spring Boot 主类LikeDemoApplication或使用命令mvn spring-boot:run。服务启动后访问http://localhost:8080应能看到 Whitelabel Error Page正常因为我们没写首页说明服务已运行。6.2 启动前端应用进入前端项目目录like-frontend。运行npm run dev。根据终端输出访问开发服务器地址通常是http://localhost:5173。6.3 功能测试打开前端页面你会看到一个带有爱心图标和计数的按钮。点击按钮观察以下变化爱心图标应立即变为红色填充状态。计数应立即增加1并有一个轻微的放大动画。控制台网络标签中应能看到一个发送到http://localhost:8080/api/like/toggle的 POST 请求。按钮状态变为“已点赞”。再次点击按钮观察变化爱心图标变回轮廓状态。计数减少1。状态恢复为“未点赞”。刷新页面点赞状态和计数应被正确保持从后端API重新获取。7. 常见问题与排查思路在实际开发中你可能会遇到以下问题问题现象可能原因排查思路与解决方案点击按钮无任何反应1. 前端事件未绑定。2. 按钮被disabled。检查浏览器控制台是否有JS错误。检查click事件绑定和loading状态。点击后UI更新但很快回滚后端API请求失败乐观更新被回滚。1. 检查网络请求是否成功状态码200。2. 查看后端日志确认业务逻辑是否出错如数据库连接失败。3. 检查API路径、参数、请求头是否正确。重复点击导致计数错误1. 前端未防止重复提交。2. 后端并发处理有漏洞。1. 前端确保loading状态生效。2. 后端依赖数据库唯一索引 (uk_user_target) 是最终防线确保其生效。3. 检查后端toggleLike方法的事务和异常处理逻辑。计数显示为负数Redis 缓存中的计数与数据库不一致。1. 检查updateLikeCountInRedis方法逻辑确保delta值正确1或-1。2. 实现一个定时任务定期将 Redis 计数同步回数据库并修复差异。其他用户看不到点赞数更新1. 前端未轮询或使用WebSocket。2. 计数缓存未及时失效。1. 对于实时性要求高的场景考虑使用 WebSocket (如 Socket.IO) 广播点赞事件。2. 缩短 Redis 计数缓存的过期时间或在使用计数时强制回源查询。数据库出现重复的user_like记录唯一索引未生效或代码逻辑绕过。1. 确认数据库表UNIQUE KEY uk_user_target已创建成功。2. 在代码中插入前先查询 (findByUserIdAndTargetIdAndTargetType) 是常见做法但在极高并发下仍可能插入重复最终需靠唯一索引约束和异常处理 (DataIntegrityViolationException) 兜底。8. 最佳实践与进阶优化上面的实现是一个基础且可用的版本。对于生产环境还需要考虑更多。8.1 性能优化计数缓存策略我们使用了 Redis 缓存计数。对于超级热点的内容如顶级网红视频可以考虑使用更高效的数据结构如 Redis 的Hash来存储多个目标的计数。引入本地缓存如 Caffeine配合 Redis 形成二级缓存。异步落库点赞操作可以拆解为“写关系表”和“更新计数”。可以将“更新计数”这个操作放入消息队列如 RabbitMQ、Kafka异步处理大幅提高写接口的响应速度和解耦。数据库优化user_like表会随着时间急剧增长。需考虑按时间进行分表如按月分表。建立合适的索引(target_id, target_type)用于查计数(user_id)用于查询用户喜欢列表。8.2 可扩展性设计支持多种实体我们设计了target_type字段未来可以轻松支持对评论、用户、视频等点赞。抽象服务层可以定义一个Likeable接口让不同的实体类实现它服务层通过策略模式来处理不同实体的点赞逻辑。开放API当前的API是内部格式。对外提供时应遵循 RESTful 规范使用标准的 HTTP 状态码并考虑 API 版本管理。8.3 安全与风控用户认证必须使用安全的认证方式如 JWT在服务器端从 Token 中解析用户ID绝不能信任前端传来的用户ID。频率限制防止刷赞。可以在网关层或使用RateLimiter注解对/api/like/toggle接口进行限流例如每个用户每分钟最多操作60次。业务风控识别异常行为例如同一个IP在短时间内对大量不同目标点赞可能触发风控规则进行验证码挑战或临时限制。8.4 前端体验增强防抖与节流虽然组件有loading状态但在网络极慢时用户可能连续点击。可以加入防抖函数确保一定时间内只发送一次请求。动画效果使用 CSS 或动画库如 GSAP为爱心图标添加更丰富的点击动画如脉动、飘散效果提升情感化设计。离线支持在 PWA 或需要离线能力的场景中可以将点赞操作暂存到 IndexedDB待网络恢复后同步到服务器。通过以上步骤我们不仅实现了一个功能完整的“喜欢”系统更深入探讨了其背后的技术细节、潜在问题和优化方向。从数据库的唯一索引保证数据唯一性到后端的幂等设计再到前端的乐观更新提供即时反馈每一个环节都影响着最终的用户体验。