Spring Boot实战:社交应用用户关系系统设计与内容过滤实现
2026/8/4 18:36:51 网站建设 项目流程

最近在开发一个社区类应用时,遇到了一个非常典型的场景:如何优雅地处理用户之间的关注、拉黑等社交关系,并在内容流中实现精准的过滤。比如,用户A不希望看到用户B(一个被戏称为“老头”的用户)发布的任何内容,甚至希望系统能“劝诫”用户B不要与特定用户互动。这背后涉及的核心技术,就是关系型数据建模实时过滤策略

本文将深入探讨如何设计一个高可用、可扩展的用户关系系统,以应对诸如“sakura你不要跟那个老头在一起啊”这类复杂的社交过滤需求。我们将从数据库表设计出发,涵盖关系定义、状态流转、查询优化,并最终给出一个Spring Boot微服务实战案例,包含完整的代码和API设计。无论你是正在构建社交功能的后端开发者,还是对数据模型设计感兴趣的同学,都能从中获得一套可直接复用的解决方案。

1. 核心概念与业务场景分析

在深入代码之前,我们首先要厘清几个核心概念和需要解决的业务问题。

1.1 什么是用户关系系统?

用户关系系统是社交平台的核心组件之一,它负责定义和管理用户之间的双向连接状态。常见的状态包括:

  • 无关系:双方未建立任何连接。
  • 关注:单向或双向的关注行为,是社交图谱的基础。
  • 好友:一种特殊的双向关注关系,通常需要对方确认。
  • 拉黑/屏蔽:一种强过滤关系,用户A拉黑用户B后,A将看不到B的任何内容,且B通常无法与A互动(如评论、私信)。
  • 特别关注:在关注基础上的强化关系,用于优先展示其内容。

本文重点要解决的“不希望用户A和用户B互动”的场景,主要涉及拉黑关系,并可能结合内容流的过滤规则

1.2 “sakura”与“老头”场景拆解

我们以一个具体场景为例:“用户sakura”不希望“用户老头”与“用户C”在一起(互动)。从系统层面,这可以分解为几个子需求:

  1. 关系定义:sakura可以拉黑“老头”。这是最直接的操作,拉黑后,sakura的时间线将过滤掉“老头”的内容。
  2. 内容过滤:sakura在浏览全局内容或“用户C”的主页时,系统需要过滤掉所有“老头”参与的内容(发帖、评论、点赞)。
  3. 干预提示(可选):这是一个更复杂的业务逻辑。当“老头”试图与“用户C”进行特定互动(如评论、私信)时,系统可能需要根据sakura设置的某种“规则”进行提醒或拦截。这属于高级业务规则,本文会提供扩展思路。

1.3 系统设计目标

  • 高效查询:能快速判断任意两个用户之间的关系。
  • 状态清晰:关系状态(关注、拉黑)明确,且支持状态转换(如从关注变为拉黑)。
  • 可扩展:易于添加新的关系类型或属性。
  • 实时性:关系变更能较快地影响到内容过滤。

2. 数据库表结构设计

设计良好的数据模型是系统稳定的基石。我们将采用一种经典且灵活的设计方案。

2.1 核心关系表 (user_relationship)

这张表记录每对用户之间的具体关系。采用“宽表”设计,将不同关系作为独立的字段,便于查询和更新。

