Commit 106cf1c9f719457769e93e7ef93150dab4c7779d
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 唯一索引
Showing
3 changed files
with
44 additions
and
10 deletions
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 | ... | ... |