☰
高并发点赞系统设计:从数据库到缓存与异步架构的工程实践
2026/9/25 16:50:49 网站建设 项目流程

在实际开发中,我们经常需要处理用户偏好、点赞、收藏这类“喜欢”行为。无论是社交应用的内容点赞、电商平台的商品收藏,还是内容推荐系统的兴趣标记,其背后的技术模型和实现路径都高度相似。很多开发者初次接触时,可能会简单地设计一个“用户-对象”关联表,但很快就会发现,随着业务增长,诸如高并发点赞的性能瓶颈、重复操作的幂等性、喜欢列表的分页查询效率、以及如何将“喜欢”数据用于个性化推荐等问题会接踵而至。

本文将以构建一个稳健、可扩展的“喜欢”系统为核心主线,不仅带你从零设计数据库和API,更会深入探讨在高并发场景下的技术选型、缓存策略、数据一致性保障等工程实践。适合正在或即将开发用户互动功能的中后端开发者,无论你使用的是Java Spring Boot、Python Django还是Go Gin,本文所阐述的设计思想和解决方案都具有普适性。我们将从概念模型出发,逐步完成环境搭建、核心实现、压力测试和常见问题排查,最终形成一个可用于学习并借鉴到生产环境的设计方案。

1. 理解“喜欢”系统的核心模型与挑战

在动手写代码之前,我们必须先厘清“喜欢”这个行为在数据层面的本质,以及它可能带来的技术挑战。这有助于我们做出更合理的技术决策,避免后期重构。

1.1 数据关系模型:多对多的连接

“用户喜欢某个实体(如文章、视频、商品)”,这是一个典型的多对多关系。一个用户可以喜欢多个实体,一个实体也可以被多个用户喜欢。在关系型数据库中,这通常通过三张表来体现:

  • 用户表 (users):存储用户基本信息。
  • 实体表 (targets):存储被喜欢对象的信息。这里用targets泛指文章、视频等,实践中可能对应posts、videos等具体表。
  • 喜欢关系表 (likes):作为连接表,记录用户与实体之间的喜欢关系。这是系统的核心表。

其ER关系可以简单表示为:users <- likes -> targets。

1.2 核心表结构设计

likes表的设计至关重要,它至少需要以下字段:

CREATE TABLE `likes` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键', `user_id` bigint(20) NOT NULL COMMENT '用户ID', `target_type` varchar(50) NOT NULL COMMENT '目标类型,如 post, video, product', `target_id` bigint(20) NOT NULL COMMENT '目标ID', `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态:1-喜欢,0-取消', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_user_target` (`user_id`,`target_type`,`target_id`), KEY `idx_target` (`target_type`,`target_id`,`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户喜欢关系表';

关键字段解释:

  • target_type&target_id: 使用“类型+ID”的复合字段来标识被喜欢的对象,这是一种通用的多态设计,避免了为每种实体创建单独的关联表。target_type建议使用枚举值。
  • uk_user_target: 唯一索引。这是保证数据一致性的生命线,确保同一个用户对同一个对象只能有一条记录,防止重复喜欢。
  • idx_target: 联合索引。用于高效查询某个实体被哪些用户喜欢(粉丝列表),或查询某个实体的喜欢总数(count操作)。
  • status: 状态字段。用于软删除或取消喜欢,避免物理删除记录,便于数据统计和恢复。

1.3 面临的主要技术挑战

如果只是实现基础功能,上述表结构加上简单的CRUD即可。但在真实生产环境中,我们必须考虑以下挑战:

  1. 高并发写入:热门内容可能在瞬间收到成千上万的点赞请求,直接对数据库行进行update或insert会造成严重的锁竞争,导致接口超时。
  2. 计数实时性与准确性:用户需要实时看到点赞数更新。频繁使用SELECT COUNT(*) FROM likes WHERE target_id=?查询,在数据量大时效率极低,且在高并发下计数可能不准确。
  3. 列表查询性能:查询“我喜欢的列表”或“这篇文章的点赞用户列表”时,如果涉及分页和用户信息连表查询,性能会随着数据增长而下降。
  4. 数据一致性:当引入缓存来提升性能后,如何保证缓存中的计数与数据库中的真实数据一致,成为一个难题。
  5. 业务扩展性:未来可能增加“点赞时间线”、“共同喜欢”等衍生功能,数据结构需要能够支撑。

