Spring——SpringCache 缓存框架整合 Redis

本贴最后更新于 761 天前,其中的信息可能已经天翻地覆

SpringCache 缓存框架整合 Redis

SpringCache 简介

  • 文档:https://spring.io/guides/gs/caching/
  • 自 Spring 3.1 起,提供了类似于 @Transactional 注解事务的注解 Cache 支持,且提供了 Cache 抽象
  • 提供基本的 Cache 抽象,方便切换各种底层 Cache
  • 只需要更少的代码就可以完成业务数据的缓存
  • 提供事务回滚时也自动回滚缓存,支持比较复杂的缓存逻辑
  • 核心
    • 一个是 Cache 接口,缓存操作的 API;
    • 一个是 CacheManager 管理各类缓存,有多个缓存框架的实现

前提配置

SpringBoot 基础依赖:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.4.5</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
    <java.version>1.8</java.version>
</properties>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <excludes>
                    <exclude>
                        <groupId>org.projectlombok</groupId>
                        <artifactId>lombok</artifactId>
                    </exclude>
                </excludes>
            </configuration>
        </plugin>
    </plugins>
</build>

使用:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
  • 配置文件指定缓存类型
spring:
  cache:
    type: redis
  • 启动类开启缓存注解
@EnableCaching

SpringBoot 整合 MyBatisPlus

依赖

<!--mybatis plus和springboot整合-->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.0</version>
</dependency>

<!--数据库驱动-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.15</version>
</dependency>

配置

spring:
  redis:
    host: ip
    port: 6379
    password: 密码
  cache:
    type: redis
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/rdis6?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: xk857.com
  #配置plus打印sql日志
  mybatis-plus:
    configuration:
      log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

配置 MyBatisPlus 分页

@Configuration
@MapperScan("com.xk857.mapper")
public class MyBatisConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}

创建数据库初始化数据

CREATE TABLE `product` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `title` varchar(128) DEFAULT NULL COMMENT '标题',
  `cover_img` varchar(128) DEFAULT NULL COMMENT '封面图',
  `detail` varchar(256) DEFAULT '' COMMENT '详情',
  `amount` int(10) DEFAULT NULL COMMENT '新价格',
  `stock` int(11) DEFAULT NULL COMMENT '库存',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4;

INSERT INTO `redis6`.`product` (`id`, `title`, `cover_img`, `detail`, `amount`, `stock`, `create_time`) VALUES (1, '李宁短袖男士旗舰官方BADFIVE篮球男装夏季宽松纯棉T恤圆领运动服', 'https://img.alicdn.com/imgextra/https://img.alicdn.com/imgextra/i4/92688455/O1CN01T2SulH2CKRPooOUXV-92688455.jpg_430x430q90.jpg', '//gdp.alicdn.com/imgextra/i4/92688455/O1CN015ptmSW2CKRPvritGg_!!92688455.jpg', 213, 100, '2021-09-12 00:00:00');
INSERT INTO `redis6`.`product` (`id`, `title`, `cover_img`, `detail`, `amount`, `stock`, `create_time`) VALUES (2, '优衣库 男装亲子装(UT)ULTRAMAN印花T恤(奥特英雄系列短袖)438340', 'https://img.alicdn.com/imgextra/i1/196993935/O1CN0110HkIq1ewHBtmX8aX_!!0-item_pic.jpg_430x430q90.jpg', 'https://img.alicdn.com/imgextra/i3/196993935/O1CN01DLn7VI1ewHBorNeqY_!!196993935.jpg', 42, 100, '2021-03-12 00:00:00');
INSERT INTO `redis6`.`product` (`id`, `title`, `cover_img`, `detail`, `amount`, `stock`, `create_time`) VALUES (3, 'Hazzys哈吉斯珠地棉夏季POLO衫男士短袖轻薄凉感T恤男装体恤上衣', 'https://img.alicdn.com/imgextra/https://img.alicdn.com/imgextra/i1/1910887896/O1CN01szBuiA28CPzj8Ine3_!!1910887896.jpg_430x430q90.jpg', 'https://img.alicdn.com/imgextra/https://img.alicdn.com/imgextra/i1/1910887896/O1CN01szBuiA28CPzj8Ine3_!!1910887896.jpg_430x430q90.jpg', 12, 20, '2021-03-22 00:00:00');
INSERT INTO `redis6`.`product` (`id`, `title`, `cover_img`, `detail`, `amount`, `stock`, `create_time`) VALUES (4, '男士短袖t恤2021新款纯棉上衣服体桖潮流半袖潮牌夏装冰丝男装白T', 'https://img.alicdn.com/imgextra/https://img.alicdn.com/imgextra/i3/874506242/O1CN01qYi8Q21vysk0qHzUR_!!874506242.jpg_430x430q90.jpg', 'https://img.alicdn.com/imgextra/https://img.alicdn.com/imgextra/i3/874506242/O1CN01qYi8Q21vysk0qHzUR_!!874506242.jpg_430x430q90.jpg', 14, 20, '2022-11-12 00:00:00');

