IM系统里最烧脑的‘好友关系’与‘群组管理’,我是用SpringBoot+MySQL这样设计的
SpringBootMySQL设计高并发IM系统好友与群组管理的核心逻辑1. 关系链数据模型设计精要即时通讯系统中关系链设计直接决定了系统的扩展性和性能上限。我们采用双向好友关系模型每条记录包含from_id、to_id、status、remark等核心字段通过组合索引优化查询性能CREATE TABLE im_friendship ( id bigint NOT NULL AUTO_INCREMENT, app_id int NOT NULL COMMENT 应用ID, from_id varchar(32) NOT NULL COMMENT 用户A, to_id varchar(32) NOT NULL COMMENT 用户B, status tinyint DEFAULT 0 COMMENT 0未添加 1正常 2删除, black tinyint DEFAULT 0 COMMENT 1拉黑, sequence bigint DEFAULT NULL COMMENT 序列号, remark varchar(64) DEFAULT NULL COMMENT 备注, add_source varchar(16) DEFAULT NULL COMMENT 来源, extra json DEFAULT NULL COMMENT 扩展字段, create_time bigint DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY idx_unique_relation (app_id,from_id,to_id), KEY idx_to_id (to_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4关键设计决策采用双写机制用户A添加B时同时写入A→B和B→A两条记录使用sequence字段实现多端同步通过Redis生成全局递增序列extra字段采用JSON类型存储动态属性避免频繁修改表结构2. 高并发好友申请处理方案好友申请流程需要处理去重、幂等和状态同步三大核心问题。我们设计了三层校验体系内存级过滤使用Guava Cache缓存最近操作记录数据库校验通过SELECT...FOR UPDATE实现行级锁最终一致性检查通过定时任务补偿异常状态Transactional public ResponseVO handleFriendApply(FriendApplyDTO dto) { // 1. 校验基础参数 ParamValidator.validate(dto); // 2. 获取分布式锁 String lockKey friend_apply: dto.getFromId() : dto.getToId(); if (!redisLock.tryLock(lockKey, 10, TimeUnit.SECONDS)) { throw new BusinessException(操作过于频繁); } try { // 3. 查询现有申请记录 FriendApply existApply applyMapper.selectOne(new LambdaQueryWrapperFriendApply() .eq(FriendApply::getFromId, dto.getFromId()) .eq(FriendApply::getToId, dto.getToId()) .last(FOR UPDATE)); // 4. 状态机处理 if (existApply null) { // 新建申请流程 FriendApply newApply buildNewApply(dto); applyMapper.insert(newApply); sendApplyNotification(newApply); } else { // 更新已有申请 processExistApply(existApply, dto); } return ResponseVO.success(); } finally { redisLock.unlock(lockKey); } }性能优化点采用异步写扩散模式通过消息队列解耦核心流程与通知逻辑申请列表查询使用游标分页避免深度分页性能问题高频查询走多级缓存Redis → Caffeine → DB3. 群组权限体系设计IM群组管理的复杂度主要来自角色权限系统。我们设计了基于RBAC模型的五层权限体系角色类型权限项数值二进制位群主解散群100000001管理员踢人200000010管理员禁言400000100成员发言800001000成员退群1600010000public class GroupPermission { private static final MapInteger, String PERMISSION_MAP ImmutableMap.of( 1, OWNER, 2, ADMIN_KICK, 4, ADMIN_MUTE, 8, MEMBER_SPEAK, 16, MEMBER_QUIT ); public static boolean hasPermission(int role, int permission) { return (role permission) permission; } public static int addPermission(int role, int permission) { return role | permission; } }典型业务场景处理public void transferGroupOwner(String groupId, String operator, String newOwner) { // 1. 校验操作者权限 GroupMember operatorMember getMember(groupId, operator); if (!GroupPermission.hasPermission(operatorMember.getRole(), 1)) { throw new BusinessException(无转让权限); } // 2. 更新群主身份 GroupMember newOwnerMember getMember(groupId, newOwner); groupMemberMapper.updateRole(operator, 0); // 原群主降级 groupMemberMapper.updateRole(newOwner, 1); // 新群主升级 // 3. 记录操作日志 GroupOperateLog log new GroupOperateLog(); log.setOperateType(TRANSFER); log.setContent(operator 将群主转让给 newOwner); operateLogMapper.insert(log); // 4. 通知所有群成员 GroupNotifyMessage message buildTransferMessage(operator, newOwner); messageProducer.sendGroupMessage(groupId, message); }4. 高性能关系校验方案好友关系校验在IM系统中属于高频操作我们设计了多级缓存策略布隆过滤器快速排除非好友关系// 初始化布隆过滤器 BloomFilterString friendBloomFilter BloomFilter.create( Funnels.stringFunnel(Charset.defaultCharset()), 1000000, 0.01); // 添加关系时更新 public void afterAddFriend(String userId1, String userId2) { String key1 userId1 : userId2; String key2 userId2 : userId1; friendBloomFilter.put(key1); friendBloomFilter.put(key2); }Redis缓存存储完整关系数据# 好友关系存储结构 HSET im:friend:{appId}:{fromId} {toId} {status}|{black}|{timestamp}数据库查询最终一致性检查/* 双向关系校验SQL */ SELECT a.from_id, a.to_id, CASE WHEN a.status1 AND b.status1 THEN 1 WHEN a.status1 AND b.status!1 THEN 2 WHEN a.status!1 AND b.status1 THEN 3 ELSE 4 END AS relation_status FROM im_friendship a JOIN im_friendship b ON a.from_id b.to_id AND a.to_id b.from_id WHERE a.app_id#{appId} AND a.from_id#{fromId} AND a.to_id IN (foreach collectiontoIds itemid#{id}/foreach)性能对比方案QPS平均耗时适用场景纯DB查询120045ms低频复杂查询Redis缓存150008ms高频简单校验内存缓存500002ms极端高频场景5. 群成员列表分页优化大群成员列表查询是典型的高消耗操作我们采用冷热数据分离策略热数据最近活跃成员存储在Redis ZSET中// 成员活跃度更新 public void updateMemberActive(String groupId, String userId) { String key group:active: groupId; redisTemplate.opsForZSet().add(key, userId, System.currentTimeMillis()); // 保持最近200个活跃成员 redisTemplate.opsForZSet().removeRange(key, 0, -201); }分页查询优化/* 优化后的分页查询 */ SELECT m.* FROM ( SELECT id FROM im_group_member WHERE group_id #{groupId} ORDER BY last_active_time DESC LIMIT #{offset}, #{pageSize} ) t JOIN im_group_member m ON t.id m.id二级缓存策略第一次查询Redis ZSET → DB → 回填缓存 后续查询Redis ZSET → 本地缓存 → 返回结果性能提升效果万人大群成员列表查询从1200ms降至80msRedis内存占用减少60%仅存储活跃成员数据库负载降低75%6. 事务与最终一致性保障IM系统对数据一致性要求极高我们采用柔性事务方案本地消息表Transactional public void addFriendWithMessage(String fromId, String toId) { // 1. 写入好友关系 friendshipMapper.insert(buildFriendShip(fromId, toId)); friendshipMapper.insert(buildFriendShip(toId, fromId)); // 2. 写入本地消息表 EventMessage message new EventMessage(); message.setEventType(FRIEND_ADD); message.setContent(buildMessageContent(fromId, toId)); eventMessageMapper.insert(message); }定时任务补偿Scheduled(fixedDelay 30000) public void compensateFailedEvents() { ListEventMessage failedMessages eventMessageMapper.selectFailedMessages(); failedMessages.forEach(message - { try { eventPublisher.republish(message); eventMessageMapper.updateStatus(message.getId(), PROCESSED); } catch (Exception e) { log.error(事件补偿失败, e); } }); }幂等设计public void handleFriendApplyEvent(ApplyEvent event) { String lockKey apply_event: event.getEventId(); if (redisTemplate.opsForValue().setIfAbsent(lockKey, 1, 24, TimeUnit.HOURS)) { // 业务处理 } else { log.warn(重复事件直接返回); } }这套方案在实际业务中实现了事务成功率从99.2%提升到99.99%异常恢复时间从小时级降到分钟级系统容错能力显著增强