2. 环境准备与项目骨架搭建

我们将以一个基于Spring Boot的RESTful API服务为例进行实现。你可以轻松地将核心思想迁移到其他技术栈。

2.1 基础环境与依赖

确保你的开发环境已安装:

  • JDK 8 或 11
  • Maven 3.6+
  • MySQL 5.7+ 或 PostgreSQL
  • Redis 5.0+ (用于缓存和计数器)
  • IDE (IntelliJ IDEA 或 Eclipse)

创建一个新的Spring Boot项目,在pom.xml中添加必要依赖:

<dependencies> <!-- Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 数据访问 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!-- 缓存 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- 工具 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- 测试 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>

2.2 配置文件与数据库初始化

在application.yml中配置数据源和Redis:

spring: datasource: url: jdbc:mysql://localhost:3306/like_system?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update # 初期开发使用,生产环境应改为 validate 或 none,并使用Flyway/Liquibase show-sql: true properties: hibernate: dialect: org.hibernate.dialect.MySQL5InnoDBDialect format_sql: true redis: host: localhost port: 6379 password: # 如果有密码则填写 database: 0 lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0

运行项目,JPA的ddl-auto: update会根据实体类自动创建users和likes表(targets表需根据你的业务自行创建)。但更推荐使用SQL文件手动初始化核心表结构,以确保索引等细节完全符合设计。

3. 核心实现:从基础CRUD到异步优化

我们将分阶段实现,首先完成基础功能,然后逐步引入缓存和异步处理来应对高并发场景。

3.1 定义实体与枚举

首先定义Like实体,映射到数据库表。

package com.example.likesystem.domain; import lombok.Data; import javax.persistence.*; import java.time.LocalDateTime; @Entity @Table(name = "likes", uniqueConstraints = { @UniqueConstraint(columnNames = {"userId", "targetType", "targetId"}) }) @Data public class Like { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false) private Long userId; @Column(nullable = false, length = 50) @Enumerated(EnumType.STRING) // 存储枚举的字符串值,如 "POST" private TargetType targetType; @Column(nullable = false) private Long targetId; @Column(nullable = false) private Integer status = 1; // 1-有效,0-取消 @Column(updatable = false) private LocalDateTime createdAt; private LocalDateTime updatedAt; @PrePersist protected void onCreate() { createdAt = LocalDateTime.now(); updatedAt = LocalDateTime.now(); } @PreUpdate protected void onUpdate() { updatedAt = LocalDateTime.now(); } }

定义目标类型枚举:

