多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Spring Boot实战:社交应用用户关系系统设计与内容过滤实现

Spring Boot实战:社交应用用户关系系统设计与内容过滤实现 最近在开发一个社区类应用时遇到了一个非常典型的场景如何优雅地处理用户之间的关注、拉黑等社交关系并在内容流中实现精准的过滤。比如用户A不希望看到用户B一个被戏称为“老头”的用户发布的任何内容甚至希望系统能“劝诫”用户B不要与特定用户互动。这背后涉及的核心技术就是关系型数据建模与实时过滤策略。本文将深入探讨如何设计一个高可用、可扩展的用户关系系统以应对诸如“sakura你不要跟那个老头在一起啊”这类复杂的社交过滤需求。我们将从数据库表设计出发涵盖关系定义、状态流转、查询优化并最终给出一个Spring Boot微服务实战案例包含完整的代码和API设计。无论你是正在构建社交功能的后端开发者还是对数据模型设计感兴趣的同学都能从中获得一套可直接复用的解决方案。1. 核心概念与业务场景分析在深入代码之前我们首先要厘清几个核心概念和需要解决的业务问题。1.1 什么是用户关系系统用户关系系统是社交平台的核心组件之一它负责定义和管理用户之间的双向连接状态。常见的状态包括无关系双方未建立任何连接。关注单向或双向的关注行为是社交图谱的基础。好友一种特殊的双向关注关系通常需要对方确认。拉黑/屏蔽一种强过滤关系用户A拉黑用户B后A将看不到B的任何内容且B通常无法与A互动如评论、私信。特别关注在关注基础上的强化关系用于优先展示其内容。本文重点要解决的“不希望用户A和用户B互动”的场景主要涉及拉黑关系并可能结合内容流的过滤规则。1.2 “sakura”与“老头”场景拆解我们以一个具体场景为例“用户sakura”不希望“用户老头”与“用户C”在一起互动。从系统层面这可以分解为几个子需求关系定义sakura可以拉黑“老头”。这是最直接的操作拉黑后sakura的时间线将过滤掉“老头”的内容。内容过滤sakura在浏览全局内容或“用户C”的主页时系统需要过滤掉所有“老头”参与的内容发帖、评论、点赞。干预提示可选这是一个更复杂的业务逻辑。当“老头”试图与“用户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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户关系表;设计解读from_user_id和to_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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT动态/帖子表;3. 环境准备与项目搭建我们将使用 Spring Boot 2.7.x 和 MyBatis-Plus 3.5.x 来快速构建后端服务。3.1 技术栈与版本JDK: 1.8 或 11Spring Boot: 2.7.18MyBatis-Plus: 3.5.4MySQL: 5.7 或 8.0Maven: 3.63.2 项目初始化与依赖使用 Spring Initializr 或 IDE 创建项目主要依赖如下!-- 文件路径pom.xml -- dependencies !-- Spring Boot Web -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis-Plus -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.4/version /dependency !-- MySQL 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency !-- Lombok -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency !-- 测试 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency /dependencies3.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?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/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: 04. 核心代码实现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 BaseMapperUserRelationship { // 可以在此定义复杂查询方法 }服务层封装核心业务逻辑特别是关系状态的更新。// 文件路径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 IServiceUserRelationship { /** * 关注用户 */ 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 ServiceImplUserRelationshipMapper, UserRelationship implements UserRelationshipService { Override Transactional(rollbackFor Exception.class) public boolean follow(Long fromUserId, Long toUserId) { if (fromUserId.equals(toUserId)) { throw new IllegalArgumentException(不能关注自己); } LambdaQueryWrapperUserRelationship 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) { LambdaUpdateWrapperUserRelationship 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(不能拉黑自己); } LambdaQueryWrapperUserRelationship 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) { LambdaUpdateWrapperUserRelationship 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) { LambdaQueryWrapperUserRelationship queryWrapper new LambdaQueryWrapper(); queryWrapper.eq(UserRelationship::getFromUserId, fromUserId) .eq(UserRelationship::getToUserId, toUserId); return this.getOne(queryWrapper); } }关键逻辑说明follow和block方法实现了“创建或更新”的逻辑确保同一对用户只有一条记录。在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 PagePost getPersonalFeed(Long viewerId, int pageNo, int pageSize) { // 1. 查询当前用户拉黑了哪些人 LambdaQueryWrapperUserRelationship blockQuery new LambdaQueryWrapper(); blockQuery.eq(UserRelationship::getFromUserId, viewerId) .eq(UserRelationship::getIsBlocked, true); ListUserRelationship blockRelationships relationshipService.list(blockQuery); ListLong blockedUserIds blockRelationships.stream() .map(UserRelationship::getToUserId) .collect(Collectors.toList()); // 2. 构建动态查询条件排除拉黑用户发布的内容 LambdaQueryWrapperPost postQuery new LambdaQueryWrapper(); postQuery.eq(Post::getVisibility, 1) // 假设只查公开内容 .notIn(!blockedUserIds.isEmpty(), Post::getUserId, blockedUserIds) // 关键过滤条件 .orderByDesc(Post::getCreateTime); // 3. 执行分页查询 PagePost page new Page(pageNo, pageSize); return postMapper.selectPage(page, postQuery); } /** * 获取特定用户的公开动态浏览他人主页 * param viewerId 浏览者ID * param ownerId 主页主人ID * param pageNo 页码 * param pageSize 页大小 * return 过滤后的动态列表 */ public PagePost getUserPublicPosts(Long viewerId, Long ownerId, int pageNo, int pageSize) { // 首先检查浏览者是否拉黑了主页主人这里业务上可能有不同处理。 // 假设我们允许查看但需要过滤掉主页主人发布的、被浏览者拉黑的其他人参与的内容吗 // 这是一个更复杂的场景。我们先实现基础版本只查看主页主人的公开帖子。 LambdaQueryWrapperPost postQuery new LambdaQueryWrapper(); postQuery.eq(Post::getUserId, ownerId) .eq(Post::getVisibility, 1) .orderByDesc(Post::getCreateTime); PagePost 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 ResponseEntityString 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 ResponseEntityString unfollow(RequestAttribute Long currentUserId, PathVariable Long toUserId) { boolean success relationshipService.unfollow(currentUserId, toUserId); return success ? ResponseEntity.ok(取消关注成功) : ResponseEntity.badRequest().body(操作失败); } PostMapping(/block/{toUserId}) public ResponseEntityString block(RequestAttribute Long currentUserId, PathVariable Long toUserId) { boolean success relationshipService.block(currentUserId, toUserId); return success ? ResponseEntity.ok(拉黑成功) : ResponseEntity.badRequest().body(操作失败); } PostMapping(/unblock/{toUserId}) public ResponseEntityString unblock(RequestAttribute Long currentUserId, PathVariable Long toUserId) { boolean success relationshipService.unblock(currentUserId, toUserId); return success ? ResponseEntity.ok(取消拉黑成功) : ResponseEntity.badRequest().body(操作失败); } GetMapping(/status/{toUserId}) public ResponseEntityUserRelationship 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 PagePost getPersonalFeed(RequestAttribute Long currentUserId, RequestParam(defaultValue 1) int page, RequestParam(defaultValue 20) int size) { return feedService.getPersonalFeed(currentUserId, page, size); } GetMapping(/user/{ownerId}) public PagePost 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. 查询是否有针对此互动的规则 ListInteractionRule 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 (...)查询可能会变慢。优化方案使用 JOIN 替代 NOT IN对于大数据集LEFT JOIN ... IS NULL有时性能更好。引入缓存将用户的拉黑列表缓存在 Redis 中。// 伪代码缓存拉黑列表 public ListLong getBlockedUserIds(Long userId) { String cacheKey user:blocked: userId; ListLong 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; }布隆过滤器 (Bloom Filter)对于超大规模拉黑列表可以先使用布隆过滤器快速判断一个用户ID肯定不在拉黑列表或可能在拉黑列表中对于“可能在”的情况再查缓存或DB确认。这能极大减少不必要的数据库查询。5.3 关系状态同步与一致性关系变更如拉黑需要及时反映在内容流中。除了上述缓存策略还可以考虑发布/订阅模式当用户拉黑关系变更时发布一个事件。内容流服务订阅该事件主动更新或失效相关缓存。双写机制在更新关系表的同时直接更新一份用户-被拉黑者ID的集合到 Redis Set 中供内容查询时直接使用。6. 常见问题与排查思路在实际开发和运维中你可能会遇到以下问题问题现象可能原因排查思路与解决方案拉黑后仍能看到对方内容1. 缓存未更新或过期。2. 内容查询逻辑未正确应用过滤条件。3. 分页查询时数据穿透。1. 检查缓存键和过期策略手动清除缓存测试。2. 在FeedService的查询方法中打日志确认blockedUserIds是否正确获取并应用到SQL中。3. 确保过滤条件在分页查询前生效。“关注”和“拉黑”状态同时为true业务逻辑漏洞。检查block方法确保在设置isBlockedtrue时强制将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. 最佳实践与工程建议明确业务规则在编码前务必与产品经理明确“关注”、“拉黑”、“好友”等状态之间的互斥和转换规则。例如拉黑是否自动取消关注取消拉黑后是否恢复关注这些规则要体现在状态机中。索引是生命线user_relationship表的查询模式WHERE from_user_id ? AND is_blocked 1决定了必须创建合适的复合索引。定期使用EXPLAIN分析慢查询。缓存策略分层热点数据活跃用户的拉黑列表可以缓存在Redis中设置合理的过期时间如30分钟。全量数据对于关系变更不频繁的场景可以考虑在应用启动时全量加载到本地内存如Guava Cache并通过消息队列监听变更事件进行更新。考虑数据分片对于亿级用户规模的社交平台user_relationship表必须分片。常见的分片键是from_user_id这样同一个用户发起的全部关系记录都落在同一个分片上方便查询。API设计注意安全性所有操作API都必须验证当前登录用户身份防止越权操作。currentUserId应从安全的会话或Token中获取绝不能由前端直接传递。对toUserId进行基础校验防止无效ID或恶意请求。监控与告警监控关系变更QPS、接口响应时间、缓存命中率。对缓存穿透查询一个不存在的巨大拉黑列表和缓存雪崩大量缓存同时过期要有预案。测试覆盖单元测试覆盖所有关系状态转换。集成测试模拟并发关注/拉黑操作。性能压测验证过滤查询在大数据量下的表现。通过以上设计我们构建了一个能够处理“sakura你不要跟那个老头在一起啊”这类复杂社交过滤需求的核心系统。从精准的关系数据建模到高效的内容过滤查询再到可扩展的高级规则引擎这套方案为社交应用提供了坚实的基础。在实际项目中你可以根据业务复杂度的增长逐步引入缓存、消息队列、分库分表等中间件进行演进。
返回列表