使用 Spring Cache + Redis 作为缓存并支持自定义单个key设置过期时长

Spring Cache

Spring针对不同的缓存技术,需要实现不同的cacheManager,
Spring定义了如下的cacheManger实现,具体使用哪种需要你自己实现。
在这里插入图片描述

pom

springboot 项目加入如下依赖


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

配置redis的地址和端口号

redis:
        host: 127.0.0.1
        password :
        port: 6379
        timeout: 60s

配置缓存为redis

需要实现CacheManager用来具体实现缓存管理器

@Configuration
@EnableCaching
public class RedisCacheConfig {


    private int defaultExpireTime=36000;//毫秒

    private int userCacheExpireTime=10000;

    private String userCacheName="cache";

    /**
     * 缓存管理器
     *
     * @param lettuceConnectionFactory
     * @return
     */
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory lettuceConnectionFactory) {
        RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig();
        // 设置缓存管理器管理的缓存的默认过期时间
        defaultCacheConfig = defaultCacheConfig.entryTtl(Duration.ofSeconds(defaultExpireTime))
                // 设置 key为string序列化
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                // 设置value为json序列化
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
                // 不缓存空值
                .disableCachingNullValues();

        Set<String> cacheNames = new HashSet<>();
        cacheNames.add(userCacheName);

        // 对每个缓存空间应用不同的配置
        Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
        configMap.put(userCacheName, defaultCacheConfig.entryTtl(Duration.ofSeconds(userCacheExpireTime)));

        RedisCacheManager cacheManager = RedisCacheManager.builder(lettuceConnectionFactory)
                .cacheDefaults(defaultCacheConfig)
                .initialCacheNames(cacheNames)
                .withInitialCacheConfigurations(configMap)
                .build();
        return cacheManager;
    }

}

业务类启用注解

@Service
@CacheConfig(cacheNames="user")// cacheName 是一定要指定的属性,可以通过 @CacheConfig 声明该类的通用配置
public class UserServiceImpl {

    /**
     * 将结果缓存,当参数相同时,不会执行方法,从缓存中取
     *
     * @param id
     * @return
     */
    @Cacheable(key = "#id")
    public User findUserById(Integer id) {
        System.out.println("===> findUserById(id), id = " + id);
        return new User(id, "taven");
    }

    /**
     * 将结果缓存,并且该方法不管缓存是否存在,每次都会执行
     *
     * @param user
     * @return
     */
    @CachePut(key = "#user.id")
    public User update(User user) {
        System.out.println("===> update(user), user = " + user);
        return user;
    }

    /**
     * 移除缓存,根据指定key
     *
     * @param user
     */
    @CacheEvict(key = "#user.id")
    public void deleteById(User user) {
        System.out.println("===> deleteById(), user = " + user);
    }

    /**
     * 移除当前 cacheName下所有缓存
     *
     */
    @CacheEvict(allEntries = true)
    public void deleteAll() {
        System.out.println("===> deleteAll()");
    }

}

运行即可看到,缓存数据到redis中

Spring Cache 注解

注解作用
@Cacheable将方法的结果缓存起来,下一次方法执行参数相同时,将不执行方法,返回缓存中的结果
@CacheEvict移除指定缓存
@CachePut标记该注解的方法总会执行,根据注解的配置将结果缓存
@Caching可以指定相同类型的多个缓存注解,例如根据不同的条件
@CacheConfig类级别注解,可以设置一些共通的配置,@CacheConfig(cacheNames=“user”), 代表该类下的方法均使用这个cacheNames

Cacheable
@Cacheable使用两个或多个参数作为缓存的key
常见的如分页查询:使用单引号指定分割符,最终会拼接为一个字符串

@Cacheable(key = "#page+':'+#pageSize")
public List<User> findAllUsers(int page,int pageSize) {
    int pageStart = (page-1)*pageSize;
    return userMapper.findAllUsers(pageStart,pageSize);
}

findAllUsers会组成的key为1:10

当然还可以使用单引号自定义字符串作为缓存的key值

@Cacheable(key = "'countUsers'")
public int countUsers() {
    return userMapper.countUsers();
}

在这里插入图片描述

CacheEvict
可以移除指定key
声明 allEntries=true移除该CacheName下所有缓存
声明beforeInvocation=true 在方法执行之前清除缓存,无论方法执行是否成功

//清除所有books下的实体
@CacheEvict(cacheNames="books", allEntries=true) 
public void loadBooks(InputStream batch)

Caching
可以让你在一个方法上嵌套多个相同的Cache 注解(@Cacheable, @CachePut, @CacheEvict),分别指定不同的条件

//清除primary,secondary:deposit的缓存
@Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames="secondary", key="#p0") })
public Book importBooks(String deposit, Date date)

默认 cache key

