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 |
思考,发现什么问题没有
- 没有对 Redis 进行序列化不方便程序员查看
- 没有过期时间永久生效,容易造成内存泄漏
- 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 发现,数据已经生效
自定义缓存 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);
}
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于