Spring Cache的坑入坑出

Spring Cache的坑入坑出

最近的项目用到了Spring Cache做Redis的序列化配置,做完业务后整理一份学习记录供大家参考

写在最前面

缓存是一种在实际的开发中经常使用的提高性能的方法,提到缓存第一个想到的当然就是Redis了。在spring中,有一种基于redis序列化的cache配置。本文将介绍,如何在少的配置下可以给既有代码提供缓存能力。

一、Spring Cache的简单介绍

Spring 3.1引入了基于注解的缓存技术,它本质上不是一个具体的缓存实现方案(比如EHCache或OSCache),而是一个对缓存使用的抽象。通过在既有代码中加入少量的注解和配置,既能够实现缓存,提供缓存能力。

Spring的缓存技术还具备相当的灵活性。不仅能够用SpEL(Spring Expression Language)来定义缓存的key和各种condition,还提供开箱即用的缓存暂时存储方案,也支持主流的专业缓存比如EHCache集成。

其特点总结例如下:

  • 通过少量的配置 annotation 凝视就可以使得既有代码支持缓存
  • 支持开箱即用 Out-Of-The-Box,即不用安装和部署额外第三方组件就可以使用缓存
  • 支持 Spring Express Language,能使用对象的不论什么属性或者方法来定义缓存的 key 和 condition
  • 支持 AspectJ,并通过事实上现不论什么方法的缓存支持
  • 支持自己定义 key 和自己定义缓存管理者,具有相当的灵活性和扩展性

下面,通过一步步实现的过程,逐步介绍其内容。

二、开发测试环境和代码结构

工具:IDEA

项目类型:MAVEN项目

JDK版本:1.8

代码结构:
在这里插入图片描述
结构简单,就是基本的springboot+mysql+mybatis,依次说明一下:

config包是用来存放配置代码,比如Redis的配置,还有一些自定义的configuration。

controller包存放控制器(很基础,主要写给需要的萌新)。

entity存放实体类,也就是常说的POJO。

贴一下代码作参考:

package com.guanzhi.entity;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;

/**
 * @author guan.zhi
 * @date 2021/3/8
 */
@TableName("user")
public class User {
    @TableId
    private Long id;
    @TableField("name")
    private String name;
    @TableField("pwd")
    private String pwd;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPwd() {
        return pwd;
    }

    public void setPwd(String pwd) {
        this.pwd = pwd;
    }
}

mapper放mybatis的映射mapper类,用来和对应的xml做映射,关联SQL。

service是我们的业务层的代码,主要是放业务实现代码,service包下放接口,impl下放实现类。DTO顾名思义是数据传输对象,用来接收前端传递的参数,放在对象中做处理。

toolkit存放工具类,本文中主要使用到了统一API返回接口,做接口返回处理。

maven就不多说了吧,就是基本的springboot、mysql驱动、mybatis等依赖,不做赘述。需要了解的小伙伴可以参考这个:

https://blog.csdn.net/S_L__/article/details/104197511

和我不一样的是,他直接建springboot项目,我是以maven项目为基础的。

三、Cache实现的关键步骤

1.CacheConfiguration(自定义缓存读写机制)

CacheConfiguration的主要作用是继承CachingConfigurerSupport来进行部分配置,本文中主要重写了keyGenerator,用来生成自定义的key。还有errorHandler,缓存仅仅是为了业务更快地查询而存在的,如果因为缓存操作失败导致正常的业务流程失败,有点得不偿失了。因此需要开发者自定义CacheErrorHandler处理缓存读写的异常,当redis异常时,打印日志但程序继续运行。其次,使用redis进行缓存需要配置其为序列化方式,以此替换默认的jdk序列化。关于springcache的序列化方式参考这个博客:

https://blog.csdn.net/wabiaozia/article/details/107134081

我测试的代码如下:

package com.guanzhi.config;

import ...//篇幅原因省略

/**
 * @author guan.zhi
 * @date 2021/3/8
 */
@EnableCaching
@Configuration(proxyBeanMethods = false)
public class CacheConfiguration extends CachingConfigurerSupport {

    public static final Logger log = LoggerFactory.getLogger(CacheConfiguration.class);

    private final static String CLASS = "class";
    private final static String METHOD_NAME = "methodName";
    private final static String PACKAGE = "package";

    @Bean
    public RedisCacheConfiguration redisCacheConfiguration() {
        GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();
        RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig();
        configuration = configuration.serializeValuesWith(RedisSerializationContext
                .SerializationPair
                .fromSerializer(serializer))
                .entryTtl(Duration.ofHours(2));
        return configuration;
    }

    @Override
    public KeyGenerator keyGenerator() {
        return (target, method, params) -> {
            Map<String, Object> container = new HashMap<>(3);
            Class<?> targetClassClass = target.getClass();

            container.put(CLASS, targetClassClass.toGenericString());
            container.put(METHOD_NAME, method.getName());
            container.put(PACKAGE, targetClassClass.getPackage());

            for (int i = 0; i < params.length; i++) {
                container.put(String.valueOf(i),params[i]);
            }

            ObjectMapper objectMapper = new ObjectMapper();
            String jsonString = null;
            try {
                jsonString = objectMapper.writeValueAsString(container);
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }

            return DigestUtils.sha256Hex(jsonString);
        };
    }

