跳至主要內容

Spring Boot Cache With Redis

cpgege原创大约 3 分钟随笔Spring BootCache

本篇文章将介绍如何使用 Redis 作为数据源,实现 Spring Boot 的方法缓存。

引入相关依赖

要实现方法缓存,首先需要引入 spring-boot-starter-cachespring-boot-starter-data-redis 这两个依赖。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

编写自动配置类

编写一个自动配置类,以注入默认的 RedisCacheConfiguration 和 自定义的配置 RedisCacheManagerBuilderCustomizer,并通过 @EnableCaching 启用 Spring Cache。

@Configuration
@ConditionalOnProperty(name = "spring.cache.type", havingValue = "redis")
@EnableConfigurationProperties({SpringCacheProperties.class})
public class SpringCacheConfiguration {

    @Bean
    public RedisCacheConfiguration cacheConfiguration() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
                ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);

        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer =
                new Jackson2JsonRedisSerializer<>(mapper, Object.class);

        return RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(SpringCacheProperties.DEFAULT_TTL_SECONDS))
                // 禁止缓存 null value
                .disableCachingNullValues()
                // 禁用前缀
                .disableKeyPrefix()
                .serializeValuesWith(
                        RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));
    }

    @Bean
    public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer(
            SpringCacheProperties springCacheProperties) {
        return (builder) -> builder
                .withInitialCacheConfigurations(Optional.ofNullable(springCacheProperties.getEntries())
                        .orElse(Collections.emptyList()).stream()
                        .collect(Collectors.toMap(SpringCacheProperties.Entry::getCacheName,
                                entry -> cacheConfiguration().entryTtl(Duration.ofSeconds(entry.getTtlSeconds())))));
    }
}
@Getter
@Setter
@ConfigurationProperties("spring-cache")
public class SpringCacheProperties {

    public static final Long DEFAULT_TTL_SECONDS = 60L;

    private List<Entry> entries;

    @Getter
    @Setter
    public static class Entry {

        private String cacheName;

        private Long ttlSeconds;
    }
}
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class CacheNameConstant {

    /**
     * 较短时间(几秒)
     */
    public static final String SHORT_SECONDS = "SHORT_SECONDS";

    /**
     * 短时间(几分钟)
     */
    public static final String SHORT_MINUTES = "SHORT_MINUTES";

    /**
     * 中等时间(几十分钟到及小时)
     */
    public static final String SEVERAL_HOURS = "SEVERAL_HOURS";

    /**
     * 长时间(几天到一个月)
     */
    public static final String SEVERAL_DAYS = "SEVERAL_DAYS";

    /**
     * 超长时间(几个月)
     */
    public static final String EXTRA_LONG_MONTHS = "EXTRA_LONG_MONTHS";

    /**
     * 永不过期
     */
    public static final String NO_EXPIRY = "NO_EXPIRY";
}

配置文件指定 Spring Cache 的类型

spring:
  cache:
    type: redis
spring-cache:
  entries:
    # 较短时间(几秒)
    - cache-name: SHORT_SECONDS
      ttl-seconds: 1
      # 短时间(几分钟)
    - cache-name: SHORT_MINUTES
      ttl-seconds: 60
      # 中等时间(几十分钟到及小时)
    - cache-name: SEVERAL_HOURS
      ttl-seconds: 3600
      # 长时间(几天到一个月)
    - cache-name: SEVERAL_DAYS
      ttl-seconds: 86400
      # 超长时间(几个月)
    - cache-name: EXTRA_LONG_MONTHS
      ttl-seconds: 2592000
      # 永不过期
    - cache-name: NO_EXPIRY
      ttl-seconds: 0

使用

@Cacheable(value = CacheNameConstant.SEVERAL_DAYS,
        key = "T(com.cpgege.constant.RedisKeyConstant).getRolePermissionsKey(#roleId)",
        unless = "#result == null")
public List<SysPermissionDat> rolePermissions(Long roleId) {
    return getBaseMapper().selectByRoleId(roleId);
}