缓存的本质还是以 key-value 的形式存储的,默认情况下我们不指定key的时候 ,使用 SimpleKeyGenerator 作为key的生成策略

  • 如果没有给出参数,则返回SimpleKey.EMPTY。
  • 如果只给出一个Param,则返回该实例。
  • 如果给出了更多的Param,则返回包含所有参数的SimpleKey。
    注意:当使用默认策略时,我们的参数需要有 有效的hashCode()和equals()方法

实现原理

使用注解切入方法创建代理拦截器,实现调用方法之前之后执行关于redis相关的操作
@EnableCaching 注释触发后置处理器, 检查每一个Spring bean 的 public 方法是否存在缓存注解。如果找到这样的一个注释, 自动创建一个代理拦截方法调用和处理相应的缓存行为。

同步缓存-同步锁缓存

在多线程环境中,可能会出现相同的参数的请求并发调用方法的操作,默认情况下,spring cache 不会锁定任何东西,相同的值可能会被计算几次,这就违背了缓存的目的

对于这些特殊情况,可以使用sync属性。此时只有一个线程在处于计算,而其他线程则被阻塞,直到在缓存中更新条目为止。

@Cacheable(cacheNames="foos", sync=true) 
public Foo executeExpensiveOperation(String id) {...}

条件缓存

  • condition: 什么情况缓存,condition = true 时缓存,反之不缓存
  • unless: 什么情况不缓存,unless = true 时不缓存,反之缓存
//name长度<32时缓存
@Cacheable(cacheNames="book", condition="#name.length() < 32") 
public Book findBook(String name)

//name长度<32时缓存 否则result不为空则缓存result为空则hardback
@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result?.hardback")
public Optional<Book> findBook(String name) 

高级点的东西

以上只是基本应用,我们可以自己定义序列化方式,和缓存key的前缀。多级缓存的实现,如果redis挂掉如何不影响业务正常运行?

CachingConfigurerSupport提供了如下几个方法,它可以让我们通过实现该方法实现更多选择

public class CachingConfigurerSupport implements CachingConfigurer {
    public CachingConfigurerSupport() {
    }

    @Nullable
    public CacheManager cacheManager() {//使用那种形式的缓存-redis还是Ecache
        return null;
    }

    @Nullable
    public CacheResolver cacheResolver() {//如何选择缓存器-多级缓存
        return null;
    }

    @Nullable
    public KeyGenerator keyGenerator() {//key的生成方案
        return null;
    }

    @Nullable
    public CacheErrorHandler errorHandler() {//异常处理
        return null;
    }
}

自定义StringSerializer和自定义缓存key前缀

实现自定义的序列化方式只需要实现redis序列化RedisSerializer接口即可,这里有两个关键方法,一个是序列化,一个是反序列化。同时在这个方法中也可以加入我们自定义的规则,比如统一把缓存放到一个全局的key前缀下面,这样就比较集中的方式实现缓存,避免散乱。

@Component
public class MyStringSerializer implements RedisSerializer<String> {
 
    private final Logger logger = LoggerFactory.getLogger ( this.getClass () );
 
    @Autowired
    private RedisProperties redisProperties;
 
    private final Charset charset;
 
    public MyStringSerializer() {
        this ( Charset.forName ( "UTF8" ) );
    }
 
    public MyStringSerializer(Charset charset) {
        Assert.notNull ( charset, "Charset must not be null!" );
        this.charset = charset;
    }
 
    @Override
    public String deserialize(byte[] bytes) {
        String keyPrefix = redisProperties.getKeyPrefix ();
        String saveKey = new String ( bytes, charset );
        int indexOf = saveKey.indexOf ( keyPrefix );
        if (indexOf > 0) {
            logger.info ( "key缺少前缀" );
        } else {
            saveKey = saveKey.substring ( indexOf );
        }
        logger.info ( "saveKey:{}",saveKey);
        return (saveKey.getBytes () == null ? null : saveKey);
    }
 
    @Override
    public byte[] serialize(String string) {
        String keyPrefix = redisProperties.getKeyPrefix ();
        String key = keyPrefix + string;
        logger.info ( "key:{},getBytes:{}",key, key.getBytes ( charset ));
        return (key == null ? null : key.getBytes ( charset ));
    }
}

上面的序列化中加入了自定义key前缀

在上面的RedisCacheConfig里我们可以修改为自定义的序列化方式

@Autowired
MyStringSerializer  myStringSerializer;


 // 配置序列化(解决乱码的问题)
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                // 7 天缓存过期
                .entryTtl(Duration.ofSeconds(cacheConfigProperties.getTtlTime()))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(myStringSerializer))//自定义的key序列化方式
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))//这里我们也可以修改成FastJson等第三方序列化
                .disableCachingNullValues();