    @Override
    public CacheErrorHandler errorHandler() {
        // 异常处理,当Redis发生异常时,打印日志,但是程序正常走
        log.info("初始化 -> [{}]", "Redis CacheErrorHandler");
        return new CacheErrorHandler() {
            @Override
            public void handleCacheGetError(RuntimeException e, Cache cache, Object key) {
                log.error("Redis occur handleCacheGetError:key -> [{}]", key, e);
            }

            @Override
            public void handleCachePutError(RuntimeException e, Cache cache, Object key, @Nullable Object value) {
                log.error("Redis occur handleCachePutError:key -> [{}];value -> [{}]", key, value, e);
            }

            @Override
            public void handleCacheEvictError(RuntimeException e, Cache cache, Object key) {
                log.error("Redis occur handleCacheEvictError:key -> [{}]", key, e);
            }

            @Override
            public void handleCacheClearError(RuntimeException e, Cache cache) {
                log.error("Redis occur handleCacheClearError:", e);
            }
        };
    }
}

2.yml配置

#只展示重要部分
spring:
  application:
    name: demo
  datasource:
    type: com.mysql.cj.jdbc.MysqlDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=Asia/Shanghai&characterEncoding=utf8&
    useSSL=false
    username: root
    password: 123456
  cache:
    type: redis
    redis:
      cache-null-values: false
      time-to-live: 3600000ms
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
  redis:
    host: localhost

这样SpringCache将会自动配置redis,注入相关bean,我们就可以在代码中使用注解了。

3.代码注解

springcache的注解主要有四种:@Cacheable@CacheEvict@CachePut

我在开发的过程中常用的是前两个,以此介绍一下主角们:

@Cacheable

注解在方法上,标识方法可以进行缓存,可以将数据返回进行缓存。当我们第一次访问的时候,会先把查出来的数据根据key缓存到redis中,当再次发起访问请求的时候,会先去缓存中查看时候有相同的key,如果有就调用缓存。它有这几个参数:

名称解释例子
value缓存的名称,在 spring 配置文件中定义,必须指定至少一个例如:
@Cacheable(value=”mycache”)
@Cacheable(value={”cache1”,”cache2”}
key缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合@Cacheable(value=”testcache”,key=”#userName”)
condition缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存@Cacheable(value=”testcache”,condition=”#userName.length()>2”)

我的测试代码实例:

    @Cacheable(value = "user",  key = "'list:' + #queryCondition.current", unless = "#result == null")
    public Page<User> getList(QueryCondition queryCondition) {
        Page page = new Page(queryCondition.getCurrent(), queryCondition.getSize());
        Page<User> userPage = userMapper.getList(page);
        return userPage;
    }

在这个方法中,首先用@Cacheable注解,写了三个参数,value、key和缓存的条件设置。

@CachePut

这个注解的作用就其实有点类似于上面的,只是他的作用相当于我们数据库中的update的作用,加上这个注解后,我们每次访问数据都不会从缓存中拿取数据,而是直接通过去数据库查询并将数据缓存到redis中。可能有人会问,这样有什么作用呢?其实如果你是结合到cachable就有作用了,因为我们更新后数据是不是变化了,所以我们就需要将之前的数据的给清空掉,否则的话就会产生脏数据。
同样他也有几个参数需要填写,其实就和cachable一样的。

@CacheEvict

我们上面的put的注解说了他是用于更新的,那么如果我们增加了数据或者删除了数据怎么办,比如通过用户id来查询用户所购买的所有东西的数据,如果用户又买了新东西或者删除了某个物品的信息,那么我们怎么办,理论上我们是可以使用上面的注解,可是我们删除和增加其实没有必要去查询新数据。所以我们这个CacheEvict注解的作用就出现了。他的作用其实就是帮我们清空指定的缓存,他可以清空value,也可以清空value.key的数据。这样的话我们可以针对性的去处理数据。主要参数有:

参数解释example
value缓存的名称,在 spring 配置文件中定义,必须指定至少一个@CacheEvict(value=”my cache”)
key缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合@CacheEvict(value=”testcache”,key=”#userName”)
condition缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存@CacheEvict(value=”testcache”,condition=”#userName.length()>2”)
allEntries是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存@CachEvict(value=”testcache”,allEntries=true)
beforeInvocation是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存@CachEvict(value=”testcache”,beforeInvocation=true)

我的测试代码实例:

    @CacheEvict(value = "user", allEntries = true)
    public int insertSelective(User record) {
        return userMapper.insert(record);
    }

上面的代码作用插入新的用户信息,但是插入了数据以后导致缓存中的部分数据失效,所以需要清除所有缓存。

四、一些坑

  1. 配置一定要写好,在yml中cache:redis一定要写出来,其次redis的基本配置也要写好,不然注解会报错。

  2. 注解中的value参数,尽可能写上,避免因为key的读取问题不能存或者读缓存。

  3. 上述的一些代码写的不是很严谨,只是参考和测试使用,在实际的开发中一定要对key的设置,再细节一点。比如,如果是查询方法,需要把当前这个Query的key唯一标识出来,主要的目的是方便更新数据后@CacheEvict来准确的清理所有脏数据。

  4. 最开始做的时候,没有注意@Cacheable和@CachePut的区别,字面意思以为Put是存数据,默认会读取缓存,读了文档和源码后,才发现其实不然。所以希望大家在做开发的时候,一定要知其然也知其所以然,这样才能写出健壮的代码,保证系统整体的稳定性。

    如果卷不可避免,那也要让自己时刻保持清醒。加油打工人们~

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值