Commit f0be7ba137481fc69b5c5927be8921285a0d918a
1 parent
e266a0fc
✨ feat(im): 优化好友请求处理逻辑,增加乐观锁支持
Showing
4 changed files
with
52 additions
and
28 deletions
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/friend/ImFriendRequestMapper.java
| @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.im.dal.mysql.friend; | @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.im.dal.mysql.friend; | ||
| 3 | import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; | 3 | import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; |
| 4 | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; | 4 | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; |
| 5 | import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendRequestDO; | 5 | import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendRequestDO; |
| 6 | +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; | ||
| 6 | import org.apache.ibatis.annotations.Mapper; | 7 | import org.apache.ibatis.annotations.Mapper; |
| 7 | 8 | ||
| 8 | import java.util.List; | 9 | import java.util.List; |
| @@ -33,4 +34,9 @@ public interface ImFriendRequestMapper extends BaseMapperX<ImFriendRequestDO> { | @@ -33,4 +34,9 @@ public interface ImFriendRequestMapper extends BaseMapperX<ImFriendRequestDO> { | ||
| 33 | return selectList(wrapper); | 34 | return selectList(wrapper); |
| 34 | } | 35 | } |
| 35 | 36 | ||
| 37 | + default int updateByIdAndHandleResult(Long id, Integer handleResult, ImFriendRequestDO updateObj) { | ||
| 38 | + return update(updateObj, new LambdaUpdateWrapper<ImFriendRequestDO>() | ||
| 39 | + .eq(ImFriendRequestDO::getId, id).eq(ImFriendRequestDO::getHandleResult, handleResult)); | ||
| 40 | + } | ||
| 41 | + | ||
| 36 | } | 42 | } |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImpl.java
| @@ -121,14 +121,18 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService { | @@ -121,14 +121,18 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService { | ||
| 121 | @Override | 121 | @Override |
| 122 | @Transactional(rollbackFor = Exception.class) | 122 | @Transactional(rollbackFor = Exception.class) |
| 123 | public void agreeFriendRequest(Long userId, Long requestId) { | 123 | public void agreeFriendRequest(Long userId, Long requestId) { |
| 124 | - // 1. 校验申请存在 + 未处理 + 操作人是接收方 | 124 | + // 1. 校验申请存在 + 未处理 + 操作人是接收方(fail-fast;并发场景仍由下面的乐观锁兜底) |
| 125 | ImFriendRequestDO request = validateRequestForHandle(userId, requestId); | 125 | ImFriendRequestDO request = validateRequestForHandle(userId, requestId); |
| 126 | 126 | ||
| 127 | - // 2. 更新申请:handleResult=AGREED + handleTime | ||
| 128 | - LocalDateTime now = LocalDateTime.now(); | ||
| 129 | - friendRequestMapper.updateById(new ImFriendRequestDO().setId(request.getId()) | ||
| 130 | - .setHandleResult(ImFriendRequestHandleResultEnum.AGREED.getResult()).setHandleTime(now)); | ||
| 131 | - request.setHandleResult(ImFriendRequestHandleResultEnum.AGREED.getResult()).setHandleTime(now); | 127 | + // 2. 乐观锁更新申请:handleResult=AGREED + handleTime;并发同意会有一方 affectedRows=0 |
| 128 | + ImFriendRequestDO updateObj = new ImFriendRequestDO() | ||
| 129 | + .setHandleResult(ImFriendRequestHandleResultEnum.AGREED.getResult()).setHandleTime(LocalDateTime.now()); | ||
| 130 | + int affected = friendRequestMapper.updateByIdAndHandleResult(request.getId(), | ||
| 131 | + ImFriendRequestHandleResultEnum.UNHANDLED.getResult(), updateObj); | ||
| 132 | + if (affected == 0) { | ||
| 133 | + throw exception(FRIEND_REQUEST_HANDLED); | ||
| 134 | + } | ||
| 135 | + request.setHandleResult(ImFriendRequestHandleResultEnum.AGREED.getResult()).setHandleTime(updateObj.getHandleTime()); | ||
| 132 | 136 | ||
| 133 | // 3. 双向建立好友关系 | 137 | // 3. 双向建立好友关系 |
| 134 | friendService.becomeFriends(request); | 138 | friendService.becomeFriends(request); |
| @@ -144,14 +148,18 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService { | @@ -144,14 +148,18 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService { | ||
| 144 | @Override | 148 | @Override |
| 145 | @Transactional(rollbackFor = Exception.class) | 149 | @Transactional(rollbackFor = Exception.class) |
| 146 | public void refuseFriendRequest(Long userId, Long requestId, String handleContent) { | 150 | public void refuseFriendRequest(Long userId, Long requestId, String handleContent) { |
| 147 | - // 1. 校验申请存在 + 未处理 + 操作人是接收方 | 151 | + // 1. 校验申请存在 + 未处理 + 操作人是接收方(fail-fast;并发场景仍由下面的乐观锁兜底) |
| 148 | ImFriendRequestDO request = validateRequestForHandle(userId, requestId); | 152 | ImFriendRequestDO request = validateRequestForHandle(userId, requestId); |
| 149 | 153 | ||
| 150 | - // 2. 更新申请:handleResult=REFUSED + handleContent + handleTime | ||
| 151 | - LocalDateTime now = LocalDateTime.now(); | ||
| 152 | - friendRequestMapper.updateById(new ImFriendRequestDO().setId(request.getId()) | 154 | + // 2. 乐观锁更新申请:handleResult=REFUSED + handleContent + handleTime;并发拒绝会有一方 affectedRows=0 |
| 155 | + ImFriendRequestDO updateObj = new ImFriendRequestDO() | ||
| 153 | .setHandleResult(ImFriendRequestHandleResultEnum.REFUSED.getResult()) | 156 | .setHandleResult(ImFriendRequestHandleResultEnum.REFUSED.getResult()) |
| 154 | - .setHandleContent(handleContent).setHandleTime(now)); | 157 | + .setHandleContent(handleContent).setHandleTime(LocalDateTime.now()); |
| 158 | + int affected = friendRequestMapper.updateByIdAndHandleResult(request.getId(), | ||
| 159 | + ImFriendRequestHandleResultEnum.UNHANDLED.getResult(), updateObj); | ||
| 160 | + if (affected == 0) { | ||
| 161 | + throw exception(FRIEND_REQUEST_HANDLED); | ||
| 162 | + } | ||
| 155 | 163 | ||
| 156 | // 3. 推 FRIEND_REQUEST_REJECTED 给 fromUser 多端 | 164 | // 3. 推 FRIEND_REQUEST_REJECTED 给 fromUser 多端 |
| 157 | FriendRequestRejectedNotification payload = (FriendRequestRejectedNotification) | 165 | FriendRequestRejectedNotification payload = (FriendRequestRejectedNotification) |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java
| @@ -115,11 +115,16 @@ public class ImFriendServiceImpl implements ImFriendService { | @@ -115,11 +115,16 @@ public class ImFriendServiceImpl implements ImFriendService { | ||
| 115 | } | 115 | } |
| 116 | 116 | ||
| 117 | @Override | 117 | @Override |
| 118 | + @Caching(evict = { | ||
| 119 | + @CacheEvict(cacheNames = FRIEND_STATE, key = "#request.fromUserId + '_' + #request.toUserId"), | ||
| 120 | + @CacheEvict(cacheNames = FRIEND_STATE, key = "#request.toUserId + '_' + #request.fromUserId") | ||
| 121 | + }) | ||
| 118 | @Transactional(rollbackFor = Exception.class) | 122 | @Transactional(rollbackFor = Exception.class) |
| 119 | public void becomeFriends(ImFriendRequestDO request) { | 123 | public void becomeFriends(ImFriendRequestDO request) { |
| 120 | Long fromUserId = request.getFromUserId(); | 124 | Long fromUserId = request.getFromUserId(); |
| 121 | Long toUserId = request.getToUserId(); | 125 | Long toUserId = request.getToUserId(); |
| 122 | // 1. 双向建立关系:A 侧带申请的 displayName / addSource;B 侧 displayName 为空、addSource 同来源 | 126 | // 1. 双向建立关系:A 侧带申请的 displayName / addSource;B 侧 displayName 为空、addSource 同来源 |
| 127 | + // FRIEND_STATE 双向失效由方法上的 @Caching 注解处理;framework 已开 transactionAware 自动延迟到 afterCommit | ||
| 123 | getSelf().addFriend0(fromUserId, toUserId, request.getDisplayName(), request.getAddSource()); | 128 | getSelf().addFriend0(fromUserId, toUserId, request.getDisplayName(), request.getAddSource()); |
| 124 | getSelf().addFriend0(toUserId, fromUserId, null, request.getAddSource()); | 129 | getSelf().addFriend0(toUserId, fromUserId, null, request.getAddSource()); |
| 125 | 130 | ||
| @@ -139,9 +144,13 @@ public class ImFriendServiceImpl implements ImFriendService { | @@ -139,9 +144,13 @@ public class ImFriendServiceImpl implements ImFriendService { | ||
| 139 | } | 144 | } |
| 140 | 145 | ||
| 141 | @Override | 146 | @Override |
| 147 | + @Caching(evict = { | ||
| 148 | + @CacheEvict(cacheNames = FRIEND_STATE, key = "#userId + '_' + #friendUserId"), | ||
| 149 | + @CacheEvict(cacheNames = FRIEND_STATE, key = "#friendUserId + '_' + #userId") | ||
| 150 | + }) | ||
| 142 | @Transactional(rollbackFor = Exception.class) | 151 | @Transactional(rollbackFor = Exception.class) |
| 143 | public void silentReAddFriend(Long userId, Long friendUserId, String displayName, Integer addSource) { | 152 | public void silentReAddFriend(Long userId, Long friendUserId, String displayName, Integer addSource) { |
| 144 | - // 1. 单边重新启用我侧好友关系;addFriend0 内部已做 @CacheEvict 双向失效 | 153 | + // 1. 单边重新启用我侧好友关系 |
| 145 | getSelf().addFriend0(userId, friendUserId, displayName, addSource); | 154 | getSelf().addFriend0(userId, friendUserId, displayName, addSource); |
| 146 | 155 | ||
| 147 | // 2. 仅推 FRIEND_ADD 给 userId 多端(不通知对方,保持「对方一直把我当好友」的错觉) | 156 | // 2. 仅推 FRIEND_ADD 给 userId 多端(不通知对方,保持「对方一直把我当好友」的错觉) |
| @@ -152,6 +161,10 @@ public class ImFriendServiceImpl implements ImFriendService { | @@ -152,6 +161,10 @@ public class ImFriendServiceImpl implements ImFriendService { | ||
| 152 | } | 161 | } |
| 153 | 162 | ||
| 154 | @Override | 163 | @Override |
| 164 | + @Caching(evict = { | ||
| 165 | + @CacheEvict(cacheNames = FRIEND_STATE, key = "#userId + '_' + #friendUserId"), | ||
| 166 | + @CacheEvict(cacheNames = FRIEND_STATE, key = "#friendUserId + '_' + #userId") | ||
| 167 | + }) | ||
| 155 | @Transactional(rollbackFor = Exception.class) | 168 | @Transactional(rollbackFor = Exception.class) |
| 156 | public void deleteFriend(Long userId, Long friendUserId) { | 169 | public void deleteFriend(Long userId, Long friendUserId) { |
| 157 | // 1. 单边软删:仅 userId 视角的关系置 DISABLE;friendUserId 视角不动 | 170 | // 1. 单边软删:仅 userId 视角的关系置 DISABLE;friendUserId 视角不动 |
| @@ -220,17 +233,15 @@ public class ImFriendServiceImpl implements ImFriendService { | @@ -220,17 +233,15 @@ public class ImFriendServiceImpl implements ImFriendService { | ||
| 220 | } | 233 | } |
| 221 | 234 | ||
| 222 | /** | 235 | /** |
| 223 | - * 单向绑定好友关系(内部方法,被 {@link #becomeFriends} 调用): | 236 | + * 单向绑定好友关系(内部方法,被 {@link #becomeFriends} / {@link #silentReAddFriend} 调用): |
| 224 | * - 情况一:已存在记录(含 ENABLE / DISABLE)→ 复用并恢复 ENABLE,重置 muted / pinned / blocked 为 false(删好友再加回来不沿袭旧设置) | 237 | * - 情况一:已存在记录(含 ENABLE / DISABLE)→ 复用并恢复 ENABLE,重置 muted / pinned / blocked 为 false(删好友再加回来不沿袭旧设置) |
| 225 | * - 情况二:不存在记录 → 直接插入新记录 | 238 | * - 情况二:不存在记录 → 直接插入新记录 |
| 226 | * <p> | 239 | * <p> |
| 227 | - * 并发安全:业务侧通过 friend_request.handle_result 已保证 agree 不会并发触发; | ||
| 228 | - * 极端并发下若插入唯一键冲突,让 DuplicateKeyException 向外抛出,外层事务回滚。 | 240 | + * 并发安全:agree 路径由 {@code friend_request.handle_result} 的乐观锁单边推进; |
| 241 | + * 极端并发下若插入唯一键冲突,让 DuplicateKeyException 向外抛出,外层事务回滚。 | ||
| 242 | + * <p> | ||
| 243 | + * FRIEND_STATE 缓存失效由调用方的 @Caching 注解统一处理,本方法不主动 evict | ||
| 229 | */ | 244 | */ |
| 230 | - @Caching(evict = { | ||
| 231 | - @CacheEvict(cacheNames = FRIEND_STATE, key = "#userId + '_' + #friendUserId"), | ||
| 232 | - @CacheEvict(cacheNames = FRIEND_STATE, key = "#friendUserId + '_' + #userId") | ||
| 233 | - }) | ||
| 234 | public void addFriend0(Long userId, Long friendUserId, String displayName, Integer addSource) { | 245 | public void addFriend0(Long userId, Long friendUserId, String displayName, Integer addSource) { |
| 235 | ImFriendDO exists = friendMapper.selectByUserIdAndFriendUserId(userId, friendUserId); | 246 | ImFriendDO exists = friendMapper.selectByUserIdAndFriendUserId(userId, friendUserId); |
| 236 | // 情况一:复用旧记录 → 恢复 ENABLE + 重置 muted / pinned / blocked 与首次新增对齐 | 247 | // 情况一:复用旧记录 → 恢复 ENABLE + 重置 muted / pinned / blocked 与首次新增对齐 |
| @@ -259,11 +270,9 @@ public class ImFriendServiceImpl implements ImFriendService { | @@ -259,11 +270,9 @@ public class ImFriendServiceImpl implements ImFriendService { | ||
| 259 | * 单向解除好友关系(status 设为 DISABLE,记录 deleteTime) | 270 | * 单向解除好友关系(status 设为 DISABLE,记录 deleteTime) |
| 260 | * <p> | 271 | * <p> |
| 261 | * blocked 不主动重置:删好友期间保留拉黑状态;如果未来再 addFriend0,由 addFriend0 统一重置 | 272 | * blocked 不主动重置:删好友期间保留拉黑状态;如果未来再 addFriend0,由 addFriend0 统一重置 |
| 273 | + * <p> | ||
| 274 | + * FRIEND_STATE 缓存失效由调用方的 @Caching 注解统一处理,本方法不主动 evict | ||
| 262 | */ | 275 | */ |
| 263 | - @Caching(evict = { | ||
| 264 | - @CacheEvict(cacheNames = FRIEND_STATE, key = "#userId + '_' + #friendUserId"), | ||
| 265 | - @CacheEvict(cacheNames = FRIEND_STATE, key = "#friendUserId + '_' + #userId") | ||
| 266 | - }) | ||
| 267 | public void deleteFriend0(Long userId, Long friendUserId) { | 276 | public void deleteFriend0(Long userId, Long friendUserId) { |
| 268 | ImFriendDO exists = friendMapper.selectByUserIdAndFriendUserId(userId, friendUserId); | 277 | ImFriendDO exists = friendMapper.selectByUserIdAndFriendUserId(userId, friendUserId); |
| 269 | if (exists == null || CommonStatusEnum.isDisable(exists.getStatus())) { | 278 | if (exists == null || CommonStatusEnum.isDisable(exists.getStatus())) { |
| @@ -273,10 +282,6 @@ public class ImFriendServiceImpl implements ImFriendService { | @@ -273,10 +282,6 @@ public class ImFriendServiceImpl implements ImFriendService { | ||
| 273 | .setStatus(CommonStatusEnum.DISABLE.getStatus()).setDeleteTime(LocalDateTime.now())); | 282 | .setStatus(CommonStatusEnum.DISABLE.getStatus()).setDeleteTime(LocalDateTime.now())); |
| 274 | } | 283 | } |
| 275 | 284 | ||
| 276 | - private ImFriendServiceImpl getSelf() { | ||
| 277 | - return SpringUtil.getBean(getClass()); | ||
| 278 | - } | ||
| 279 | - | ||
| 280 | // ==================== 管理后台 ==================== | 285 | // ==================== 管理后台 ==================== |
| 281 | 286 | ||
| 282 | @Override | 287 | @Override |
| @@ -284,4 +289,8 @@ public class ImFriendServiceImpl implements ImFriendService { | @@ -284,4 +289,8 @@ public class ImFriendServiceImpl implements ImFriendService { | ||
| 284 | return friendMapper.selectPage(reqVO); | 289 | return friendMapper.selectPage(reqVO); |
| 285 | } | 290 | } |
| 286 | 291 | ||
| 292 | + private ImFriendServiceImpl getSelf() { | ||
| 293 | + return SpringUtil.getBean(getClass()); | ||
| 294 | + } | ||
| 295 | + | ||
| 287 | } | 296 | } |
yudao-module-im/src/test/resources/sql/create_tables.sql
| @@ -96,7 +96,8 @@ CREATE TABLE IF NOT EXISTS "im_friend" ( | @@ -96,7 +96,8 @@ CREATE TABLE IF NOT EXISTS "im_friend" ( | ||
| 96 | "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, | 96 | "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| 97 | "deleted" bit NOT NULL DEFAULT FALSE, | 97 | "deleted" bit NOT NULL DEFAULT FALSE, |
| 98 | "tenant_id" bigint NOT NULL DEFAULT 0, | 98 | "tenant_id" bigint NOT NULL DEFAULT 0, |
| 99 | - PRIMARY KEY ("id") | 99 | + PRIMARY KEY ("id"), |
| 100 | + CONSTRAINT "uk_user_friend" UNIQUE ("user_id", "friend_user_id", "tenant_id") | ||
| 100 | ) COMMENT 'IM 好友关系表'; | 101 | ) COMMENT 'IM 好友关系表'; |
| 101 | 102 | ||
| 102 | CREATE TABLE IF NOT EXISTS "im_friend_request" ( | 103 | CREATE TABLE IF NOT EXISTS "im_friend_request" ( |