Redis 与本地缓存
这篇解决什么
热点数据不要每次打库:令牌、角色、字典、文件客户端各有落点。读完能改 RedisTemplate / @Cacheable,会把租户写进 Key,并判断该用 Redis 还是进程内缓存。
默认连本机 Redis 127.0.0.1:6379,库号 0。管理端前缀仍是 /admin-api。
示意图:业务模块只调 DAO 或注解;框架连 Redis,或把有状态对象放进进程内缓存。
组件位置
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| Redis Starter | Redisson 客户端、RedisTemplate、Spring Cache | ruoyi-office/yudao-framework/yudao-spring-boot-starter-redis/ |
| 连接 | 本机 host / port / database | ruoyi-office/yudao-server/src/main/resources/application-local.yaml |
| 默认 TTL | spring.cache.type=REDIS,time-to-live: 1h | ruoyi-office/yudao-server/src/main/resources/application.yaml |
| 租户 Cache | @Cacheable 的 name 后拼 tenantId | yudao-spring-boot-starter-biz-tenant/.../TenantRedisCacheManager.java |
| Key 常量 | 每个模块一份,像对待表名 | 例如 yudao-module-system-server/.../dal/redis/RedisKeyConstants.java |
| 本地工具 | Guava LoadingCache,上限 10000 | ruoyi-office/yudao-framework/yudao-common/.../CacheUtils.java |
| 监控页 | 基础信息、命令、内存 | ruoyi-office-vben/apps/web-antd/src/views/infra/redis/ |
多数读模型用 Redis。进程内缓存留给「带连接的 Java 对象」和框架热路径。业务数据不要再抄一份本地 Map。
编程式缓存
过期时间和 Key 要自己算时,用 RedisTemplate / StringRedisTemplate。客户端是 Redisson,Starter 在 RedissonAutoConfigurationV2 之前注册自己的 RedisTemplate。
连接与序列化
application-local.yaml 只配地址。生产再开密码,不要把共享环境的 Redis 写进本地默认。
spring:
data:
redis:
host: 127.0.0.1
port: 6379
database: 0YudaoRedisAutoConfiguration:Key 用 String,Value 用 JSON,并注册 JavaTimeModule,避免 LocalDateTime 序列化失败。
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
RedisSerializer<?> redisSerializer = buildRedisSerializer();
template.setValueSerializer(redisSerializer);
template.setHashValueSerializer(redisSerializer);
return template;
}多系统共用一个 Redis 库时,给 spring.cache.redis.key-prefix 加前缀。computePrefixWith 用单冒号 cacheName:,方便客户端按目录看 Key。
Redis Key 常量
每个 yudao-module-xxx-server 的 dal/redis/RedisKeyConstants 集中声明 Key。查一个模块用了哪些缓存,先打开这个接口。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
oauth2_access_token:%s | 访问令牌,TTL 跟过期时间走 | RedisKeyConstants.OAUTH2_ACCESS_TOKEN |
weather:now:%s:%s | 天气结果,实例内跨租户共享 | WEATHER_NOW |
weather:lock:%s:%s | 天气单飞锁 | WEATHER_LOCK |
role / user_role_ids | Spring Cache 的 cacheName | 同文件其它常量 |
简单值可以直接 opsForValue。复杂对象建 DO,放 dal/dataobject,再配一个 RedisDAO。
访问令牌
OAuth2AccessTokenDO 映表 system_oauth2_access_token,并继承 TenantBaseDO。令牌过期时间按客户端配置,不适合固定 1 小时的 @Cacheable。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
OAuth2AccessTokenDO | 令牌实体,租户在字段里 | yudao-module-system-server/.../oauth2/OAuth2AccessTokenDO.java |
OAuth2AccessTokenRedisDAO | StringRedisTemplate + JsonUtils | .../dal/redis/oauth2/OAuth2AccessTokenRedisDAO.java |
OAuth2TokenServiceImpl | 先 Redis,没有再回源 MySQL | .../service/oauth2/OAuth2TokenServiceImpl.java |
public void set(OAuth2AccessTokenDO accessTokenDO) {
String redisKey = formatKey(accessTokenDO.getAccessToken());
accessTokenDO.setUpdater(null).setUpdateTime(null).setCreateTime(null)
.setCreator(null).setDeleted(null);
long time = LocalDateTimeUtil.between(LocalDateTime.now(),
accessTokenDO.getExpiresTime(), ChronoUnit.SECONDS);
if (time > 0) {
stringRedisTemplate.opsForValue().set(redisKey,
JsonUtils.toJsonString(accessTokenDO), time, TimeUnit.SECONDS);
}
}getAccessToken 先 oauth2AccessTokenRedisDAO.get。没有则查库,未过期再回写。删除令牌时库和 Redis 一起清。
RedisTemplate 还是 @Cacheable
TTL 跟业务字段走、要 SETNX 锁:用 RedisDAO。固定过期、读多写少:用 @Cacheable。两种可以同时出现在一个模块里。
声明式缓存
YudaoCacheAutoConfiguration 打开 @EnableCaching,Cache 落 Redis。读方法加注解即可。
@Cacheable(value = "users", key = "#id")
UserDO getUserById(Integer id);注解
| 名称 | 说明 | 仓库路径 |
|---|---|---|
@Cacheable | 有缓存直接返回;没有则执行方法再写入 | Spring Cache |
@CachePut | 每次执行方法,再按条件写入 | 同上 |
@CacheEvict | 删一条或 allEntries 清空该 name | 同上 |
unless | 例如 #result == null 不缓存空值 | RoleServiceImpl |
allEntries 靠 SCAN,批量大小是 yudao.cache.redis-scan-batch-size,默认 30。要按前缀删一批,Spring Cache 做不到,改 RedisDAO 或拆更细的 Key。
TenantRedisCacheManager 和 TimeoutRedisCacheManager 都 setTransactionAware(true)。@Transactional 里的 @CacheEvict / @CachePut 等到 afterCommit。没事务则立刻生效。
角色缓存
角色走被动读:第一次 getRoleFromCache 回源并写入;改库后再删缓存。不必把全表提前灌进 Redis。
@Cacheable(value = RedisKeyConstants.ROLE, key = "#id", unless = "#result == null")
public RoleDO getRoleFromCache(Long id) {
return roleMapper.selectById(id);
}
@CacheEvict(value = RedisKeyConstants.ROLE, key = "#updateReqVO.id")
public void updateRole(RoleSaveReqVO updateReqVO) { /* 先改库 */ }
@CacheEvict(value = RedisKeyConstants.ROLE, key = "#id")
public void deleteRole(Long id) { /* 先删库 */ }批量按 id 取缓存时,Spring Cache 没有批量 get。getRoleListFromCache 拿代理 getSelf(),对每个 id 调 getRoleFromCache,让注解生效。
同类写法还有部门子节点、用户角色、菜单、短信/邮件模板、OAuth 客户端。cacheName 都来自该模块的 RedisKeyConstants。
示意图:先读缓存;写库成功后再删 Key。
过期时间
没写后缀时,用 spring.cache.redis.time-to-live,默认 1 小时。TimeoutRedisCacheManager 认 cacheNames 里的 name#ttl。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 默认 1h | 未写 #ttl 的 @Cacheable | application.yaml 的 spring.cache.redis |
name#30m | 该 Cache 单独 30 分钟 | TimeoutRedisCacheManager |
| 单位 | d 天、h 小时、m 分钟、s 秒;纯数字当秒 | 同上 |
| 资产分类 | asset_category_simple_list#30m | yudao-module-asset-server/.../RedisKeyConstants.java |
String CATEGORY_SIMPLE_LIST = "asset_category_simple_list#30m";# 后面若还带冒号,只取时间段。解析后的 cacheName 不含 ttl 后缀。
租户与 Key
开启多租户后,主 RedisCacheManager 是 TenantRedisCacheManager。getCache(name) 会变成 name:{tenantId},再交给父类拼 Key。
当前上下文忽略租户、或 tenantId 为空、或 name 在 yudao.tenant.ignore-caches 里,则不拼。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 默认分桶 | role + 租户 1 + id 10 → role:1:10 | TenantRedisCacheManager |
ignore-caches | 全局共享的 cacheName | yudao-server/.../application.yaml |
| 已忽略示例 | user_role_ids、oauth_client、sms_template 等 | 同上 yudao.tenant.ignore-caches |
| 编程式 Key | oauth2_access_token:{token} 不自动加租户 | OAuth2AccessTokenRedisDAO |
oauth2_access_token 的租户在 JSON 的 tenantId 里,不在 Key 上。令牌本身全局唯一。天气 Key 也是网格坐标,跨租户共享。
租户必须进隔离 Key
角色、部门、业务列表默认按租户分桶。全局字典式缓存才写进 ignore-caches。自己用 RedisTemplate 拼 Key 时,隔离数据要带 tenantId,不要抄令牌那种「值里带租户、Key 不带」。
菜单全表缓存在 menu_list。表是全局的,查库不按租户滤;未列入 ignore-caches 时,Spring Cache 仍可能按当前租户分一份。
缓存击穿
热点 Key 同时过期,并发会一起打库或打外部 HTTP。@Cacheable 本身不加锁。
天气用 SETNX 单飞:先读 weather:now,没有再抢 weather:lock,只有拿到锁的线程去拉数据。
public boolean tryLock(String part1, String part2) {
Boolean ok = stringRedisTemplate.opsForValue().setIfAbsent(
formatLockKey(part1, part2), "1", 10L, TimeUnit.SECONDS);
return Boolean.TRUE.equals(ok);
}本地 CacheUtils 用 refreshAfterWrite:刷新时其它线程仍读旧值。异步版只阻塞加载线程。
热点回源要单飞
外部接口、大 SQL 不要裸 @Cacheable 然后集体失效。编程式用短 TTL 锁;进程内用 refreshAfterWrite。空结果要 unless 或单独的空值 TTL,避免把「没有」缓存成永久 miss。
本地缓存
Redis 够用就不要再加一层 JVM 缓存。多节点时本地副本不会自动对齐。
还在用本地缓存的,多半是「有状态」对象:文件客户端、微信 SDK、敏感词引擎。它们不好序列化进 Redis。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
buildAsyncReloadingCache | 异步刷新;全局、与人无关 | CacheUtils |
buildCache | 同步刷新;和登录用户 / ThreadLocal 有关 | 同上 |
| 租户校验 | 1 分钟,validTenant / getTenantIds | TenantFrameworkServiceImpl |
| 字典 | 1 分钟,dictType → List | DictFrameworkUtils |
| 权限 | 1 分钟,Key 带 userId:deptId | SecurityFrameworkServiceImpl |
| 文件客户端 | 10 秒,FileClient 单例 | FileConfigServiceImpl |
| 微信 SDK | 10 秒,WxMpService / WxMaService | SocialClientServiceImpl |
| Caffeine | 只给 JSQLParser 解析 SQL,不是业务缓存 | YudaoMybatisAutoConfiguration |
和「人」有关的(当前用户、部门、权限)用 buildCache,避免异步线程丢 ThreadLocal。和「系统」有关的用 buildAsyncReloadingCache。
private final LoadingCache<Long, FileClient> clientCache =
buildAsyncReloadingCache(Duration.ofSeconds(10L),
new CacheLoader<Long, FileClient>() {
@Override
public FileClient load(Long id) {
FileConfigDO config = Objects.equals(0L, id)
? fileConfigMapper.selectByMaster()
: fileConfigMapper.selectById(id);
// createOrUpdateFileClient ...
return fileClientFactory.getFileClient(
config == null ? id : config.getId());
}
});SecurityFrameworkServiceImpl 的权限 Key 必须带会话部门。多组织切换任职后,只按 userId 缓存会继续用旧权限大约 1 分钟。false 不进权限缓存,避免负缓存卡住。
公众号账号仍用 volatile Map + @PostConstruct。initLocalCache 包在 TenantUtils.executeIgnore 里,避免只加载当前租户。多节点靠 @Scheduled 每 60 秒看 updateTime,有变更再全量重建,并重做 MpServiceFactory。
新业务优先 Redis。只有对象带连接、或框架每次请求都要读时,再加 CacheUtils。
Redis 监控
RedisController 调 INFO、DBSIZE、INFO commandstats,拼成 RedisMonitorRespVO。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 接口 | GET /infra/redis/get-monitor-info | yudao-module-infra-server/.../redis/RedisController.java |
| 权限 | infra:redis:get-monitor-info | 同上 @PreAuthorize |
| 页面 | 基础信息、命令统计、内存 | ruoyi-office-vben/apps/web-antd/src/views/infra/redis/ |
| API | getRedisMonitorInfo() | .../src/api/infra/redis/index.ts |
本机连不上 127.0.0.1:6379 时,先看进程和 application-local.yaml,不要改成别的环境的 Redis。
配置与操作
业务缓存用 @Cacheable,底层是 Redis,不是 Caffeine。本地 LoadingCache 走 Guava CacheUtils。Redis 监控页只看运维,不改 TTL。
开启见 框架层。