@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class)
@CacheEvict(value = CacheNameConstant.SEVERAL_DAYS,
        key = "T(com.cpgege.constant.RedisKeyConstant).getRolePermissionsKey(#updateRoleDTO.id)")
public void updateRole(UpdateRoleDTO updateRoleDTO) {
    // 查询角色
    SysRoleDat role = getById(updateRoleDTO.getId());
    Assert.notNull(role, "角色ID[{}]对应的角色不存在", updateRoleDTO.getId());

    // 更新前校验
    checkBeforeUpdate(role, updateRoleDTO);

    SysRoleDat sysRoleDat = updateRoleDTO.toBean(SysRoleDat.class);

    if (DataPermissionEnum.SPECIFIED_DEPARTMENT.getCode().equals(updateRoleDTO.getDataPermission())) {
        sysRoleDat.setSpecifiedDeptIds(JSON.toJSONString(updateRoleDTO.getSpecifiedDeptIdList()));
    }

    updateById(sysRoleDat);

    // permissionIds 为 null,则返回
    if (updateRoleDTO.getPermissionIds() == null) {
        return;
    }

    // 解绑角色下的权限
    LambdaQueryWrapper<SysRolePermissionDat> wrapper = Wrappers.lambdaQuery();
    sysRolePermissionDatService.remove(wrapper.eq(SysRolePermissionDat::getRoleId, updateRoleDTO.getId()));

    // 保存角色与权限关联关系
    batchSaveRolePermission(updateRoleDTO.getId(), updateRoleDTO.getPermissionIds(),
            updateRoleDTO.getClientId(), updateRoleDTO.getCreateBy());
}

@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class)
@CacheEvict(value = CacheNameConstant.SEVERAL_DAYS,
        key = "T(com.cpgege.constant.RedisKeyConstant).getRolePermissionsKey(#roleId)")
public void removeRole(Long roleId) {
    // 查询角色是否关联成员
    LambdaQueryWrapper<AccountRoleDat> wrapper = Wrappers.lambdaQuery();
    List<AccountRoleDat> accountRoles = accountRoleDatMapper.selectList(
            wrapper.eq(AccountRoleDat::getRoleId, roleId));
    Assert.isTrue(CollectionUtils.isEmpty(accountRoles), "请先移除角色成员");

    // 删除角色下的所有权限
    LambdaQueryWrapper<SysRolePermissionDat> rolePermissionQueryWrapper = Wrappers.lambdaQuery();
    sysRolePermissionDatService.remove(rolePermissionQueryWrapper.eq(SysRolePermissionDat::getRoleId, roleId));

    // 删除角色
    removeById(roleId);
}

说明

org.springframework.data.redis.cache.RedisCache#put

@Override
public void put(Object key, @Nullable Object value) {

  Object cacheValue = preProcessCacheValue(value);

  if (!isAllowNullValues() && cacheValue == null) {

    throw new IllegalArgumentException(String.format(
        "Cache '%s' does not allow 'null' values; Avoid storing null via '@Cacheable(unless=\"#result == null\")' or configure RedisCache to allow 'null' via RedisCacheConfiguration",
        name));
  }

  cacheWriter.put(name, createAndConvertCacheKey(key), serializeCacheValue(cacheValue), cacheConfig.getTtl());
}

如果禁止缓存 null value,且 @Cacheable 未配置 unless="#result == null",则抛出如下异常:

java.lang.IllegalArgumentException: Cache 'SEVERAL_HOURS' does not allow 'null' values; Avoid storing null via '@Cacheable(unless="#result == null")' or configure RedisCache to allow 'null' via RedisCacheConfiguration

如果未禁止缓存 null value,且未配置 unless="#result == null",当 result = null 时,会缓存 null 值,缓存的 value 为如下:

��sr+org.springframework.cache.support.NullValuexp

如果未禁止缓存 null value,且 @Cacheable 配置了 unless="#result == null",当 result = null 时,不会缓存 null 值。

上次编辑于: