Commit 7813c4019adc8fd85d2c61af8e6da8a7235da359

Authored by YunaiV
1 parent dc42f0f1

1. 部门的缓存刷新机制

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
... ...