实体类

@Data
@TableName("product")
public class ProductDO implements Serializable  {

    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    /**
     * 标题
     */
    private String title;

    /**
     * 封面图
     */
    private String coverImg;

    /**
     * 详情
     */
    private String detail;

    /**
     * 新价格
     */
    private Integer amount;

    /**
     * 库存
     */
    private Integer stock;

    /**
     * 创建时间
     */
    private Date createTime;
}

Service

public interface IProductService{

    int save(ProductDO product);

    int delById(int id);

    int updateById(ProductDO product);

    ProductDO findById(int id);
    
    IPage<ProductDO> page(int page, int size);
}

ServiceImpl

@Service
public class ProductServiceImpl implements IProductService {

    @Autowired
    private ProductMapper productMapper;

    @Override
    public int save(ProductDO productDO) {
        return productMapper.insert(productDO);
    }

    @Override
    public int delById(int id) {
        return productMapper.deleteById(id);
    }

    @Override
    public int updateById(ProductDO productDO) {
        return productMapper.updateById(productDO);
    }

    @Override
    public ProductDO findById(int id) {
        return productMapper.selectById(id);
    }
    
    @Override
    public IPage<ProductDO> page(int page, int size) {
        Page<ProductDO> pageInfo = new Page<>(page, size);
        return productMapper.selectPage(pageInfo, null);
    }
}

Controller

@RestController
@RequestMapping("/api/video")
public class ProductController {

    @Autowired
    private IProductService productService;


    @PostMapping("add")
    public JsonData add(@RequestBody ProductDO productDO) {
        productDO.setCreateTime(new Date());
        int save = productService.save(productDO);
        return JsonData.buildSuccess(save);
    }


    @PutMapping("update")
    public JsonData update(@RequestBody ProductDO productDO) {
        productService.updateById(productDO);
        return JsonData.buildSuccess();
    }


    @DeleteMapping("delete/{id}")
    public JsonData delete(@PathVariable int id) {
        productService.delById(id);
        return JsonData.buildSuccess();
    }


    @GetMapping("/{id}")
    public JsonData findById(@PathVariable int id) {
        ProductDO productDO = productService.findById(id);
        return JsonData.buildSuccess(productDO);
    }
    
    
    @GetMapping("/page/{page}/{size}")
    public JsonData findPage(@PathVariable int page,@PathVariable int size) {
        IPage<ProductDO> page1 = productService.page(page, size);
        return JsonData.buildSuccess(page1);
    }
}

SpringCache 框架常用注解

注意:使用前请先在启动类加上 @EnableCaching 注解

@Cacheable

  • 标记在一个方法上,也可以标记在一个类上,一般都是标注在方法上
  • 缓存标注对象的返回结果,标注在方法上缓存该方法的返回值,标注在类上缓存该类所有的方法返回值
  • value 缓存名称,可以有多个
  • key 缓存的 key 规则,可以用 springEL 表达式,默认是方法参数组合
  • condition 缓存条件,使用 springEL 编写,返回 true 才缓存

案例演示:

@Override
@Cacheable(value = {"product"},key = "#root.args[0]") // product::1
public ProductDO findById(int id) {
    return productMapper.selectById(id);
}

@Override
@Cacheable(value = {"product_page"},key = "#root.methodName+'_'+#page+'_'+#size") //product_page::page_1_5
public IPage<ProductDO> page(int page, int size) {
    Page<ProductDO> pageInfo = new Page<>(page, size);
    return productMapper.selectPage(pageInfo, null);
}

Spring 为我们提供了一个 root 对象可以用来生成 key。通过该 root 对象我们可以获取到以下信息,重点关注 methodName 和 args ,这两个可以满足绝大多数使用场景。

属性名称 描述 示例
methodName 当前方法名 #root.methodName
method 当前方法 #root.method.name
target 当前被调用的对象 #root.target
targetClass 当前被调用的对象的 class #root.targetClass
args 当前方法参数组成的数组 #root.args[0]
caches 当前被调用的方法使用的 Cache #root.caches[0].name

思考,发现什么问题没有

  1. 没有对 Redis 进行序列化不方便程序员查看
  2. 没有过期时间永久生效,容易造成内存泄漏
  3. key 的定义在团队合作中可能会出现不规范的情况,每次编写略显繁琐

后面会对问题进行解决。

@CachePut

在支持 Spring Cache 的环境下,对于使用 @Cacheable 标注的方法,Spring 在每次执行前都会检查 Cache 中是否存在相同 key 的缓存元素,如果存在就不再执行该方法,而是直接从缓存中获取结果进行返回,否则才会执行并将返回结果存入指定的缓存中。@CachePut 也可以声明一个方法支持缓存功能。与 @Cacheable 不同的是使用 @CachePut 标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。类似于更新操作

使用案例

