Commit 7813c4019adc8fd85d2c61af8e6da8a7235da359
1 parent
dc42f0f1
1. 部门的缓存刷新机制
Showing
8 changed files
with
154 additions
and
21 deletions
src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dao/dept/SysDeptMapper.java
| ... | ... | @@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; |
| 7 | 7 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| 8 | 8 | import org.apache.ibatis.annotations.Mapper; |
| 9 | 9 | |
| 10 | +import java.util.Date; | |
| 10 | 11 | import java.util.List; |
| 11 | 12 | |
| 12 | 13 | @Mapper |
| ... | ... | @@ -30,4 +31,9 @@ public interface SysDeptMapper extends BaseMapper<SysDeptDO> { |
| 30 | 31 | return selectCount(new QueryWrapper<SysDeptDO>().eq("parent_id", parentId)); |
| 31 | 32 | } |
| 32 | 33 | |
| 34 | + default boolean selectExistsByUpdateTimeAfter(Date maxUpdateTime) { | |
| 35 | + return selectOne(new QueryWrapper<SysDeptDO>().select("id") | |
| 36 | + .gt("update_time", maxUpdateTime).last("LIMIT 1")) != null; | |
| 37 | + } | |
| 38 | + | |
| 33 | 39 | } | ... | ... |
src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/dept/SysDeptRefreshConsumer.java
0 → 100644
| 1 | +package cn.iocoder.dashboard.modules.system.mq.consumer.dept; | |
| 2 | + | |
| 3 | +import cn.iocoder.dashboard.framework.redis.core.pubsub.AbstractChannelMessageListener; | |
| 4 | +import cn.iocoder.dashboard.modules.system.mq.message.dept.SysDeptRefreshMessage; | |
| 5 | +import cn.iocoder.dashboard.modules.system.service.dept.SysDeptService; | |
| 6 | +import lombok.extern.slf4j.Slf4j; | |
| 7 | +import org.springframework.stereotype.Component; | |
| 8 | + | |
| 9 | +import javax.annotation.Resource; | |
| 10 | + | |
| 11 | +/** | |
| 12 | + * 针对 {@link SysDeptRefreshMessage} 的消费者 | |
| 13 | + * | |
| 14 | + * @author 芋道源码 | |
| 15 | + */ | |
| 16 | +@Component | |
| 17 | +@Slf4j | |
| 18 | +public class SysDeptRefreshConsumer extends AbstractChannelMessageListener<SysDeptRefreshMessage> { | |
| 19 | + | |
| 20 | + @Resource | |
| 21 | + private SysDeptService deptService; | |
| 22 | + | |
| 23 | + @Override | |
| 24 | + public void onMessage(SysDeptRefreshMessage message) { | |
| 25 | + log.info("[onMessage][收到 Dept 刷新消息]"); | |
| 26 | + deptService.initLocalCache(); | |
| 27 | + } | |
| 28 | + | |
| 29 | +} | ... | ... |
src/main/java/cn/iocoder/dashboard/modules/system/mq/message/dept/SysDeptRefreshMessage.java
0 → 100644
| 1 | +package cn.iocoder.dashboard.modules.system.mq.message.dept; | |
| 2 | + | |
| 3 | +import cn.iocoder.dashboard.framework.redis.core.pubsub.ChannelMessage; | |
| 4 | +import lombok.Data; | |
| 5 | + | |
| 6 | +/** | |
| 7 | + * 部门数据刷新 Message | |
| 8 | + */ | |
| 9 | +@Data | |
| 10 | +public class SysDeptRefreshMessage implements ChannelMessage { | |
| 11 | + | |
| 12 | + @Override | |
| 13 | + public String getChannel() { | |
| 14 | + return "system.dept.refresh"; | |
| 15 | + } | |
| 16 | + | |
| 17 | +} | ... | ... |
src/main/java/cn/iocoder/dashboard/modules/system/mq/producer/dept/SysDeptProducer.java
0 → 100644
| 1 | +package cn.iocoder.dashboard.modules.system.mq.producer.dept; | |
| 2 | + | |
| 3 | +import cn.iocoder.dashboard.framework.redis.core.util.RedisMessageUtils; | |
| 4 | +import cn.iocoder.dashboard.modules.system.mq.message.dept.SysDeptRefreshMessage; | |
| 5 | +import org.springframework.data.redis.core.StringRedisTemplate; | |
| 6 | +import org.springframework.stereotype.Component; | |
| 7 | + | |
| 8 | +import javax.annotation.Resource; | |
| 9 | + | |
| 10 | +/** | |
| 11 | + * Dept 部门相关消息的 Producer | |
| 12 | + */ | |
| 13 | +@Component | |
| 14 | +public class SysDeptProducer { | |
| 15 | + | |
| 16 | + @Resource | |
| 17 | + private StringRedisTemplate stringRedisTemplate; | |
| 18 | + | |
| 19 | + /** | |
| 20 | + * 发送 {@link SysDeptRefreshMessage} 消息 | |
| 21 | + */ | |
| 22 | + public void sendMenuRefreshMessage() { | |
| 23 | + SysDeptRefreshMessage message = new SysDeptRefreshMessage(); | |
| 24 | + RedisMessageUtils.sendChannelMessage(stringRedisTemplate, message); | |
| 25 | + } | |
| 26 | + | |
| 27 | +} | ... | ... |
src/main/java/cn/iocoder/dashboard/modules/system/service/dept/SysDeptService.java
| ... | ... | @@ -20,16 +20,9 @@ import java.util.Map; |
| 20 | 20 | public interface SysDeptService { |
| 21 | 21 | |
| 22 | 22 | /** |
| 23 | - * 初始化 | |
| 23 | + * 初始化部门的本地缓存 | |
| 24 | 24 | */ |
| 25 | - void init(); | |
| 26 | - | |
| 27 | - /** | |
| 28 | - * 获得所有部门列表 | |
| 29 | - * | |
| 30 | - * @return 部门列表 | |
| 31 | - */ | |
| 32 | - List<SysDeptDO> listDepts(); | |
| 25 | + void initLocalCache(); | |
| 33 | 26 | |
| 34 | 27 | /** |
| 35 | 28 | * 获得指定编号的部门列表 | ... | ... |
src/main/java/cn/iocoder/dashboard/modules/system/service/dept/impl/SysDeptServiceImpl.java
| ... | ... | @@ -3,6 +3,7 @@ package cn.iocoder.dashboard.modules.system.service.dept.impl; |
| 3 | 3 | import cn.hutool.core.collection.CollUtil; |
| 4 | 4 | import cn.iocoder.dashboard.common.enums.CommonStatusEnum; |
| 5 | 5 | import cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil; |
| 6 | +import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO; | |
| 6 | 7 | import cn.iocoder.dashboard.modules.system.controller.dept.vo.dept.SysDeptCreateReqVO; |
| 7 | 8 | import cn.iocoder.dashboard.modules.system.controller.dept.vo.dept.SysDeptListReqVO; |
| 8 | 9 | import cn.iocoder.dashboard.modules.system.controller.dept.vo.dept.SysDeptUpdateReqVO; |
| ... | ... | @@ -10,19 +11,18 @@ import cn.iocoder.dashboard.modules.system.convert.dept.SysDeptConvert; |
| 10 | 11 | import cn.iocoder.dashboard.modules.system.dal.mysql.dao.dept.SysDeptMapper; |
| 11 | 12 | import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.dept.SysDeptDO; |
| 12 | 13 | import cn.iocoder.dashboard.modules.system.enums.dept.DeptIdEnum; |
| 14 | +import cn.iocoder.dashboard.modules.system.mq.producer.dept.SysDeptProducer; | |
| 13 | 15 | import cn.iocoder.dashboard.modules.system.service.dept.SysDeptService; |
| 14 | 16 | import com.google.common.collect.ImmutableMap; |
| 15 | 17 | import com.google.common.collect.ImmutableMultimap; |
| 16 | 18 | import com.google.common.collect.Multimap; |
| 17 | 19 | import lombok.extern.slf4j.Slf4j; |
| 20 | +import org.springframework.scheduling.annotation.Scheduled; | |
| 18 | 21 | import org.springframework.stereotype.Service; |
| 19 | 22 | |
| 20 | 23 | import javax.annotation.PostConstruct; |
| 21 | 24 | import javax.annotation.Resource; |
| 22 | -import java.util.ArrayList; | |
| 23 | -import java.util.Collection; | |
| 24 | -import java.util.List; | |
| 25 | -import java.util.Map; | |
| 25 | +import java.util.*; | |
| 26 | 26 | |
| 27 | 27 | import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.*; |
| 28 | 28 | |
| ... | ... | @@ -36,6 +36,12 @@ import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.*; |
| 36 | 36 | public class SysDeptServiceImpl implements SysDeptService { |
| 37 | 37 | |
| 38 | 38 | /** |
| 39 | + * 定时执行 {@link #schedulePeriodicRefresh()} 的周期 | |
| 40 | + * 因为已经通过 Redis Pub/Sub 机制,所以频率不需要高 | |
| 41 | + */ | |
| 42 | + private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L; | |
| 43 | + | |
| 44 | + /** | |
| 39 | 45 | * 部门缓存 |
| 40 | 46 | * key:部门编号 {@link SysDeptDO#getId()} |
| 41 | 47 | * |
| ... | ... | @@ -50,30 +56,64 @@ public class SysDeptServiceImpl implements SysDeptService { |
| 50 | 56 | * 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向 |
| 51 | 57 | */ |
| 52 | 58 | private volatile Multimap<Long, SysDeptDO> parentDeptCache; |
| 59 | + /** | |
| 60 | + * 缓存部门的最大更新时间,用于后续的增量轮询,判断是否有更新 | |
| 61 | + */ | |
| 62 | + private volatile Date maxUpdateTime; | |
| 53 | 63 | |
| 54 | 64 | @Resource |
| 55 | 65 | private SysDeptMapper deptMapper; |
| 56 | 66 | |
| 67 | + @Resource | |
| 68 | + private SysDeptProducer deptProducer; | |
| 69 | + | |
| 57 | 70 | @Override |
| 58 | 71 | @PostConstruct |
| 59 | - public void init() { | |
| 60 | - // 从数据库中读取 | |
| 61 | - List<SysDeptDO> sysDeptDOList = deptMapper.selectList(); | |
| 72 | + public synchronized void initLocalCache() { | |
| 73 | + // 获取部门列表,如果有更新 | |
| 74 | + List<SysDeptDO> deptList = this.loadDeptIfUpdate(maxUpdateTime); | |
| 75 | + if (CollUtil.isEmpty(deptList)) { | |
| 76 | + return; | |
| 77 | + } | |
| 78 | + | |
| 62 | 79 | // 构建缓存 |
| 63 | 80 | ImmutableMap.Builder<Long, SysDeptDO> builder = ImmutableMap.builder(); |
| 64 | 81 | ImmutableMultimap.Builder<Long, SysDeptDO> parentBuilder = ImmutableMultimap.builder(); |
| 65 | - sysDeptDOList.forEach(sysRoleDO -> { | |
| 82 | + deptList.forEach(sysRoleDO -> { | |
| 66 | 83 | builder.put(sysRoleDO.getId(), sysRoleDO); |
| 67 | 84 | parentBuilder.put(sysRoleDO.getParentId(), sysRoleDO); |
| 68 | 85 | }); |
| 69 | 86 | // 设置缓存 |
| 70 | 87 | deptCache = builder.build(); |
| 71 | 88 | parentDeptCache = parentBuilder.build(); |
| 72 | - log.info("[init][初始化 Dept 数量为 {}]", sysDeptDOList.size()); | |
| 89 | + assert deptList.size() > 0; // 断言,避免告警 | |
| 90 | + maxUpdateTime = deptList.stream().max(Comparator.comparing(BaseDO::getUpdateTime)).get().getUpdateTime(); | |
| 91 | + log.info("[init][初始化 Dept 数量为 {}]", deptList.size()); | |
| 73 | 92 | } |
| 74 | 93 | |
| 75 | - @Override | |
| 76 | - public List<SysDeptDO> listDepts() { | |
| 94 | + @Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD) | |
| 95 | + public void schedulePeriodicRefresh() { | |
| 96 | + initLocalCache(); | |
| 97 | + } | |
| 98 | + | |
| 99 | + /** | |
| 100 | + * 如果部门发生变化,从数据库中获取最新的全量部门。 | |
| 101 | + * 如果未发生变化,则返回空 | |
| 102 | + * | |
| 103 | + * @param maxUpdateTime 当前部门的最大更新时间 | |
| 104 | + * @return 部门列表 | |
| 105 | + */ | |
| 106 | + private List<SysDeptDO> loadDeptIfUpdate(Date maxUpdateTime) { | |
| 107 | + // 第一步,判断是否要更新。 | |
| 108 | + if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据 | |
| 109 | + log.info("[loadMenuIfUpdate][首次加载全量部门]"); | |
| 110 | + } else { // 判断数据库中是否有更新的部门 | |
| 111 | + if (!deptMapper.selectExistsByUpdateTimeAfter(maxUpdateTime)) { | |
| 112 | + return null; | |
| 113 | + } | |
| 114 | + log.info("[loadMenuIfUpdate][增量加载全量部门]"); | |
| 115 | + } | |
| 116 | + // 第二步,如果有更新,则从数据库加载所有部门 | |
| 77 | 117 | return deptMapper.selectList(); |
| 78 | 118 | } |
| 79 | 119 | |
| ... | ... | @@ -134,6 +174,8 @@ public class SysDeptServiceImpl implements SysDeptService { |
| 134 | 174 | // 插入部门 |
| 135 | 175 | SysDeptDO dept = SysDeptConvert.INSTANCE.convert(reqVO); |
| 136 | 176 | deptMapper.insert(dept); |
| 177 | + // 发送消息 | |
| 178 | + deptProducer.sendMenuRefreshMessage(); | |
| 137 | 179 | return dept.getId(); |
| 138 | 180 | } |
| 139 | 181 | |
| ... | ... | @@ -144,6 +186,8 @@ public class SysDeptServiceImpl implements SysDeptService { |
| 144 | 186 | // 更新部门 |
| 145 | 187 | SysDeptDO updateObj = SysDeptConvert.INSTANCE.convert(reqVO); |
| 146 | 188 | deptMapper.updateById(updateObj); |
| 189 | + // 发送消息 | |
| 190 | + deptProducer.sendMenuRefreshMessage(); | |
| 147 | 191 | } |
| 148 | 192 | |
| 149 | 193 | @Override |
| ... | ... | @@ -156,6 +200,8 @@ public class SysDeptServiceImpl implements SysDeptService { |
| 156 | 200 | } |
| 157 | 201 | // 删除部门 |
| 158 | 202 | deptMapper.deleteById(id); |
| 203 | + // 发送消息 | |
| 204 | + deptProducer.sendMenuRefreshMessage(); | |
| 159 | 205 | } |
| 160 | 206 | |
| 161 | 207 | private void checkCreateOrUpdate(Long id, Long parentId, String name) { | ... | ... |
src/main/java/cn/iocoder/dashboard/modules/system/service/dict/impl/SysDictDataServiceImpl.java
| ... | ... | @@ -82,7 +82,7 @@ public class SysDictDataServiceImpl implements SysDictDataService { |
| 82 | 82 | |
| 83 | 83 | @Override |
| 84 | 84 | @PostConstruct |
| 85 | - public void initLocalCache() { | |
| 85 | + public synchronized void initLocalCache() { | |
| 86 | 86 | // 获取字典数据列表,如果有更新 |
| 87 | 87 | List<SysDictDataDO> dataList = this.loadDictDataIfUpdate(maxUpdateTime); |
| 88 | 88 | if (CollUtil.isEmpty(dataList)) { | ... | ... |
src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysMenuServiceImpl.java
| ... | ... | @@ -21,6 +21,9 @@ import com.google.common.collect.Multimap; |
| 21 | 21 | import lombok.extern.slf4j.Slf4j; |
| 22 | 22 | import org.springframework.scheduling.annotation.Scheduled; |
| 23 | 23 | import org.springframework.stereotype.Service; |
| 24 | +import org.springframework.transaction.annotation.Transactional; | |
| 25 | +import org.springframework.transaction.support.TransactionSynchronization; | |
| 26 | +import org.springframework.transaction.support.TransactionSynchronizationManager; | |
| 24 | 27 | |
| 25 | 28 | import javax.annotation.PostConstruct; |
| 26 | 29 | import javax.annotation.Resource; |
| ... | ... | @@ -169,6 +172,8 @@ public class SysMenuServiceImpl implements SysMenuService { |
| 169 | 172 | SysMenuDO menu = SysMenuConvert.INSTANCE.convert(reqVO); |
| 170 | 173 | initMenuProperty(menu); |
| 171 | 174 | menuMapper.insert(menu); |
| 175 | + // 发送刷新消息 | |
| 176 | + menuProducer.sendMenuRefreshMessage(); | |
| 172 | 177 | // 返回 |
| 173 | 178 | return menu.getId(); |
| 174 | 179 | } |
| ... | ... | @@ -196,6 +201,7 @@ public class SysMenuServiceImpl implements SysMenuService { |
| 196 | 201 | * |
| 197 | 202 | * @param menuId 菜单编号 |
| 198 | 203 | */ |
| 204 | + @Transactional | |
| 199 | 205 | public void deleteMenu(Long menuId) { |
| 200 | 206 | // 校验更新的菜单是否存在 |
| 201 | 207 | if (menuMapper.selectById(menuId) == null) { |
| ... | ... | @@ -213,6 +219,15 @@ public class SysMenuServiceImpl implements SysMenuService { |
| 213 | 219 | menuMapper.deleteById(menuId); |
| 214 | 220 | // 删除授予给角色的权限 |
| 215 | 221 | permissionService.processMenuDeleted(menuId); |
| 222 | + // 发送刷新消息. 注意,需要事务提交后,在进行发送消息。不然 db 还未提交,结果缓存先刷新了 | |
| 223 | + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { | |
| 224 | + | |
| 225 | + @Override | |
| 226 | + public void afterCommit() { | |
| 227 | + menuProducer.sendMenuRefreshMessage(); | |
| 228 | + } | |
| 229 | + | |
| 230 | + }); | |
| 216 | 231 | } |
| 217 | 232 | |
| 218 | 233 | @Override | ... | ... |