package cn.iocoder.yudao.module.im.dal.redis.rtc; import cn.iocoder.yudao.module.im.enums.ImConversationTypeEnum; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.stereotype.Repository; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.im.dal.redis.RedisKeyConstants.IM_RTC_CALL_LOCK; import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.RTC_INVITE_BUSY; /** * IM 通话同对 / 同群活跃唯一性的锁 Redis DAO *

* invite 入口包一层;锁内做「SELECT 已有活跃通话 → 命中即加入分支;否则 INSERT 新通话」 * * @author 芋道源码 */ @Repository @Slf4j public class ImRtcCallLockRedisDAO { /** * 等待获取锁的最长时间;超时抛 RTC_INVITE_BUSY */ private static final long LOCK_WAIT_MS = 5_000L; /** * 持有锁的最长时间;自动释放兜底;给 LiveKit / DB / 推送偶发慢留余地 */ private static final long LOCK_LEASE_MS = 30_000L; @Resource private RedissonClient redissonClient; /** * 私聊通话锁;按 userId 排序拼 key 保对称(同对呼叫和反向呼叫共用一把锁) */ public V lockPrivate(Long userIdA, Long userIdB, Callable callable) throws Exception { String key = String.format(IM_RTC_CALL_LOCK, ImConversationTypeEnum.PRIVATE.getType(), Math.min(userIdA, userIdB)+ "_" + Math.max(userIdA, userIdB)); return doLock(key, callable); } /** * 群通话锁;同群所有 invite 串行 */ public V lockGroup(Long groupId, Callable callable) throws Exception { String key = String.format(IM_RTC_CALL_LOCK, ImConversationTypeEnum.GROUP.getType(), groupId); return doLock(key, callable); } /** * tryLock(waitTime, leaseTime, unit):waitTime 内拿不到锁直接抛繁忙;拿到后 leaseTime 自动释放 *

* unlock 前用 isHeldByCurrentThread 兜底;业务超过 leaseTime 时锁已自动释放,不再抛 IllegalMonitorStateException */ private V doLock(String lockKey, Callable callable) throws Exception { RLock lock = redissonClient.getLock(lockKey); boolean acquired = lock.tryLock(LOCK_WAIT_MS, LOCK_LEASE_MS, TimeUnit.MILLISECONDS); if (!acquired) { log.error("[doLock][lockKey={} 等待 {}ms 仍未获取到锁]", lockKey, LOCK_WAIT_MS); throw exception(RTC_INVITE_BUSY); } try { return callable.call(); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } else { log.error("[doLock][lockKey={} 业务超过 leaseTime={}ms,锁已被 Redisson 自动释放]", lockKey, LOCK_LEASE_MS); } } } }