效果如下,这里我的前缀配置的是bamboo,其他和没有配置前缀没有差别,这样所有的注解缓存都会放入该子路径下面,可以做到一键清除所有缓存。
在这里插入图片描述
自定义fastJson请查看下文的参考内容

CacheResolver实现多级缓存

开发者可以通过自定义CacheResolver实现动态选择CacheManager,使用多种缓存机制:优先从堆内存读取缓存,堆内存缓存不存在时再从redis读取缓存,redis缓存不存在时最后从mysql读取数据,并将读取到的数据依次写到redis和堆内存中。

	@Override
    public CacheResolver cacheResolver() {
        // 通过Guava实现的自定义堆内存缓存管理器
        CacheManager guavaCacheManager = new GuavaCacheManager();
        CacheManager redisCacheManager = redisCacheManager();
        List<CacheManager> list = new ArrayList<>();
        // 优先读取堆内存缓存
        list.add(concurrentMapCacheManager);
        // 堆内存缓存读取不到该key时再读取redis缓存
        list.add(redisCacheManager);
        return new CustomCacheResolver(list);
    }

Redis故障或不可用时仍然执行方法服务可用

SimpleCacheErrorHandler直接抛出异常,我们可以重写org.springframework.cache.annotation.CachingConfigurerSupport.errorHandler方法自定义CacheErrorHandler操作缓存异常时异常处理。

重写errorHandler异常处理什么都不做即可,这里只是打印异常信息

@Override
    public CacheErrorHandler errorHandler() {
        CacheErrorHandler cacheErrorHandler = new CacheErrorHandler() {

            @Override
            public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
                RedisErrorException(exception, key);
            }

            @Override
            public void handleCachePutError(RuntimeException exception, Cache cache, Object key, Object value) {
                RedisErrorException(exception, key);
            }

            @Override
            public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) {
                RedisErrorException(exception, key);
            }

            @Override
            public void handleCacheClearError(RuntimeException exception, Cache cache) {
                RedisErrorException(exception, null);
            }
        };
        return cacheErrorHandler;
    }

    protected void RedisErrorException(Exception exception,Object key){
        logger.error("redis异常:key=[{}], exception={}", key, exception.getMessage());
    }

直接关闭redis服务,然后访问接口,看控制台打印的记录如下,抛出异常后接着调用mysql查询出结果

2020-01-10 16:29:29,891 - redis key=bamboo:retailRatio::2,getBytes=[98, 97, 109, 98, 111, 111, 58, 114, 101, 116, 97, 105, 108, 82, 97, 116, 105, 111, 58, 58, 50]
2020-01-10 16:29:31,895 - redis异常:key=[2], exception=Unable to connect to Redis; nested exception is io.lettuce.core.RedisConnectionException: Unable to connect to localhost:6379
2020-01-10 16:29:31,895 - ==>  Preparing: select id, retail_role, unratio, ratio from j_retail_ratio where id = ? 
2020-01-10 16:29:31,895 - ==> Parameters: 2(Integer)
2020-01-10 16:29:31,897 - <==      Total: 1

推荐第三方maven

以上封装觉得麻烦可以直接使用中央仓库中我已经封装好的的版本

1.pom依赖


<dependency>
  <groupId>com.github.bamboo-cn</groupId>
  <artifactId>jt-common-core</artifactId>
  <version>1.0.3</version>
</dependency>

2.Java启动类配置和业务层数据注解配置

//Java启动类配置
@SpringBootApplication(scanBasePackages = {"com.bamboo.common"})

//serviceImp需要使用注解
serviceImp类中的启用spring cache注解,用法同UserServiceImpl

3.yml配置自定义缓存配置-可以不用配置使用默认值

spring:
    cache: #缓存配置
        prefix: bamboo #key前缀
        ttlTime: 2000  #缓存时长单位秒

cache配置默认值

  • prefix: spring-cache
  • ttlTime: 3600 一个小时
  • 使用方式和spring cache注解相同
  • 使用fastjson作为序列化

4.在1.0.3版本中已经支持单个key设置过期时长,从而避免使用默认过期时间

自定义单个key设置超时时间,key加上字符串TTL=1000实现扩展单个KEY过期时间

    //@Cacheable(key = "T(String).valueOf(#code).concat('TTL=10')")
    @Cacheable(key = "#code+'TTL=10'")
    public Double getRetailByCode(Integer code) {
        RetailRatio retailRatio =  retailRatioMapper.selectByPrimaryKey(code);
        return retailRatio.getUnratio();
    }

参考资料

redis缓存实现
https://www.jianshu.com/p/931484bb3fdc

自定义序列化和key前缀实现方式
https://www.jianshu.com/p/713069fbd889
https://blog.csdn.net/u012129558/article/details/80520693
自定义fastJson作为redis序列化方式
https://blog.csdn.net/b376924098/article/details/79820642

  • 4
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值