package com.example.likesystem.domain; public enum TargetType { POST, // 文章 VIDEO, // 视频 COMMENT, // 评论 PRODUCT // 商品 // 可根据业务扩展 }

3.2 实现基础Repository与Service

创建LikeRepository,利用JPA实现基础查询。

package com.example.likesystem.repository; import com.example.likesystem.domain.Like; import com.example.likesystem.domain.TargetType; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Optional; public interface LikeRepository extends JpaRepository<Like, Long> { // 查找用户对某个目标的喜欢记录 Optional<Like> findByUserIdAndTargetTypeAndTargetId(Long userId, TargetType targetType, Long targetId); // 检查用户是否喜欢了某个目标 boolean existsByUserIdAndTargetTypeAndTargetIdAndStatus(Long userId, TargetType targetType, Long targetId, Integer status); // 统计某个目标的喜欢数量(有效状态) Long countByTargetTypeAndTargetIdAndStatus(TargetType targetType, Long targetId, Integer status); // 查询用户喜欢的目标ID列表(分页) @Query("SELECT l.targetId FROM Like l WHERE l.userId = :userId AND l.targetType = :targetType AND l.status = 1 ORDER BY l.createdAt DESC") List<Long> findLikedTargetIdsByUser(Long userId, TargetType targetType, org.springframework.data.domain.Pageable pageable); // 取消喜欢(软删除) @Transactional @Modifying @Query("UPDATE Like l SET l.status = 0, l.updatedAt = CURRENT_TIMESTAMP WHERE l.userId = :userId AND l.targetType = :targetType AND l.targetId = :targetId AND l.status = 1") int cancelLike(Long userId, TargetType targetType, Long targetId); }

实现基础的服务层LikeService,处理喜欢/取消喜欢的核心逻辑。

package com.example.likesystem.service; import com.example.likesystem.domain.Like; import com.example.likesystem.domain.TargetType; import com.example.likesystem.repository.LikeRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Optional; @Service @Slf4j @RequiredArgsConstructor public class LikeService { private final LikeRepository likeRepository; /** * 喜欢某个目标 * @return true-喜欢成功,false-已喜欢过 */ @Transactional public boolean like(Long userId, TargetType targetType, Long targetId) { // 1. 检查是否已喜欢 Optional<Like> existingLike = likeRepository.findByUserIdAndTargetTypeAndTargetId(userId, targetType, targetId); if (existingLike.isPresent()) { Like like = existingLike.get(); if (like.getStatus() == 1) { return false; // 已经喜欢,无需重复操作 } else { // 之前取消过,重新激活 like.setStatus(1); likeRepository.save(like); return true; } } // 2. 创建新的喜欢记录 Like newLike = new Like(); newLike.setUserId(userId); newLike.setTargetType(targetType); newLike.setTargetId(targetId); newLike.setStatus(1); try { likeRepository.save(newLike); return true; } catch (DataIntegrityViolationException e) { // 并发请求下,可能唯一索引冲突,捕获后视为已喜欢 log.warn("Duplicate like attempt detected: userId={}, target={}-{}", userId, targetType, targetId); return false; } } /** * 取消喜欢 * @return true-取消成功,false-未喜欢过 */ @Transactional public boolean cancel(Long userId, TargetType targetType, Long targetId) { int updatedRows = likeRepository.cancelLike(userId, targetType, targetId); return updatedRows > 0; } /** * 检查是否喜欢 */ public boolean isLiked(Long userId, TargetType targetType, Long targetId) { return likeRepository.existsByUserIdAndTargetTypeAndTargetIdAndStatus(userId, targetType, targetId, 1); } /** * 获取喜欢数量 */ public Long getLikeCount(TargetType targetType, Long targetId) { return likeRepository.countByTargetTypeAndTargetIdAndStatus(targetType, targetId, 1); } }

这个基础版本已经实现了功能,但在高并发下直接操作数据库会成为瓶颈。接下来我们引入缓存。

3.3 引入Redis缓存优化计数与状态

喜欢数和用户是否喜欢的状态是读多写少的数据,非常适合用缓存。我们使用Redis的Hash和String结构。

缓存设计:

  • 喜欢数缓存:String类型,Key格式为like:count:{targetType}:{targetId},Value为计数值。
  • 用户喜欢状态缓存:Hash类型,一个大Key存储所有用户状态,例如like:status:{targetType}:{targetId},Field是userId,Value是1或0。也可以为每个用户单独设Key,但Hash结构更节省内存,且能一次性获取所有喜欢该目标的用户。

创建一个LikeCacheService来封装缓存操作。

package com.example.likesystem.service; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import java.util.concurrent.TimeUnit; @Service @RequiredArgsConstructor public class LikeCacheService { private final RedisTemplate<String, Object> redisTemplate; private static final String LIKE_COUNT_KEY_PREFIX = "like:count:"; private static final String LIKE_STATUS_KEY_PREFIX = "like:status:"; // 喜欢数缓存操作 public void incrementCount(String targetType, Long targetId) { String key = LIKE_COUNT_KEY_PREFIX + targetType + ":" + targetId; redisTemplate.opsForValue().increment(key, 1); redisTemplate.expire(key, 7, TimeUnit.DAYS); // 设置过期时间,避免冷数据常驻内存 } public void decrementCount(String targetType, Long targetId) { String key = LIKE_COUNT_KEY_PREFIX + targetType + ":" + targetId; Long value = redisTemplate.opsForValue().decrement(key, 1); // 防止计数减到负数 if (value != null && value < 0) { redisTemplate.opsForValue().set(key, 0); } } public Long getCount(String targetType, Long targetId) { String key = LIKE_COUNT_KEY_PREFIX + targetType + ":" + targetId; Object val = redisTemplate.opsForValue().get(key); return val == null ? null : Long.parseLong(val.toString()); } // 用户状态缓存操作 public void setUserStatus(String targetType, Long targetId, Long userId, Integer status) { String key = LIKE_STATUS_KEY_PREFIX + targetType + ":" + targetId; redisTemplate.opsForHash().put(key, userId.toString(), status.toString()); redisTemplate.expire(key, 7, TimeUnit.DAYS); } public Integer getUserStatus(String targetType, Long targetId, Long userId) { String key = LIKE_STATUS_KEY_PREFIX + targetType + ":" + targetId; Object val = redisTemplate.opsForHash().get(key, userId.toString()); return val == null ? null : Integer.parseInt(val.toString()); } // 删除整个状态缓存(当计数严重不一致时,可强制刷新) public void deleteStatusCache(String targetType, Long targetId) { String key = LIKE_STATUS_KEY_PREFIX + targetType + ":" + targetId; redisTemplate.delete(key); } }

然后,改造LikeService,在数据库操作的同时更新缓存。这里需要注意缓存与数据库的一致性。我们采用“先更新数据库,再删除/更新缓存”的策略(Cache-Aside pattern)。对于计数,我们使用增量更新,因为顺序不重要;对于状态,直接覆盖。

// 在LikeService中注入LikeCacheService private final LikeCacheService likeCacheService; @Transactional public boolean like(Long userId, TargetType targetType, Long targetId) { // ... 原有的数据库检查逻辑 ... // 在成功插入或更新数据库后 if (success) { // 更新缓存 likeCacheService.incrementCount(targetType.name(), targetId); likeCacheService.setUserStatus(targetType.name(), targetId, userId, 1); } return success; } @Transactional public boolean cancel(Long userId, TargetType targetType, Long targetId) { // ... 原有的数据库更新逻辑 ... if (updatedRows > 0) { likeCacheService.decrementCount(targetType.name(), targetId); likeCacheService.setUserStatus(targetType.name(), targetId, userId, 0); return true; } return false; } public boolean isLiked(Long userId, TargetType targetType, Long targetId) { // 先查缓存 Integer cachedStatus = likeCacheService.getUserStatus(targetType.name(), targetId, userId); if (cachedStatus != null) { return cachedStatus == 1; } // 缓存未命中,查数据库 boolean dbStatus = likeRepository.existsByUserIdAndTargetTypeAndTargetIdAndStatus(userId, targetType, targetId, 1); // 回写缓存 likeCacheService.setUserStatus(targetType.name(), targetId, userId, dbStatus ? 1 : 0); return dbStatus; } public Long getLikeCount(TargetType targetType, Long targetId) { // 先查缓存 Long cachedCount = likeCacheService.getCount(targetType.name(), targetId); if (cachedCount != null) { return cachedCount; } // 缓存未命中,查数据库 Long dbCount = likeRepository.countByTargetTypeAndTargetIdAndStatus(targetType, targetId, 1); // 回写缓存 if (dbCount != null) { // 这里需要原子操作,避免并发回写覆盖。简单做法是直接set,因为count查询本身是准确的。 redisTemplate.opsForValue().set(LIKE_COUNT_KEY_PREFIX + targetType.name() + ":" + targetId, dbCount, 7, TimeUnit.DAYS); } return dbCount; }

3.4 应对超高并发:消息队列与异步落库

在极端场景下(如顶流直播、热搜话题),瞬时点赞请求可能高达每秒数万次。即使有缓存,频繁的数据库INSERT操作和缓存更新仍然是巨大压力。此时可以引入消息队列(如RabbitMQ, Kafka, RocketMQ)进行异步削峰填谷。

流程改造:

  1. 用户点击喜欢,服务端校验基础参数(用户身份、目标存在性)后,立即返回“操作成功”。
  2. 将点赞事件(userId,targetType,targetId,action,timestamp)发送到消息队列。
  3. 独立的消费者服务从队列中消费消息,执行实际的数据库写入和缓存更新。

这种做法的优点是极大提高了接口的吞吐量和响应速度,将数据库压力平摊到时间轴上。缺点是数据一致性变为最终一致性,用户可能在极短时间内看不到计数更新(取决于消费延迟)。

这里以Spring AMQP为例,展示发送端的改造:

// LikeService中注入AmqpTemplate private final AmqpTemplate amqpTemplate; public boolean likeAsync(Long userId, TargetType targetType, Long targetId) { // 1. 快速校验(如用户是否存在,目标是否存在) // 2. 检查内存/Redis中的短期重复请求(例如5秒内同一用户对同一目标只接受一次请求),防止消息队列被刷爆 String duplicateKey = "like:req:dup:" + userId + ":" + targetType + ":" + targetId; Boolean canProcess = redisTemplate.opsForValue().setIfAbsent(duplicateKey, "1", 5, TimeUnit.SECONDS); if (Boolean.FALSE.equals(canProcess)) { return false; // 短时间内重复请求 } // 3. 发送消息到队列 LikeEvent event = new LikeEvent(userId, targetType, targetId, "LIKE", System.currentTimeMillis()); amqpTemplate.convertAndSend("like.exchange", "like.routing.key", event); // 4. 先更新本地缓存(预增加),提升用户体验 likeCacheService.incrementCount(targetType.name(), targetId); likeCacheService.setUserStatus(targetType.name(), targetId, userId, 1); return true; // 告诉前端操作已接受 }

消费者服务则需要保证消息处理的幂等性(因为消息可能重复投递),并处理好数据库与缓存的一致性问题。

4. 接口设计与性能验证

4.1 RESTful API设计

创建LikeController,提供对外的HTTP接口。

package com.example.likesystem.controller; import com.example.likesystem.domain.TargetType; import com.example.likesystem.service.LikeService; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/v1/likes") @RequiredArgsConstructor public class LikeController { private final LikeService likeService; @PostMapping("/{targetType}/{targetId}") public ApiResponse<Boolean> like(@RequestHeader("X-User-Id") Long userId, @PathVariable TargetType targetType, @PathVariable Long targetId) { boolean success = likeService.like(userId, targetType, targetId); return ApiResponse.success(success); } @DeleteMapping("/{targetType}/{targetId}") public ApiResponse<Boolean> cancel(@RequestHeader("X-User-Id") Long userId, @PathVariable TargetType targetType, @PathVariable Long targetId) { boolean success = likeService.cancel(userId, targetType, targetId); return ApiResponse.success(success); } @GetMapping("/{targetType}/{targetId}/status") public ApiResponse<Boolean> getStatus(@RequestHeader("X-User-Id") Long userId, @PathVariable TargetType targetType, @PathVariable Long targetId) { boolean isLiked = likeService.isLiked(userId, targetType, targetId); return ApiResponse.success(isLiked); } @GetMapping("/{targetType}/{targetId}/count") public ApiResponse<Long> getCount(@PathVariable TargetType targetType, @PathVariable Long targetId) { Long count = likeService.getLikeCount(targetType, targetId); return ApiResponse.success(count); } }

ApiResponse是一个简单的通用响应包装类。

4.2 使用JMeter进行压力测试

为了验证优化效果,我们需要进行压力测试。使用JMeter模拟高并发点赞场景。

  1. 测试计划:创建线程组,设置线程数(如500)、循环次数(如100)、Ramp-Up时间(如5秒)。
  2. HTTP请求:配置服务器地址、端口、路径(如POST /api/v1/likes/POST/123),并添加X-User-Id请求头,值使用随机变量(如${__Random(1,10000)})模拟不同用户。
  3. 监听器:添加“查看结果树”、“聚合报告”、“图形结果”监听器。
  4. 对比测试:
    • 场景A(无缓存):注释掉LikeService中所有缓存操作代码,直接压测数据库。
    • 场景B(有缓存):启用缓存,观察QPS(每秒查询率)和平均响应时间。
    • 场景C(异步):启用异步消息队列,观察接口响应时间(会非常短),然后观察消费者服务的数据处理速率。

预期结果:场景A的QPS最低,响应时间随并发上升而急剧增加,数据库CPU飙升。场景B的QPS显著提升,响应时间稳定。场景C的接口QPS和响应时间最优,但数据一致性延迟。

5. 常见问题排查与解决方案

在实际开发和运维中,你会遇到各种问题。下表列出了一些典型问题及其排查路径。

问题现象可能原因排查步骤解决方案与建议
点赞数偶尔不准确(少1或多1)1. 缓存与数据库不一致。
2. 并发请求导致计数更新竞态条件。
3. 消息丢失或重复消费。
1. 检查Redis中计数Key的值。
2. 查询数据库likes表对应记录的精确数量。
3. 查看消息队列的消费监控和死信队列。
1.最终一致性:定期(如每5分钟)跑一个补偿任务,用数据库真实数据覆盖缓存。
2.原子操作:使用Redis的INCR/DECR命令,它们是原子的。
3.消息幂等:消费者根据(userId, targetType, targetId, action)生成唯一ID,处理前先查重。
“喜欢/取消”接口返回成功,但状态没变1. 唯一索引冲突,被异常捕获后误认为成功。
2. 缓存更新失败(如Redis连接超时)。
3. 异步消息未被消费。
1. 查看应用日志,是否有DataIntegrityViolationException警告。
2. 检查Redis连接状态和内存使用情况。
3. 查看消息队列监控,确认消息是否堆积。
1. 优化like方法,在唯一索引冲突时,改为查询当前状态并返回。
2. 增加Redis操作的重试机制和降级策略(如更新失败则记录日志,后续补偿)。
3. 确保消费者服务高可用,并设置合理的重试和告警机制。
查询“我的喜欢列表”非常慢1.likes表在user_id上缺少索引。
2. 分页查询深页码(如limit 10000,20)性能差。
3. 关联查询了targets表的大字段(如内容)。
1. 使用EXPLAIN分析SQL执行计划。
2. 检查慢查询日志。
1. 确保(user_id, target_type, status)上有联合索引,并按created_at倒序。
2. 使用“游标分页”(Cursor-based Pagination),基于created_at和id进行查询,避免OFFSET。
3. 列表查询只返回目标ID和基础信息,详情通过单独接口按需查询。
高峰期Redis内存暴涨1. 缓存Key未设置过期时间。
2. 热门目标的状态Hash过大(存储了所有点赞用户ID)。
1. 使用redis-cli info memory查看内存详情。
2. 使用redis-cli --bigkeys分析大Key。
1.必须为所有缓存Key设置合理的TTL(如7天)。
2. 对于超大Hash,考虑分片存储,或只缓存最近N个点赞用户,全量数据走数据库查询。
数据库likes表体积增长过快1. 只有插入,没有清理。
2. “取消喜欢”是软删除,记录仍存在。
1. 查询表数据量增长趋势。1. 建立归档机制,将超过一定时间(如3年)的status=0(取消)的记录迁移到历史表。
2. 定期清理测试数据。

6. 生产环境最佳实践与扩展方向

6.1 必须遵循的最佳实践清单

  1. 索引是命脉:确保(user_id, target_type, target_id)的唯一索引,以及(target_type, target_id, status)的查询索引。定期使用EXPLAIN检查慢查询。
  2. 缓存必须有过期时间:防止冷数据无限期占用内存。根据业务活跃度设置TTL,通常1-30天。
  3. 做好降级和熔断:如果Redis或数据库不可用,服务应能降级(如直接返回“服务繁忙”或从数据库查询),避免雪崩。使用Resilience4j或Hystrix等组件。
  4. 监控与告警:监控关键指标:数据库QPS、连接数、慢查询;Redis内存使用率、连接数、命中率;应用服务的错误率、接口响应时间P99。
  5. 数据补偿:设计一个定时任务,定期对比热门内容的缓存计数与数据库计数,差异超过阈值时进行修复。
  6. 接口限流与防刷:在网关或应用层对/like接口进行限流(如每秒每用户最多10次),防止恶意刷赞。

6.2 可能的扩展方向

  1. 点赞热度榜:结合点赞数、时间衰减因子,使用Redis的ZSET实现实时热度排序,用于“今日热门”等榜单。
  2. 点赞消息通知:在点赞成功后,发布一个领域事件,由专门的通知服务消费,向被点赞者发送站内信或推送。
  3. “共同喜欢”推荐:基于用户-物品的喜欢矩阵,使用协同过滤算法,计算用户相似度,实现“关注了TA的人也喜欢”的推荐。
  4. 数据统计分析:将点赞流水同步到数据仓库(如ClickHouse),分析用户活跃时段、内容受欢迎趋势等,为运营提供数据支持。
  5. 多级缓存:对于极端热点的内容(如首页置顶帖),可以在应用本地内存(如Caffeine)中再缓存一层计数,进一步减少Redis访问。

从简单的关联表到支撑高并发的异步化架构,“喜欢”系统是一个经典的、能深入考察开发者对数据一致性、缓存设计和系统扩展性理解的场景。在实现时,切忌一开始就追求大而全的复杂方案,而应根据业务的实际并发量和数据规模,从简单可靠的版本开始,随着业务增长,沿着“数据库 -> 本地缓存 -> 分布式缓存 -> 异步消息 -> 数据分区”的路径逐步演进。每次演进前,都需要用真实的压力测试数据来佐证瓶颈所在,做到有的放矢。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询