Commit 106cf1c9f719457769e93e7ef93150dab4c7779d

Authored by YunaiV
1 parent f5b07508

✨ feat(im): 好友/群成员添加增加唯一索引并发兼容

- ImFriendServiceImpl#addFriend0 捕获 DuplicateKeyException
  并发插入冲突时忽略(另一方插入必然为 ENABLE 状态)
- ImGroupMemberServiceImpl#addGroupMember 改为先查后写:
  已存在 DISABLE 复活为 ENABLE,新记录 insert 失败降级 select 返回
- ImGroupMemberServiceImpl#addGroupMembers 批量 insert 触发
  DuplicateKey 时降级为逐个 addGroupMember 走幂等兜底
- 依赖 im_friend.uk_user_friend 与 im_group_member.uk_group_user 唯一索引
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java
... ... @@ -158,9 +158,8 @@ public class ImFriendServiceImpl implements ImFriendService {
158 158 try {
159 159 imFriendMapper.insert(friend);
160 160 } catch (DuplicateKeyException e) {
161   - // 并发场景:另一个请求已先一步插入,降级走已存在逻辑
162   - log.warn("[addFriend0][userId({}) friendUserId({}) 并发插入冲突,降级处理]", userId, friendUserId);
163   - addFriend0(userId, friendUserId);
  161 + // 并发场景:另一个请求已先一步插入,且其插入的必然是 ENABLE 状态(DISABLE 场景在上方分支已处理),直接忽略
  162 + log.warn("[addFriend0][userId({}) friendUserId({}) 并发插入冲突,忽略]", userId, friendUserId);
164 163 }
165 164 }
166 165  
... ...
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImpl.java
... ... @@ -9,6 +9,8 @@ import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMemberMapper;
9 9 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
10 10 import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO;
11 11 import jakarta.annotation.Resource;
  12 +import lombok.extern.slf4j.Slf4j;
  13 +import org.springframework.dao.DuplicateKeyException;
12 14 import org.springframework.stereotype.Service;
13 15 import org.springframework.validation.annotation.Validated;
14 16  
... ... @@ -27,6 +29,7 @@ import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.GROUP_MEMBER_N
27 29 *
28 30 * @author 芋道源码
29 31 */
  32 +@Slf4j
30 33 @Service
31 34 @Validated
32 35 public class ImGroupMemberServiceImpl implements ImGroupMemberService {
... ... @@ -62,13 +65,34 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService {
62 65 return groupMemberMapper.selectQuitListByUserId(userId, minQuitTime);
63 66 }
64 67  
  68 + /**
  69 + * 并发安全:依靠 im_group_member 表的唯一索引 uk_group_user(group_id, user_id) 保证幂等,
  70 + * 当并发 insert 触发 {@link DuplicateKeyException} 时降级为 select + update。
  71 + */
65 72 @Override
66 73 public ImGroupMemberDO addGroupMember(Long groupId, Long userId) {
  74 + LocalDateTime now = LocalDateTime.now();
  75 + // 情况一:已存在记录 → 复活或跳过
  76 + ImGroupMemberDO exists = groupMemberMapper.selectByGroupIdAndUserId(groupId, userId);
  77 + if (exists != null) {
  78 + if (CommonStatusEnum.isDisable(exists.getStatus())) {
  79 + groupMemberMapper.updateById(new ImGroupMemberDO().setId(exists.getId())
  80 + .setStatus(CommonStatusEnum.ENABLE.getStatus()).setJoinTime(now));
  81 + }
  82 + return groupMemberMapper.selectById(exists.getId());
  83 + }
  84 + // 情况二:新增成员
67 85 ImGroupMemberDO member = new ImGroupMemberDO()
68 86 .setGroupId(groupId).setUserId(userId)
69   - .setStatus(CommonStatusEnum.ENABLE.getStatus()).setJoinTime(LocalDateTime.now());
70   - groupMemberMapper.insert(member);
71   - return member;
  87 + .setStatus(CommonStatusEnum.ENABLE.getStatus()).setJoinTime(now);
  88 + try {
  89 + groupMemberMapper.insert(member);
  90 + return member;
  91 + } catch (DuplicateKeyException e) {
  92 + // 并发场景:另一个请求已先一步插入,且其插入的必然是 ENABLE 状态(DISABLE 场景在上方分支已处理),查询返回
  93 + log.warn("[addGroupMember][groupId({}) userId({}) 并发插入冲突,查询返回]", groupId, userId);
  94 + return groupMemberMapper.selectByGroupIdAndUserId(groupId, userId);
  95 + }
72 96 }
73 97  
74 98 @Override
... ... @@ -91,13 +115,22 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService {
91 115 }
92 116 }
93 117  
94   - // 2. 执行
95   - if (CollUtil.isNotEmpty(inserts)) {
96   - groupMemberMapper.insertBatch(inserts);
97   - }
  118 + // 2.1 先做 update,update 没有并发冲突风险
98 119 if (CollUtil.isNotEmpty(updates)) {
99 120 groupMemberMapper.updateBatch(updates);
100 121 }
  122 + // 2.2 批量 insert。并发场景下若其它请求已先一步插入同一 (groupId, userId),
  123 + // 会触发唯一索引冲突,此时降级为逐个 addGroupMember(利用其兜底逻辑幂等处理)。
  124 + if (CollUtil.isNotEmpty(inserts)) {
  125 + try {
  126 + groupMemberMapper.insertBatch(inserts);
  127 + } catch (DuplicateKeyException e) {
  128 + log.warn("[addGroupMembers][groupId({}) userIds({}) 批量插入冲突,降级为逐个处理]", groupId, userIds);
  129 + for (ImGroupMemberDO insert : inserts) {
  130 + addGroupMember(groupId, insert.getUserId());
  131 + }
  132 + }
  133 + }
101 134 }
102 135  
103 136 @Override
... ...
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/ImWebSocketServiceImpl.java
... ... @@ -11,6 +11,7 @@ import org.springframework.scheduling.annotation.Async;
11 11 import org.springframework.stereotype.Service;
12 12 import org.springframework.transaction.support.TransactionSynchronization;
13 13 import org.springframework.transaction.support.TransactionSynchronizationManager;
  14 +import org.springframework.validation.annotation.Validated;
14 15  
15 16 import java.util.Collection;
16 17  
... ... @@ -24,6 +25,7 @@ import java.util.Collection;
24 25 * @author 芋道源码
25 26 */
26 27 @Service
  28 +@Validated
27 29 @Slf4j
28 30 public class ImWebSocketServiceImpl implements ImWebSocketService {
29 31  
... ...