Procházet zdrojové kódy

缓存改造:Dept 使用 Redis 作为缓存

YunaiV před 2 roky
rodič
revize
2db6a4510c
16 změnil soubory, kde provedl 160 přidání a 212 odebrání
  1. 3 3
      yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantAutoConfiguration.java
  2. 11 3
      yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisCacheManager.java
  3. 17 0
      yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/config/YudaoCacheAutoConfiguration.java
  4. 51 0
      yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/core/TimeoutRedisCacheManager.java
  5. 7 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/dept/DeptMapper.java
  6. 14 2
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/RedisKeyConstants.java
  7. 0 29
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/dept/DeptRefreshConsumer.java
  8. 0 21
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/dept/DeptRefreshMessage.java
  9. 0 26
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/dept/DeptProducer.java
  10. 14 12
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptService.java
  11. 31 99
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceImpl.java
  12. 7 9
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceImpl.java
  13. 1 2
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java
  14. 1 3
      yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceTest.java
  15. 2 2
      yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImplTest.java
  16. 1 0
      yudao-server/src/main/resources/application.yaml

+ 3 - 3
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantAutoConfiguration.java

@@ -133,9 +133,9 @@ public class YudaoTenantAutoConfiguration {
 
     @Bean
     @Primary // 引入租户时,tenantRedisCacheManager 为主 Bean
-    public RedisCacheManager tenantRedisCacheManager(RedisTemplate<String, Object> redisTemplate,
-                                                     RedisCacheConfiguration redisCacheConfiguration,
-                                                     TenantProperties tenantProperties) {
+    public RedisCacheManager redisCacheManager(RedisTemplate<String, Object> redisTemplate,
+                                               RedisCacheConfiguration redisCacheConfiguration,
+                                               TenantProperties tenantProperties) {
         // 创建 RedisCacheWriter 对象
         RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory());
         RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory);

+ 11 - 3
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisCacheManager.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.framework.tenant.core.redis;
 
 import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.redis.core.TimeoutRedisCacheManager;
 import cn.iocoder.yudao.framework.tenant.config.TenantProperties;
 import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
 import jodd.io.StreamUtil;
@@ -13,12 +14,19 @@ import org.springframework.data.redis.cache.RedisCacheWriter;
 /**
  * 多租户的 {@link RedisCacheManager} 实现类
  *
- * 操作指定 name 的 {@link Cache} 时,自动拼接租户后缀,格式为 name + ":" + tenantId + 后缀
+ * 操作指定 name 的 {@link Cache} 时,自动拼接租户后缀,格式为 name + "::t" + tenantId + 后缀
  *
  * @author airhead
  */
 @Slf4j
-public class TenantRedisCacheManager extends RedisCacheManager {
+public class TenantRedisCacheManager extends TimeoutRedisCacheManager {
+
+    /**
+     * 多租户 Redis Key 的前缀,补充在原有 name 的 : 后面
+     *
+     * 原因:如果只补充租户编号,可读性较差
+     */
+    private static final String PREFIX = "t";
 
     private final TenantProperties tenantProperties;
 
@@ -33,7 +41,7 @@ public class TenantRedisCacheManager extends RedisCacheManager {
     public Cache getCache(String name) {
         // 如果不忽略多租户的 Cache,则自动拼接租户后缀
         if (!tenantProperties.getIgnoreCaches().contains(name)) {
-            name = name + StrUtil.COLON + TenantContextHolder.getRequiredTenantId();
+            name = name + StrUtil.COLON + PREFIX + TenantContextHolder.getRequiredTenantId();
         }
 
         // 继续基于父方法

+ 17 - 0
yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/config/YudaoCacheAutoConfiguration.java

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.framework.redis.config;
 
+import cn.iocoder.yudao.framework.redis.core.TimeoutRedisCacheManager;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.boot.autoconfigure.cache.CacheProperties;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
@@ -7,9 +8,15 @@ import org.springframework.cache.annotation.EnableCaching;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Primary;
 import org.springframework.data.redis.cache.RedisCacheConfiguration;
+import org.springframework.data.redis.cache.RedisCacheManager;
+import org.springframework.data.redis.cache.RedisCacheWriter;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.data.redis.serializer.RedisSerializationContext;
 import org.springframework.data.redis.serializer.RedisSerializer;
 
+import java.util.Objects;
+
 import static cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration.buildRedisSerializer;
 
 /**
@@ -50,4 +57,14 @@ public class YudaoCacheAutoConfiguration {
         return config;
     }
 
+    @Bean
+    public RedisCacheManager redisCacheManager(RedisTemplate<String, Object> redisTemplate,
+                                               RedisCacheConfiguration redisCacheConfiguration) {
+        // 创建 RedisCacheWriter 对象
+        RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory());
+        RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory);
+        // 创建 TenantRedisCacheManager 对象
+        return new TimeoutRedisCacheManager(cacheWriter, redisCacheConfiguration);
+    }
+
 }

+ 51 - 0
yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/core/TimeoutRedisCacheManager.java

@@ -0,0 +1,51 @@
+package cn.iocoder.yudao.framework.redis.core;
+
+import cn.hutool.core.util.StrUtil;
+import org.springframework.boot.convert.DurationStyle;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.data.redis.cache.RedisCache;
+import org.springframework.data.redis.cache.RedisCacheConfiguration;
+import org.springframework.data.redis.cache.RedisCacheManager;
+import org.springframework.data.redis.cache.RedisCacheWriter;
+
+import java.time.Duration;
+import java.time.temporal.ChronoUnit;
+
+/**
+ * 支持自定义过期时间的 {@link RedisCacheManager} 实现类
+ *
+ * 在 {@link Cacheable#cacheNames()} 格式为 "key#ttl" 时,# 后面的 ttl 为过期时间,单位为秒
+ *
+ * @author 芋道源码
+ */
+public class TimeoutRedisCacheManager extends RedisCacheManager {
+
+    private static final String SPLIT = "#";
+
+    public TimeoutRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
+        super(cacheWriter, defaultCacheConfiguration);
+    }
+
+    @Override
+    protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
+        if (StrUtil.isEmpty(name)) {
+            return super.createRedisCache(name, cacheConfig);
+        }
+        // 如果使用 # 分隔,大小不为 2,则说明不使用自定义过期时间
+        String[] names = StrUtil.splitToArray(name, SPLIT);
+        if (names.length != 2) {
+            return super.createRedisCache(name, cacheConfig);
+        }
+
+        // 核心:通过修改 cacheConfig 的过期时间,实现自定义过期时间
+        if (cacheConfig != null) {
+            // 移除 # 后面的 : 以及后面的内容,避免影响解析
+            names[1] = StrUtil.subBefore(names[1], StrUtil.COLON, false);
+            // 解析时间
+            Duration duration = DurationStyle.detectAndParse(names[1], ChronoUnit.SECONDS);
+            cacheConfig = cacheConfig.entryTtl(duration);
+        }
+        return super.createRedisCache(names[0], cacheConfig);
+    }
+
+}

+ 7 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/dept/DeptMapper.java

@@ -2,14 +2,16 @@ package cn.iocoder.yudao.module.system.dal.mysql.dept;
 
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.framework.tenant.core.db.dynamic.TenantDS;
 import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptListReqVO;
 import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO;
 import org.apache.ibatis.annotations.Mapper;
 
+import java.util.Collection;
 import java.util.List;
 
 @Mapper
-// TODO 芋艿:@TenantDS
+@TenantDS
 public interface DeptMapper extends BaseMapperX<DeptDO> {
 
     default List<DeptDO> selectList(DeptListReqVO reqVO) {
@@ -26,4 +28,8 @@ public interface DeptMapper extends BaseMapperX<DeptDO> {
         return selectCount(DeptDO::getParentId, parentId);
     }
 
+    default List<DeptDO> selectListByParentId(Collection<Long> parentIds) {
+        return selectList(DeptDO::getParentId, parentIds);
+    }
+
 }

+ 14 - 2
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/RedisKeyConstants.java

@@ -40,7 +40,7 @@ public interface RedisKeyConstants {
      * KEY 格式:user_role_ids::{userId}
      * 数据类型:String 角色编号集合
      */
-    String USER_ROLE_ID = "user_role_id";
+    String USER_ROLE_ID_LIST = "user_role_ids";
 
     /**
      * 拥有指定菜单的角色编号的缓存
@@ -48,6 +48,18 @@ public interface RedisKeyConstants {
      * KEY 格式:user_role_ids::{menuId}
      * 数据类型:String 角色编号集合
      */
-    String MENU_ROLE_ID = "menu_role_id";
+    String MENU_ROLE_ID_LIST = "menu_role_ids";
+
+    /**
+     * 指定部门的所有子部门编号数组的缓存
+     *
+     * KEY 格式:dept_children_ids::{id}
+     * 数据类型:String 子部门编号集合
+     */
+    String DEPT_CHILDREN_ID_LIST = "dept_children_ids";
+    /**
+     * {@link #DEPT_CHILDREN_ID_LIST} 的过期时间
+     */
+    String DEPT_CHILDREN_ID_LIST_EXPIRE = "30s";
 
 }

+ 0 - 29
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/consumer/dept/DeptRefreshConsumer.java

@@ -1,29 +0,0 @@
-package cn.iocoder.yudao.module.system.mq.consumer.dept;
-
-import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
-import cn.iocoder.yudao.module.system.mq.message.dept.DeptRefreshMessage;
-import cn.iocoder.yudao.module.system.service.dept.DeptService;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Component;
-
-import javax.annotation.Resource;
-
-/**
- * 针对 {@link DeptRefreshMessage} 的消费者
- *
- * @author 芋道源码
- */
-@Component
-@Slf4j
-public class DeptRefreshConsumer extends AbstractChannelMessageListener<DeptRefreshMessage> {
-
-    @Resource
-    private DeptService deptService;
-
-    @Override
-    public void onMessage(DeptRefreshMessage message) {
-        log.info("[onMessage][收到 Dept 刷新消息]");
-        deptService.initLocalCache();
-    }
-
-}

+ 0 - 21
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/dept/DeptRefreshMessage.java

@@ -1,21 +0,0 @@
-package cn.iocoder.yudao.module.system.mq.message.dept;
-
-import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage;
-import lombok.Data;
-import lombok.EqualsAndHashCode;
-
-/**
- * 部门数据刷新 Message
- *
- * @author 芋道源码
- */
-@Data
-@EqualsAndHashCode(callSuper = true)
-public class DeptRefreshMessage extends AbstractChannelMessage {
-
-    @Override
-    public String getChannel() {
-        return "system.dept.refresh";
-    }
-
-}

+ 0 - 26
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/dept/DeptProducer.java

@@ -1,26 +0,0 @@
-package cn.iocoder.yudao.module.system.mq.producer.dept;
-
-import cn.iocoder.yudao.module.system.mq.message.dept.DeptRefreshMessage;
-import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate;
-import org.springframework.stereotype.Component;
-
-import javax.annotation.Resource;
-
-/**
- * Dept 部门相关消息的 Producer
- */
-@Component
-public class DeptProducer {
-
-    @Resource
-    private RedisMQTemplate redisMQTemplate;
-
-    /**
-     * 发送 {@link DeptRefreshMessage} 消息
-     */
-    public void sendDeptRefreshMessage() {
-        DeptRefreshMessage message = new DeptRefreshMessage();
-        redisMQTemplate.send(message);
-    }
-
-}

+ 14 - 12
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptService.java

@@ -7,10 +7,7 @@ import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptListReqV
 import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptUpdateReqVO;
 import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO;
 
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 
 /**
  * 部门 Service 接口
@@ -19,11 +16,6 @@ import java.util.Map;
  */
 public interface DeptService {
 
-    /**
-     * 初始化部门的本地缓存
-     */
-    void initLocalCache();
-
     /**
      * 创建部门
      *
@@ -54,14 +46,24 @@ public interface DeptService {
      */
     List<DeptDO> getDeptList(DeptListReqVO reqVO);
 
+    /**
+     * 获得指定部门的所有子部门
+     *
+     * @param id 部门编号
+     * @return 子部门列表
+     */
+    List<DeptDO> getChildDeptList(Long id);
+
     /**
      * 获得所有子部门,从缓存中
      *
-     * @param parentId 部门编号
-     * @param recursive 是否递归获取所有
+     * 注意,该缓存不是实时更新,最多会有 1 分钟延迟。
+     * 一般来说,不会影响使用,因为部门的变更,不会频繁发生。
+     *
+     * @param id 父部门编号
      * @return 子部门列表
      */
-    List<DeptDO> getDeptListByParentIdFromCache(Long parentId, boolean recursive);
+    Set<Long> getChildDeptIdListFromCache(Long id);
 
     /**
      * 获得部门信息数组

+ 31 - 99
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceImpl.java

@@ -2,29 +2,25 @@ package cn.iocoder.yudao.module.system.service.dept;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
-import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
-import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
+import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
 import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptCreateReqVO;
 import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptListReqVO;
 import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptUpdateReqVO;
 import cn.iocoder.yudao.module.system.convert.dept.DeptConvert;
 import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO;
 import cn.iocoder.yudao.module.system.dal.mysql.dept.DeptMapper;
+import cn.iocoder.yudao.module.system.dal.redis.RedisKeyConstants;
 import cn.iocoder.yudao.module.system.enums.dept.DeptIdEnum;
-import cn.iocoder.yudao.module.system.mq.producer.dept.DeptProducer;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableMultimap;
-import com.google.common.collect.Multimap;
-import lombok.Getter;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.cache.annotation.Cacheable;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
-import javax.annotation.PostConstruct;
 import javax.annotation.Resource;
 import java.util.*;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
 import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
 
 /**
@@ -37,54 +33,9 @@ import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
 @Slf4j
 public class DeptServiceImpl implements DeptService {
 
-    /**
-     * 部门缓存
-     * key:部门编号 {@link DeptDO#getId()}
-     *
-     * 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向
-     */
-    @Getter
-    private volatile Map<Long, DeptDO> deptCache;
-    /**
-     * 父部门缓存
-     * key:部门编号 {@link DeptDO#getParentId()}
-     * value: 直接子部门列表
-     *
-     * 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向
-     */
-    @Getter
-    private volatile Multimap<Long, DeptDO> parentDeptCache;
-
     @Resource
     private DeptMapper deptMapper;
 
-    @Resource
-    private DeptProducer deptProducer;
-
-    /**
-     * 初始化 {@link #parentDeptCache} 和 {@link #deptCache} 缓存
-     */
-    @Override
-    @PostConstruct
-    public synchronized void initLocalCache() {
-        // 注意:忽略自动多租户,因为要全局初始化缓存
-        TenantUtils.executeIgnore(() -> {
-            // 第一步:查询数据
-            List<DeptDO> depts = deptMapper.selectList();
-            log.info("[initLocalCache][缓存部门,数量为:{}]", depts.size());
-
-            // 第二步:构建缓存
-            ImmutableMap.Builder<Long, DeptDO> builder = ImmutableMap.builder();
-            ImmutableMultimap.Builder<Long, DeptDO> parentBuilder = ImmutableMultimap.builder();
-            depts.forEach(sysRoleDO -> {
-                builder.put(sysRoleDO.getId(), sysRoleDO);
-                parentBuilder.put(sysRoleDO.getParentId(), sysRoleDO);
-            });
-            deptCache = builder.build();
-            parentDeptCache = parentBuilder.build();
-        });
-    }
-
     @Override
     public Long createDept(DeptCreateReqVO reqVO) {
         // 校验正确性
@@ -95,8 +46,6 @@ public class DeptServiceImpl implements DeptService {
         // 插入部门
         DeptDO dept = DeptConvert.INSTANCE.convert(reqVO);
         deptMapper.insert(dept);
-        // 发送刷新消息
-        deptProducer.sendDeptRefreshMessage();
         return dept.getId();
     }
 
@@ -110,8 +59,6 @@ public class DeptServiceImpl implements DeptService {
         // 更新部门
         DeptDO updateObj = DeptConvert.INSTANCE.convert(reqVO);
         deptMapper.updateById(updateObj);
-        // 发送刷新消息
-        deptProducer.sendDeptRefreshMessage();
     }
 
     @Override
@@ -124,8 +71,6 @@ public class DeptServiceImpl implements DeptService {
         }
         // 删除部门
         deptMapper.deleteById(id);
-        // 发送刷新消息
-        deptProducer.sendDeptRefreshMessage();
     }
 
     @Override
@@ -134,48 +79,35 @@ public class DeptServiceImpl implements DeptService {
     }
 
     @Override
-    public List<DeptDO> getDeptListByParentIdFromCache(Long parentId, boolean recursive) {
-        if (parentId == null) {
-            return Collections.emptyList();
+    public List<DeptDO> getChildDeptList(Long id) {
+        List<DeptDO> children = new LinkedList<>();
+        // 遍历每一层
+        Collection<Long> parentIds = Collections.singleton(id);
+        for (int i = 0; i < Short.MAX_VALUE; i++) { // 使用 Short.MAX_VALUE 避免 bug 场景下,存在死循环
+            // 查询当前层,所有的子部门
+            List<DeptDO> depts = deptMapper.selectListByParentId(parentIds);
+            // 1. 如果没有子部门,则结束遍历
+            if (CollUtil.isEmpty(depts)) {
+                break;
+            }
+            // 2. 如果有子部门,继续遍历
+            children.addAll(depts);
+            parentIds = convertSet(depts, DeptDO::getId);
         }
-        List<DeptDO> result = new ArrayList<>();
-        // 递归,简单粗暴
-       getDeptsByParentIdFromCache(result, parentId,
-               recursive ? Integer.MAX_VALUE : 1, // 如果递归获取,则无限;否则,只递归 1 次
-               parentDeptCache);
-        return result;
+        return children;
     }
 
-    /**
-     * 递归获取所有的子部门,添加到 result 结果
-     *
-     * @param result 结果
-     * @param parentId 父编号
-     * @param recursiveCount 递归次数
-     * @param parentDeptMap 父部门 Map,使用缓存,避免变化
-     */
-    private void getDeptsByParentIdFromCache(List<DeptDO> result, Long parentId, int recursiveCount,
-                                             Multimap<Long, DeptDO> parentDeptMap) {
-        // 递归次数为 0,结束!
-        if (recursiveCount == 0) {
-            return;
-        }
-
-        // 获得子部门
-        Collection<DeptDO> depts = parentDeptMap.get(parentId);
-        if (CollUtil.isEmpty(depts)) {
-            return;
-        }
-        // 针对多租户,过滤掉非当前租户的部门
-        Long tenantId = TenantContextHolder.getTenantId();
-        if (tenantId != null) {
-            depts = CollUtil.filterNew(depts, dept -> tenantId.equals(dept.getTenantId()));
-        }
-        result.addAll(depts);
-
-        // 继续递归
-        depts.forEach(dept -> getDeptsByParentIdFromCache(result, dept.getId(),
-                recursiveCount - 1, parentDeptMap));
+    @Override
+    @DataPermission(enable = false) // 禁用数据权限,避免简历不正确的缓存
+    @Cacheable(cacheNames = RedisKeyConstants.DEPT_CHILDREN_ID_LIST
+            + "#" + RedisKeyConstants.DEPT_CHILDREN_ID_LIST_EXPIRE, key = "#id")
+    public Set<Long> getChildDeptIdListFromCache(Long id) {
+        // 补充说明:为什么该缓存会有 1 分钟的延迟?主要有两点:
+        // 1. Spring Cache 无法方便的批量清理,所以使用 Redis 自动过期的方式。
+        // 2. 变更父节点的时候,影响父子节点的数量很多,包括原父节点及其父节点,以及新父节点及其父节点。
+        // 如果你真的对延迟比较敏感,可以考虑采用使用 allEntries = true 的方式,清理所有缓存。
+        List<DeptDO> children = getChildDeptList(id);
+        return convertSet(children, DeptDO::getId);
     }
 
     private void validateForCreateOrUpdate(Long id, Long parentId, String name) {
@@ -205,7 +137,7 @@ public class DeptServiceImpl implements DeptService {
             throw exception(DEPT_NOT_ENABLE);
         }
         // 父部门不能是原来的子部门
-        List<DeptDO> children = getDeptListByParentIdFromCache(id, true);
+        List<DeptDO> children = getChildDeptList(id);
         if (children.stream().anyMatch(dept1 -> dept1.getId().equals(parentId))) {
             throw exception(DEPT_PARENT_IS_CHILD);
         }

+ 7 - 9
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceImpl.java

@@ -9,7 +9,6 @@ import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
 import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
 import cn.iocoder.yudao.module.system.api.permission.dto.DeptDataPermissionRespDTO;
-import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO;
 import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO;
 import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO;
 import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleMenuDO;
@@ -71,7 +70,7 @@ public class PermissionServiceImpl implements PermissionService {
     }
 
     @Override
-    @Cacheable(value = RedisKeyConstants.MENU_ROLE_ID, key = "#menuId")
+    @Cacheable(value = RedisKeyConstants.MENU_ROLE_ID_LIST, key = "#menuId")
     public Set<Long> getMenuRoleIdListByMenuIdFromCache(Long menuId) {
         return convertSet(roleMenuMapper.selectListByMenuId(menuId), RoleMenuDO::getRoleId);
     }
@@ -104,7 +103,7 @@ public class PermissionServiceImpl implements PermissionService {
     }
 
     @Override
-    @Cacheable(value = RedisKeyConstants.USER_ROLE_ID, key = "#userId")
+    @Cacheable(value = RedisKeyConstants.USER_ROLE_ID_LIST, key = "#userId")
     public Set<Long> getUserRoleIdListByUserIdFromCache(Long userId) {
         return getUserRoleIdListByUserId(userId);
     }
@@ -261,7 +260,7 @@ public class PermissionServiceImpl implements PermissionService {
         }
 
         // 获得用户的部门编号的缓存,通过 Guava 的 Suppliers 惰性求值,即有且仅有第一次发起 DB 的查询
-        Supplier<Long> userDeptIdCache = Suppliers.memoize(() -> userService.getUser(userId).getDeptId());
+        Supplier<Long> userDeptId = Suppliers.memoize(() -> userService.getUser(userId).getDeptId());
         // 遍历每个角色,计算
         for (RoleDO role : roles) {
             // 为空时,跳过
@@ -278,20 +277,19 @@ public class PermissionServiceImpl implements PermissionService {
                 CollUtil.addAll(result.getDeptIds(), role.getDataScopeDeptIds());
                 // 自定义可见部门时,保证可以看到自己所在的部门。否则,一些场景下可能会有问题。
                 // 例如说,登录时,基于 t_user 的 username 查询会可能被 dept_id 过滤掉
-                CollUtil.addAll(result.getDeptIds(), userDeptIdCache.get());
+                CollUtil.addAll(result.getDeptIds(), userDeptId.get());
                 continue;
             }
             // 情况三,DEPT_ONLY
             if (Objects.equals(role.getDataScope(), DataScopeEnum.DEPT_ONLY.getScope())) {
-                CollectionUtils.addIfNotNull(result.getDeptIds(), userDeptIdCache.get());
+                CollectionUtils.addIfNotNull(result.getDeptIds(), userDeptId.get());
                 continue;
             }
             // 情况四,DEPT_DEPT_AND_CHILD
             if (Objects.equals(role.getDataScope(), DataScopeEnum.DEPT_AND_CHILD.getScope())) {
-                List<DeptDO> depts = deptService.getDeptListByParentIdFromCache(userDeptIdCache.get(), true);
-                CollUtil.addAll(result.getDeptIds(), CollectionUtils.convertList(depts, DeptDO::getId));
+                CollUtil.addAll(result.getDeptIds(), deptService.getChildDeptIdListFromCache(userDeptId.get()));
                 // 添加本身部门编号
-                CollUtil.addAll(result.getDeptIds(), userDeptIdCache.get());
+                CollUtil.addAll(result.getDeptIds(), userDeptId.get());
                 continue;
             }
             // 情况五,SELF

+ 1 - 2
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java

@@ -290,8 +290,7 @@ public class AdminUserServiceImpl implements AdminUserService {
         if (deptId == null) {
             return Collections.emptySet();
         }
-        Set<Long> deptIds = convertSet(deptService.getDeptListByParentIdFromCache(
-                deptId, true), DeptDO::getId);
+        Set<Long> deptIds = convertSet(deptService.getChildDeptList(deptId), DeptDO::getId);
         deptIds.add(deptId); // 包括自身
         return deptIds;
     }

+ 1 - 3
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceTest.java

@@ -18,7 +18,6 @@ import cn.iocoder.yudao.module.system.mq.producer.permission.PermissionProducer;
 import cn.iocoder.yudao.module.system.service.dept.DeptService;
 import cn.iocoder.yudao.module.system.service.user.AdminUserService;
 import com.google.common.collect.ImmutableMultimap;
-import com.google.common.collect.Multimap;
 import org.junit.jupiter.api.Test;
 import org.springframework.boot.test.mock.mockito.MockBean;
 import org.springframework.context.annotation.Import;
@@ -37,7 +36,6 @@ import static java.util.Collections.singleton;
 import static java.util.Collections.singletonList;
 import static org.junit.jupiter.api.Assertions.*;
 import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.same;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -471,7 +469,7 @@ public class PermissionServiceTest extends BaseDbUnitTest {
         when(userService.getUser(eq(1L))).thenReturn(new AdminUserDO().setDeptId(3L), null, null); // 最后返回 null 的目的,看看会不会重复调用
         // mock 方法(部门)
         DeptDO deptDO = randomPojo(DeptDO.class);
-        when(deptService.getDeptListByParentIdFromCache(eq(3L), eq(true)))
+        when(deptService.getChildDeptIdListFromCache(eq(3L), eq(true)))
                 .thenReturn(singletonList(deptDO));
 
         // 调用

+ 2 - 2
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImplTest.java

@@ -345,7 +345,7 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest {
         reqVO.setDeptId(1L); // 其中,1L 是 2L 的父部门
         // mock 方法
         List<DeptDO> deptList = newArrayList(randomPojo(DeptDO.class, o -> o.setId(2L)));
-        when(deptService.getDeptListByParentIdFromCache(eq(reqVO.getDeptId()), eq(true))).thenReturn(deptList);
+        when(deptService.getChildDeptIdListFromCache(eq(reqVO.getDeptId()), eq(true))).thenReturn(deptList);
 
         // 调用
         PageResult<AdminUserDO> pageResult = userService.getUserPage(reqVO);
@@ -368,7 +368,7 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest {
         reqVO.setDeptId(1L); // 其中,1L 是 2L 的父部门
         // mock 方法
         List<DeptDO> deptList = newArrayList(randomPojo(DeptDO.class, o -> o.setId(2L)));
-        when(deptService.getDeptListByParentIdFromCache(eq(reqVO.getDeptId()), eq(true))).thenReturn(deptList);
+        when(deptService.getChildDeptIdListFromCache(eq(reqVO.getDeptId()), eq(true))).thenReturn(deptList);
 
         // 调用
         List<AdminUserDO> list = userService.getUserList(reqVO);

+ 1 - 0
yudao-server/src/main/resources/application.yaml

@@ -7,6 +7,7 @@ spring:
 
   main:
     allow-circular-references: true # 允许循环依赖,因为项目是三层架构,无法避免这个情况。
+    allow-bean-definition-overriding: true # 允许覆盖 bean 定义
 
   # Servlet 配置
   servlet: