本地缓存
系统使用本地缓存,提升公用逻辑的执行效率。例如说:
- 租户模块module/system/service/tenant/TenantServiceImpl.java缓存租户信息,每次 RESTful API 校验租户是否禁用、过期时,无需读库。
- 部门模块module/system/service/dept/DeptServiceImpl.java缓存部门信息,每次数据权限校验时,无需读库。
- 权限模块module/system/service/permission/PermissionServiceImpl.java缓存权限信息,每次功能权限校验时,无需读库。
考虑到本地缓存的实时刷新,并未采用 Spring Cache 框架,而是使用 Map 缓存数据,通过 Redis Pub/Sub 实时刷新,通过 Job 兜底定时刷新。整体流程如下:
以 角色模块module/system/service/permission/RoleServiceImpl.java为例,讲解如何实现角色信息的本地缓存。
1. 初始化缓存
① 在 module/system/service/permission/RoleService.java接口中,定义 #initLocalCache() 方法。代码如下:
// RoleService.java/*** 初始化角色的本地缓存*/void initLocalCache();
为什么要定义接口方法?
稍后实时刷新缓存时,会调用 RoleService 接口的该方法。
② 在 RoleServiceImpl类中,实现 #initLocalCache() 方法,通过 @PostConstruct 注解,在项目启动时进行本地缓存的初始化。代码如下:
// RoleServiceImpl.java/*** 角色缓存* key:角色编号 {@link RoleDO#getId()}** 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向*/@Getterprivate volatile Map<Long, RoleDO> roleCache;/*** 缓存角色的最大更新时间,用于后续的增量轮询,判断是否有更新*/@Getterprivate volatile Date maxUpdateTime;/*** 初始化 {@link #roleCache} 缓存*/@Override@PostConstruct // Spring 启动时,自动初始化执行@TenantIgnore // 忽略自动多租户,全局初始化缓存public void initLocalCache() {// 获取角色列表,如果有更新List<RoleDO> roleList = loadRoleIfUpdate(maxUpdateTime);if (CollUtil.isEmpty(roleList)) {return;}// 写入缓存roleCache = CollectionUtils.convertMap(roleList, RoleDO::getId);maxUpdateTime = CollectionUtils.getMaxValue(roleList, RoleDO::getUpdateTime);log.info("[initLocalCache][初始化 Role 数量为 {}]", roleList.size());}/*** 如果角色发生变化,从数据库中获取最新的全量角色。* 如果未发生变化,则返回空** @param maxUpdateTime 当前角色的最大更新时间* @return 角色列表*/private List<RoleDO> loadRoleIfUpdate(Date maxUpdateTime) {// 第一步,判断是否要更新。if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据log.info("[loadRoleIfUpdate][首次加载全量角色]");} else { // 判断数据库中是否有更新的角色if (roleMapper.selectExistsByUpdateTimeAfter(maxUpdateTime) == null) {return null;}log.info("[loadRoleIfUpdate][增量加载全量角色]");}// 第二步,如果有更新,则从数据库加载所有角色return roleMapper.selectList();}
由于缓存的定时刷新,也是调用 #initLocalCache() 方法,所以这里会有 maxUpdateTime + roleMapper.selectExistsByUpdateTimeAfter(maxUpdateTime) == null 判断数据是否发生变化的逻辑。
疑问:为什么使用 @TenantIgnore 注解?
由于 RoleDO 是多租户隔离,如果不添加 @TenantIgnore 注解,会导致缓存刷新时,只加载某个租户的角色数据,导致本地缓存的错误。
所以,如果缓存的数据不存在多租户隔离的情况,可以不添加 @TenantIgnore 注解。
2. 定时刷新缓存
为什么需要定时刷新缓存呢?原因有两点:
- 【主要】数据库的数据被直接修改,程序无法识别到数据发生了变化。
- 【次要】Redis 或者网络不稳定,导致数据在修改时,无法使用 Redis 发布或者订阅数据发生变化的消息,影响缓存的刷新。
① 在 /module/system/dal/mysql/permission/MenuMapper.java接口中,定义 #selectExistsByUpdateTimeAfter(maxUpdateTime) 方法,通过最后获取数据时的 maxUpdateTime 最后更新时间,不断轮询是否有 update_time 更新超过它的数据。如果有,说明数据发生了变化。代码如下:
@Select("SELECT id FROM system_role WHERE update_time > #{maxUpdateTime} LIMIT 1")
RoleDO selectExistsByUpdateTimeAfter(Date maxUpdateTime);
疑问:为什么使用 @Select 来手写 SQL 语句?
考虑到 MyBatis Plus 会自动拼接 WHERE deleted = 0 的过滤逻辑删除的条件,导致删除数据的场景,会查询不到数据的变化。
因此,只能通过 @Select 或者 XML 的方式来手写 SQL 语句来避免自动拼接 WHERE deleted = 0 条件。
② 在 RoleServiceImpl类中,实现 #schedulePeriodicRefresh() 方法,通过 @Scheduled 注解声明本地定时任务,每 5 分钟轮询数据是否发生变化。如果发生变化,重新初始化缓存。代码如下:
// RoleServiceImpl.java
/**
* 定时执行 {@link #schedulePeriodicRefresh()} 的周期
* 因为已经通过 Redis Pub/Sub 机制,所以频率不需要高
*/
private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L;
@Resource
@Lazy // 注入自己,所以延迟加载
private RoleService self;
@Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD)
public void schedulePeriodicRefresh() {
self.initLocalCache(); // self 是为了 @TenantIgnore 的 AOP 忽略租户失效
}
3. 实时刷新缓存
为什么需要使用 Redis Pub/Sub 来实时刷新缓存?考虑到高可用,线上会部署多个 JVM 实例,需要通过 Redis 广播到所有实例,实现本地缓存的刷新。
友情提示:
Redis Pub/Sub 的使用与讲解,可见 《开发指南 —— 消息队列》 文档。
3.1 RoleRefreshMessage
新建 module/system/mq/message/permission/RoleRefreshMessage.java类,角色数据刷新 Message。代码如下:
@Data
@EqualsAndHashCode(callSuper = true)
public class RoleRefreshMessage extends AbstractChannelMessage {
@Override
public String getChannel() {
return "system.role.refresh";
}
}
3.2 RoleProducer
① 新建 module/system/mq/producer/permission/RoleProducer.java类,RoleRefreshMessage 的 Producer 生产者。代码如下:
② 在数据的新增 / 修改 / 删除等写入操作时,需要使用 RoleProducer 发送消息。如下图所示:
3.3 RoleRefreshConsumer
新建 module/system/mq/consumer/permission/RoleRefreshConsumer.java类,RoleRefreshMessage 的 Consumer 消费者,刷新本地缓存。代码如下:
@Component
@Slf4j
public class RoleRefreshConsumer extends AbstractChannelMessageListener<RoleRefreshMessage> {
@Resource
private RoleService roleService;
@Override
public void onMessage(RoleRefreshMessage message) {
log.info("[onMessage][收到 Role 刷新消息]");
roleService.initLocalCache();
}
}