-- 文件路径:数据库初始化脚本 /schema/table_user_relationship.sql CREATE TABLE `user_relationship` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', `from_user_id` bigint(20) NOT NULL COMMENT '关系发起方用户ID', `to_user_id` bigint(20) NOT NULL COMMENT '关系接收方用户ID', -- 关系状态字段 `is_following` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否关注,0:否,1:是', `is_friend` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否为好友,0:否,1:是', `is_blocked` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否拉黑,0:否,1:是', `is_special` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否特别关注,0:否,1:是', -- 元数据 `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间', PRIMARY KEY (`id`), -- 唯一索引,确保同一对用户只有一条记录 UNIQUE KEY `uk_from_to` (`from_user_id`,`to_user_id`), -- 复合索引,优化根据发起方查询各种关系的性能 KEY `idx_from_user_status` (`from_user_id`, `is_following`, `is_blocked`), -- 复合索引,优化根据接收方查询被谁拉黑等场景 KEY `idx_to_user_blocked` (`to_user_id`, `is_blocked`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户关系表';

设计解读

  • from_user_idto_user_id组成唯一键,防止重复记录。关系是有向的,A关注B和B关注A是两条记录。
  • 使用tinyint(1)表示布尔状态,查询和更新效率高。
  • 索引设计是关键:
    • uk_from_to:保证数据唯一性,也是关系查询的基础。
    • idx_from_user_status:当我们需要查询“用户A关注了哪些人”或“用户A拉黑了哪些人”时,这个索引能极大提升查询速度。
    • idx_to_user_blocked:用于查询“用户B被哪些人拉黑了”,在内容过滤时,如果采用“反向查询”策略会用到。

2.2 用户表与内容表(关联表)

为了完成整个示例,我们还需要最基础的用户表和内容表。

-- 文件路径:/schema/table_user.sql CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名', `nickname` varchar(100) DEFAULT NULL COMMENT '昵称', `status` tinyint(4) DEFAULT '1' COMMENT '状态', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; -- 文件路径:/schema/table_post.sql CREATE TABLE `post` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL COMMENT '发布者ID', `content` text COMMENT '内容', `visibility` tinyint(4) DEFAULT '1' COMMENT '可见性:1公开,2私密,3好友可见', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='动态/帖子表';

3. 环境准备与项目搭建

我们将使用 Spring Boot 2.7.x 和 MyBatis-Plus 3.5.x 来快速构建后端服务。

3.1 技术栈与版本

  • JDK: 1.8 或 11
  • Spring Boot: 2.7.18
  • MyBatis-Plus: 3.5.4
  • MySQL: 5.7 或 8.0
  • Maven: 3.6+

3.2 项目初始化与依赖

使用 Spring Initializr 或 IDE 创建项目,主要依赖如下:

<!-- 文件路径:pom.xml --> <dependencies> <!-- Spring Boot Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- MyBatis-Plus --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.4</version> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!-- Lombok --> <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>

3.3 核心配置

配置数据库连接和 MyBatis-Plus。

# 文件路径:src/main/resources/application.yml spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/social_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: yourpassword mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 控制台打印SQL,生产环境关闭 global-config: db-config: logic-delete-field: deleted # 全局逻辑删除字段名(如果表有) logic-delete-value: 1 logic-not-delete-value: 0

4. 核心代码实现

4.1 实体类定义

首先定义与数据库表对应的实体类。

// 文件路径:src/main/java/com/example/social/entity/UserRelationship.java package com.example.social.entity; import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.time.LocalDateTime; @Data @TableName("user_relationship") public class UserRelationship { @TableId(type = IdType.AUTO) private Long id; private Long fromUserId; private Long toUserId; private Boolean isFollowing; private Boolean isFriend; private Boolean isBlocked; private Boolean isSpecial; @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; }
// 文件路径:src/main/java/com/example/social/entity/User.java package com.example.social.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.time.LocalDateTime; @Data @TableName("user") public class User { @TableId(type = IdType.AUTO) private Long id; private String username; private String nickname; private Integer status; private LocalDateTime createTime; }

4.2 Mapper 与 Service 层

使用 MyBatis-Plus 的 BaseMapper 可以快速实现基础CRUD。

// 文件路径:src/main/java/com/example/social/mapper/UserRelationshipMapper.java package com.example.social.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.social.entity.UserRelationship; import org.apache.ibatis.annotations.Mapper; @Mapper public interface UserRelationshipMapper extends BaseMapper<UserRelationship> { // 可以在此定义复杂查询方法 }

服务层封装核心业务逻辑,特别是关系状态的更新。

// 文件路径:src/main/java/com/example/social/service/UserRelationshipService.java package com.example.social.service; import com.baomidou.mybatisplus.extension.service.IService; import com.example.social.entity.UserRelationship; public interface UserRelationshipService extends IService<UserRelationship> { /** * 关注用户 */ boolean follow(Long fromUserId, Long toUserId); /** * 取消关注 */ boolean unfollow(Long fromUserId, Long toUserId); /** * 拉黑用户 */ boolean block(Long fromUserId, Long toUserId); /** * 取消拉黑 */ boolean unblock(Long fromUserId, Long toUserId); /** * 查询关系 */ UserRelationship getRelationship(Long fromUserId, Long toUserId); }
// 文件路径:src/main/java/com/example/social/service/impl/UserRelationshipServiceImpl.java package com.example.social.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.example.social.entity.UserRelationship; import com.example.social.mapper.UserRelationshipMapper; import com.example.social.service.UserRelationshipService; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; @Slf4j @Service public class UserRelationshipServiceImpl extends ServiceImpl<UserRelationshipMapper, UserRelationship> implements UserRelationshipService { @Override @Transactional(rollbackFor = Exception.class) public boolean follow(Long fromUserId, Long toUserId) { if (fromUserId.equals(toUserId)) { throw new IllegalArgumentException("不能关注自己"); } LambdaQueryWrapper<UserRelationship> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(UserRelationship::getFromUserId, fromUserId) .eq(UserRelationship::getToUserId, toUserId); UserRelationship relationship = this.getOne(queryWrapper); if (relationship == null) { // 创建新关系记录 relationship = new UserRelationship(); relationship.setFromUserId(fromUserId); relationship.setToUserId(toUserId); relationship.setIsFollowing(true); relationship.setIsBlocked(false); // 关注时确保不是拉黑状态 return this.save(relationship); } else { // 更新现有记录 if (Boolean.TRUE.equals(relationship.getIsBlocked())) { log.warn("用户{}已拉黑用户{},无法关注。请先取消拉黑。", fromUserId, toUserId); return false; } relationship.setIsFollowing(true); relationship.setUpdateTime(LocalDateTime.now()); return this.updateById(relationship); } } @Override @Transactional(rollbackFor = Exception.class) public boolean unfollow(Long fromUserId, Long toUserId) { LambdaUpdateWrapper<UserRelationship> updateWrapper = new LambdaUpdateWrapper<>(); updateWrapper.eq(UserRelationship::getFromUserId, fromUserId) .eq(UserRelationship::getToUserId, toUserId) .set(UserRelationship::getIsFollowing, false); return this.update(updateWrapper); } @Override @Transactional(rollbackFor = Exception.class) public boolean block(Long fromUserId, Long toUserId) { if (fromUserId.equals(toUserId)) { throw new IllegalArgumentException("不能拉黑自己"); } LambdaQueryWrapper<UserRelationship> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(UserRelationship::getFromUserId, fromUserId) .eq(UserRelationship::getToUserId, toUserId); UserRelationship relationship = this.getOne(queryWrapper); if (relationship == null) { relationship = new UserRelationship(); relationship.setFromUserId(fromUserId); relationship.setToUserId(toUserId); relationship.setIsBlocked(true); relationship.setIsFollowing(false); // 拉黑时自动取消关注 return this.save(relationship); } else { relationship.setIsBlocked(true); relationship.setIsFollowing(false); // 拉黑即取消关注 relationship.setUpdateTime(LocalDateTime.now()); return this.updateById(relationship); } } @Override public boolean unblock(Long fromUserId, Long toUserId) { LambdaUpdateWrapper<UserRelationship> updateWrapper = new LambdaUpdateWrapper<>(); updateWrapper.eq(UserRelationship::getFromUserId, fromUserId) .eq(UserRelationship::getToUserId, toUserId) .set(UserRelationship::getIsBlocked, false); return this.update(updateWrapper); } @Override public UserRelationship getRelationship(Long fromUserId, Long toUserId) { LambdaQueryWrapper<UserRelationship> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(UserRelationship::getFromUserId, fromUserId) .eq(UserRelationship::getToUserId, toUserId); return this.getOne(queryWrapper); } }

关键逻辑说明

  • followblock方法实现了“创建或更新”的逻辑,确保同一对用户只有一条记录。
  • block方法中,我们强制将isFollowing设为false,因为业务上拉黑通常意味着取消关注。这是一个重要的业务规则。
  • 所有更新操作都使用了@Transactional注解,保证数据一致性。

4.3 内容查询与过滤服务

这是实现“看不到某人内容”的核心。我们创建一个FeedService来演示如何过滤动态流。

// 文件路径:src/main/java/com/example/social/service/FeedService.java package com.example.social.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.example.social.entity.Post; import com.example.social.entity.UserRelationship; import com.example.social.mapper.PostMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.util.List; import java.util.stream.Collectors; @Slf4j @Service @RequiredArgsConstructor public class FeedService { private final PostMapper postMapper; private final UserRelationshipService relationshipService; /** * 获取用户的主页动态流(已过滤拉黑用户) * @param viewerId 浏览者ID(当前登录用户) * @param pageNo 页码 * @param pageSize 页大小 * @return 过滤后的动态分页列表 */ public Page<Post> getPersonalFeed(Long viewerId, int pageNo, int pageSize) { // 1. 查询当前用户拉黑了哪些人 LambdaQueryWrapper<UserRelationship> blockQuery = new LambdaQueryWrapper<>(); blockQuery.eq(UserRelationship::getFromUserId, viewerId) .eq(UserRelationship::getIsBlocked, true); List<UserRelationship> blockRelationships = relationshipService.list(blockQuery); List<Long> blockedUserIds = blockRelationships.stream() .map(UserRelationship::getToUserId) .collect(Collectors.toList()); // 2. 构建动态查询条件,排除拉黑用户发布的内容 LambdaQueryWrapper<Post> postQuery = new LambdaQueryWrapper<>(); postQuery.eq(Post::getVisibility, 1) // 假设只查公开内容 .notIn(!blockedUserIds.isEmpty(), Post::getUserId, blockedUserIds) // 关键过滤条件 .orderByDesc(Post::getCreateTime); // 3. 执行分页查询 Page<Post> page = new Page<>(pageNo, pageSize); return postMapper.selectPage(page, postQuery); } /** * 获取特定用户的公开动态(浏览他人主页) * @param viewerId 浏览者ID * @param ownerId 主页主人ID * @param pageNo 页码 * @param pageSize 页大小 * @return 过滤后的动态列表 */ public Page<Post> getUserPublicPosts(Long viewerId, Long ownerId, int pageNo, int pageSize) { // 首先检查浏览者是否拉黑了主页主人?这里业务上可能有不同处理。 // 假设我们允许查看,但需要过滤掉主页主人发布的、被浏览者拉黑的其他人参与的内容吗? // 这是一个更复杂的场景。我们先实现基础版本:只查看主页主人的公开帖子。 LambdaQueryWrapper<Post> postQuery = new LambdaQueryWrapper<>(); postQuery.eq(Post::getUserId, ownerId) .eq(Post::getVisibility, 1) .orderByDesc(Post::getCreateTime); Page<Post> page = new Page<>(pageNo, pageSize); return postMapper.selectPage(page, postQuery); } }

过滤逻辑核心:在getPersonalFeed方法中,我们首先查询出当前用户 (viewerId) 拉黑的所有用户ID列表 (blockedUserIds)。然后,在查询帖子时,使用notIn条件将这些被拉黑的用户ID排除在外。这正是实现“sakura看不到老头内容”的关键SQL操作。

4.4 控制器层 API 设计

提供 RESTful API 供前端调用。

// 文件路径:src/main/java/com/example/social/controller/RelationshipController.java package com.example.social.controller; import com.example.social.entity.UserRelationship; import com.example.social.service.UserRelationshipService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/relationship") @RequiredArgsConstructor public class RelationshipController { private final UserRelationshipService relationshipService; @PostMapping("/follow/{toUserId}") public ResponseEntity<String> follow(@RequestAttribute Long currentUserId, @PathVariable Long toUserId) { boolean success = relationshipService.follow(currentUserId, toUserId); if (success) { return ResponseEntity.ok("关注成功"); } else { return ResponseEntity.badRequest().body("关注失败,可能已拉黑该用户"); } } @PostMapping("/unfollow/{toUserId}") public ResponseEntity<String> unfollow(@RequestAttribute Long currentUserId, @PathVariable Long toUserId) { boolean success = relationshipService.unfollow(currentUserId, toUserId); return success ? ResponseEntity.ok("取消关注成功") : ResponseEntity.badRequest().body("操作失败"); } @PostMapping("/block/{toUserId}") public ResponseEntity<String> block(@RequestAttribute Long currentUserId, @PathVariable Long toUserId) { boolean success = relationshipService.block(currentUserId, toUserId); return success ? ResponseEntity.ok("拉黑成功") : ResponseEntity.badRequest().body("操作失败"); } @PostMapping("/unblock/{toUserId}") public ResponseEntity<String> unblock(@RequestAttribute Long currentUserId, @PathVariable Long toUserId) { boolean success = relationshipService.unblock(currentUserId, toUserId); return success ? ResponseEntity.ok("取消拉黑成功") : ResponseEntity.badRequest().body("操作失败"); } @GetMapping("/status/{toUserId}") public ResponseEntity<UserRelationship> getStatus(@RequestAttribute Long currentUserId, @PathVariable Long toUserId) { UserRelationship relationship = relationshipService.getRelationship(currentUserId, toUserId); return ResponseEntity.ok(relationship); } }
// 文件路径:src/main/java/com/example/social/controller/FeedController.java package com.example.social.controller; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.example.social.entity.Post; import com.example.social.service.FeedService; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/feed") @RequiredArgsConstructor public class FeedController { private final FeedService feedService; @GetMapping("/personal") public Page<Post> getPersonalFeed(@RequestAttribute Long currentUserId, @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "20") int size) { return feedService.getPersonalFeed(currentUserId, page, size); } @GetMapping("/user/{ownerId}") public Page<Post> getUserPosts(@RequestAttribute Long currentUserId, @PathVariable Long ownerId, @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "20") int size) { return feedService.getUserPublicPosts(currentUserId, ownerId, page, size); } }

注意@RequestAttribute Long currentUserId假设用户ID已通过拦截器或过滤器从JWT Token中解析并设置到请求属性中。你需要实现自己的认证逻辑。

5. 高级场景与优化策略

基础功能实现后,我们来探讨更复杂的“干预”场景和性能优化。

5.1 实现“干预规则”扩展

如果业务上需要实现“当老头评论sakura时发出警告”,我们可以引入一个UserInteractionRule表。

CREATE TABLE `user_interaction_rule` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint NOT NULL COMMENT '规则设置者', `target_user_id` bigint NOT NULL COMMENT '被监控的用户', `interaction_type` varchar(50) COMMENT '互动类型:COMMENT, MESSAGE, LIKE', `action` varchar(50) COMMENT '触发动作:WARN, BLOCK, NOTIFY', `status` tinyint DEFAULT 1 COMMENT '规则状态', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_target_type` (`user_id`, `target_user_id`, `interaction_type`) ) COMMENT='用户互动规则表';

然后,在用户进行互动(如发表评论)的服务层,增加规则检查:

// 伪代码示例 public boolean addComment(Long fromUserId, Long toUserId, String content) { // 1. 检查基础权限(如是否被拉黑) // 2. 查询是否有针对此互动的规则 List<InteractionRule> rules = ruleService.getRulesByTargetAndAction(toUserId, fromUserId, "COMMENT"); for (Rule rule : rules) { if ("WARN".equals(rule.getAction())) { // 发送警告消息给 rule.getUserId() (即sakura) notificationService.sendWarn(rule.getUserId(), fromUserId, toUserId); } else if ("BLOCK".equals(rule.getAction())) { // 直接拦截此次互动 throw new BusinessException("互动被禁止"); } } // 3. 执行正常的评论逻辑 return commentService.save(...); }

5.2 查询性能优化

当用户拉黑列表很大时,NOT IN (...)查询可能会变慢。优化方案:

  1. 使用 JOIN 替代 NOT IN:对于大数据集,LEFT JOIN ... IS NULL有时性能更好。
  2. 引入缓存:将用户的拉黑列表缓存在 Redis 中。
    // 伪代码:缓存拉黑列表 public List<Long> getBlockedUserIds(Long userId) { String cacheKey = "user:blocked:" + userId; List<Long> blockedIds = redisTemplate.opsForList().range(cacheKey, 0, -1); if (blockedIds == null || blockedIds.isEmpty()) { // 从DB查询并缓存 blockedIds = relationshipService.listBlockedUserIds(userId); if (!blockedIds.isEmpty()) { redisTemplate.opsForList().rightPushAll(cacheKey, blockedIds); redisTemplate.expire(cacheKey, 1, TimeUnit.HOURS); // 设置过期时间 } } return blockedIds; }
  3. 布隆过滤器 (Bloom Filter):对于超大规模拉黑列表,可以先使用布隆过滤器快速判断一个用户ID肯定不在拉黑列表或可能在拉黑列表中,对于“可能在”的情况再查缓存或DB确认。这能极大减少不必要的数据库查询。

5.3 关系状态同步与一致性

关系变更(如拉黑)需要及时反映在内容流中。除了上述缓存策略,还可以考虑:

  • 发布/订阅模式:当用户拉黑关系变更时,发布一个事件。内容流服务订阅该事件,主动更新或失效相关缓存。
  • 双写机制:在更新关系表的同时,直接更新一份用户-被拉黑者ID的集合到 Redis Set 中,供内容查询时直接使用。

6. 常见问题与排查思路

在实际开发和运维中,你可能会遇到以下问题:

问题现象可能原因排查思路与解决方案
拉黑后,仍能看到对方内容1. 缓存未更新或过期。
2. 内容查询逻辑未正确应用过滤条件。
3. 分页查询时数据穿透。
1. 检查缓存键和过期策略,手动清除缓存测试。
2. 在FeedService的查询方法中打日志,确认blockedUserIds是否正确获取并应用到SQL中。
3. 确保过滤条件在分页查询前生效。
“关注”和“拉黑”状态同时为true业务逻辑漏洞。检查block方法,确保在设置isBlocked=true时,强制将isFollowing设为false。这是我们在Service层已经实现的逻辑。
查询关系速度慢,接口超时1.user_relationship表缺少有效索引。
2. 拉黑列表过大,NOT IN查询效率低。
1. 使用EXPLAIN分析SQL,确认是否命中idx_from_user_status等索引。
2. 考虑引入缓存或优化查询策略,如5.2节所述。
并发操作导致关系状态错误两个请求同时修改同一条关系记录。1. 在Service方法上使用@Transactional保证原子性。
2. 对于核心状态更新,可以考虑使用数据库悲观锁(SELECT ... FOR UPDATE)或乐观锁(版本号)。
无法拉黑不存在的用户代码中未校验toUserId是否存在。block方法开始处,先查询用户表确认toUserId有效,或依赖外键约束(如果数据库有外键)。

7. 最佳实践与工程建议

  1. 明确业务规则:在编码前,务必与产品经理明确“关注”、“拉黑”、“好友”等状态之间的互斥和转换规则。例如,拉黑是否自动取消关注?取消拉黑后是否恢复关注?这些规则要体现在状态机中。
  2. 索引是生命线user_relationship表的查询模式(WHERE from_user_id = ? AND is_blocked = 1)决定了必须创建合适的复合索引。定期使用EXPLAIN分析慢查询。
  3. 缓存策略分层
    • 热点数据:活跃用户的拉黑列表可以缓存在Redis中,设置合理的过期时间(如30分钟)。
    • 全量数据:对于关系变更不频繁的场景,可以考虑在应用启动时全量加载到本地内存(如Guava Cache),并通过消息队列监听变更事件进行更新。
  4. 考虑数据分片:对于亿级用户规模的社交平台,user_relationship表必须分片。常见的分片键是from_user_id,这样同一个用户发起的全部关系记录都落在同一个分片上,方便查询。
  5. API设计注意安全性
    • 所有操作API都必须验证当前登录用户身份,防止越权操作。
    • currentUserId应从安全的会话或Token中获取,绝不能由前端直接传递。
    • toUserId进行基础校验,防止无效ID或恶意请求。
  6. 监控与告警
    • 监控关系变更QPS、接口响应时间、缓存命中率。
    • 对缓存穿透(查询一个不存在的巨大拉黑列表)和缓存雪崩(大量缓存同时过期)要有预案。
  7. 测试覆盖
    • 单元测试覆盖所有关系状态转换。
    • 集成测试模拟并发关注/拉黑操作。
    • 性能压测验证过滤查询在大数据量下的表现。

通过以上设计,我们构建了一个能够处理“sakura你不要跟那个老头在一起啊”这类复杂社交过滤需求的核心系统。从精准的关系数据建模,到高效的内容过滤查询,再到可扩展的高级规则引擎,这套方案为社交应用提供了坚实的基础。在实际项目中,你可以根据业务复杂度的增长,逐步引入缓存、消息队列、分库分表等中间件进行演进。

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

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

立即咨询