@Override
@CachePut(value = {"product"},key = "#productDO.id",cacheManager = "cacheManager1Hour")
public ProductDO updateById(ProductDO productDO) {
    int i = productMapper.updateById(productDO);
    return productDO;
}

注意:我们这里将返回值的 int 改为对象,这样存储到 redis 中的才是对象,才能达到根据 id 查询,一切正常的效果

@CacheEvict

  • CacheEvict
    • 从缓存中移除相应数据, 触发缓存删除的操作
    • value 缓存名称,可以有多个
    • key 缓存的 key 规则,可以用 springEL 表达式,默认是方法参数组合
    • beforeInvocation = false
      • 缓存的清除是否在方法之前执行 ,默认代表缓存清除操作是在方法执行之后执行;
      • 如果出现异常缓存就不会清除
    • beforeInvocation = true
      • 代表清除缓存操作是在方法运行之前执行,无论方法是否出现异常,缓存都清除

使用案例:

@CacheEvict(value = {"product"},key = "#root.args[0]")
public int delById(int id) {
    return productMapper.deleteById(id);
}

@Caching

  • 组合多个 Cache 注解使用
  • 允许在同一方法上使用多个嵌套的 @Cacheable、@CachePut 和 @CacheEvict 注释
@Caching(
    cacheable = {
        @Cacheable(value = "product",keyGenerator = "xdclassKeyGenerator")
    },
    put = {
        @CachePut(value = "product",key = "#id"),
        @CachePut(value = "product",key = "'stock:'+#id")
    }
)

自定义 CacheManager 配置和过期时间

修改 redis 缓存序列化器和配置 manager 过期时间,加入此配置项如果不指定过期时间

@Configuration
public class RedisConfig {


    /**
     * 一小时
     * @param connectionFactory
     * @return
     */
    @Bean
    public RedisCacheManager cacheManager1Hour(RedisConnectionFactory connectionFactory) {
        RedisCacheConfiguration config = instanceConfig(3600L);
        return RedisCacheManager.builder(connectionFactory)
                .cacheDefaults(config)
                .transactionAware()
                .build();
    }

    /**
     * 一天
     * @Primary 代表默认的
     * @param connectionFactory
     * @return
     */
    @Bean
    @Primary
    public RedisCacheManager cacheManager1Day(RedisConnectionFactory connectionFactory) {
        RedisCacheConfiguration config = instanceConfig(3600 * 24L);
        return RedisCacheManager.builder(connectionFactory)
                .cacheDefaults(config)
                .transactionAware()
                .build();
    }

    /**
     * 序列化
     * @param ttl
     * @return
     */
    private RedisCacheConfiguration instanceConfig(Long ttl) {

        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        objectMapper.registerModule(new JavaTimeModule());
        // 去掉各种@JsonSerialize注解的解析
        objectMapper.configure(MapperFeature.USE_ANNOTATIONS, false);
        // 只针对非空的值进行序列化
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        // 将类型序列化到属性json字符串中
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
                ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);

        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        return RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(ttl))
                .disableCachingNullValues() // 禁止缓存null值
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));

    }

}

使用

@Override
@Cacheable(value = {"product"},key = "#root.args[0]",cacheManager = "cacheManager1Hour") // 一小时后过期
public ProductDO findById(int id) {
    return productMapper.selectById(id);
}

@Override
@Cacheable(value = {"product_page"},key = "#root.methodName+'_'+#page+'_'+#size") // 默认一天
public IPage<ProductDO> page(int page, int size) {
    Page<ProductDO> pageInfo = new Page<>(page, size);
    return productMapper.selectPage(pageInfo, null);
}

查询 Redis 发现,数据已经生效

image.png

自定义缓存 KeyGenerator

import org.springframework.cache.interceptor.KeyGenerator;

/**
  * 自定义缓存Key规则
  * @return
*/
@Bean
public KeyGenerator springCacheDefaultKeyGenerator(){
    // 类名_方法名_参数名称
    return new KeyGenerator() {
        @Override
        public Object generate(Object o, Method method, Object... objects) {
            return o.getClass().getSimpleName() + "_"
                + method.getName() + "_"
                + StringUtils.arrayToDelimitedString(objects, "_");
        }
    };
}

使用:

// @Cacheable(value = {"product_page"},key = "#root.methodName+'_'+#page+'_'+#size") // 默认一天
@Cacheable(value = {"product_page"},keyGenerator = "springCacheDefaultKeyGenerator") // 默认一天
public IPage<ProductDO> page(int page, int size) {
    Page<ProductDO> pageInfo = new Page<>(page, size);
    return productMapper.selectPage(pageInfo, null);
}
  • Spring

    Spring 是一个开源框架,是于 2003 年兴起的一个轻量级的 Java 开发框架,由 Rod Johnson 在其著作《Expert One-On-One J2EE Development and Design》中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 JavaEE 应用程序开发提供集成的框架。

    942 引用 • 1459 回帖 • 31 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...