Spring cache整合Redis
参考资料:
参考视频
Mybatis-Plus搭建教程
Spring-cache整合流程
Spring-cache官网
参考demo
Spring cache使用介绍:
简介
Spring cache简化了数据库和缓存的同步过程,在绝大多数项目中都有应用
除redis外,Spring cache还可以整合Caffeine、EhCache等
重要注解介绍
Spring cache重要的注解如下:
注解 | 说明 |
@EnableCaching | 开启缓存注解功能 |
@Cacheable | 在方法执行前先查看缓存中是否有数据, 如果有数据,则直接返回缓存数据; 若没有数据,调用方法并将方法返回值放到缓存中 |
@CachePut | 向缓存中存入数据 |
@CacheEvict | 从缓存中删除数据 |
上述这些注解,key属性支持SPEL语法,用于标记入参或回参中,某个或者某几个属性
SPEL语法
PEL语法以#作为标记的开始
指定入参中的属性
表达式 | 例子 | |
参数名 | #{参数名}.{属性1}.{属性1.1}.{属性1.1.1}....... | #student.id |
参数位置 | #p{index}.{属性1}.{属性1.1}.{属性1.1.1}....... | #p0.id |
参数名(多参数或关系) | #{参数名1}.{属性1}.{属性1.1}_#{参数名2}.{属性2}.{属性2.1}_...... | #student.id_#student.name_#user.id |
参数位置(多参数或关系) | #p{index1}.{属性1}.{属性1.1}_#p{index2}.{属性2}.{属性2.1}_...... | #p0.id_#p0.name_#p1.id |
- 以参数名作为入口
key = "#id"
key = "#student.id"
指的是student中的id元素
key = "#result.body.id"
- 以参数的位置作为入口
通常是#p+参数的index,如:#p0表示第一个参数,#p1表示第二个参数......
key = "#p0"
key = "#p0.id"
第一个元素中的id元素
- 指定多个属性,关系为或
key = "#p0.id+'_'+#p0.name+'_'+#p0.age"
key = "#student.id+'_'+#student.name+'_'+#student.age"
指的是student中的id或name或者age属性
指定回参中的属性
表达式 | 例子 |
#result.{属性1}.{属性1.1}.{属性1.1.1} | #result.body.id |
指的是返回值中的body元素中的id元素
其他常用的属性
allEntries
某个父标签下的所有子标签,如:
@CacheEvict(value = "student", allEntries = true)
删除student下的所有子标签
unless
unless : 表示满足条件则不缓存,如:
@Cacheable(value = "student",key = "#id",unless = "#result == null")
如果返回值是null则不缓存
condition
condition : 表示满足什么条件, 再进行缓存 ,如:
@Cacheable(value = "student",key = "#p0",condition = "#id.length() > 3")
如果返回值的id长度大于3才进行缓存
编写案例:
创建数据库并整合Mybatis-plus
首先创建数据库
CREATE TABLE `student` (`id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,`age` int(3) NULL DEFAULT NULL,PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
并且使用Mybatis或者Mybatis-Plus整合到SpringBoot中,参考网址
进行相关配置
pom中添加如下依赖:
<!-- 只引入所需模块 --><dependency><groupId>cn.hutool</groupId><artifactId>hutool-http</artifactId><version>5.8.12</version></dependency><!-- redis --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>
application.yml中添加如下配置
server:port: 8081
spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/test?characterEncoding=UTF-8username: rootpassword: 123888# Redis 配置redis:host: 192.168.14.53port: 6379password: nulldatabase: 0timeout: 10000 # 超时设置,毫秒jedis:pool:max-active: 10 # 最大连接数max-idle: 5 # 最大空闲连接数min-idle: 1 # 最小空闲连接数# 缓存配置(使用 Redis)cache:type: redis
并且添加相关的序列化、缓存规则
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.*;import java.lang.reflect.Method;
import java.time.Duration;/*** Redis+Cache配置类*/
@Configuration
public class RedisConfig {/*** 自定义key规则* @return*/@Beanpublic KeyGenerator keyGenerator() {return new KeyGenerator() {@Overridepublic Object generate(Object target, Method method, Object... params) {StringBuilder sb = new StringBuilder();sb.append(target.getClass().getName());sb.append(method.getName());for (Object obj : params) {sb.append(obj.toString());}return sb.toString();}};}/*** 设置RedisTemplate规则* @param redisConnectionFactory* @return*/@Beanpublic RedisTemplate<String, Object> redisTemplate(org.springframework.data.redis.connection.RedisConnectionFactory redisConnectionFactory) {RedisTemplate<String, Object> template = new RedisTemplate<>();template.setConnectionFactory(redisConnectionFactory);template.setKeySerializer(new StringRedisSerializer()); // key序列化template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); // value序列化return template;}/*** 设置CacheManager缓存规则* @param factory* @return*/@Beanpublic CacheManager cacheManager(RedisConnectionFactory factory) {RedisSerializer<String> redisSerializer = new StringRedisSerializer();Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);//解决查询缓存转换异常的问题ObjectMapper om = new ObjectMapper();om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jackson2JsonRedisSerializer.setObjectMapper(om);// 配置序列化(解决乱码的问题),过期时间600秒RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(600)).serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer)).serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)).disableCachingNullValues();RedisCacheManager cacheManager = RedisCacheManager.builder(factory).cacheDefaults(config).build();return cacheManager;}}
相关接口的编写
增加接口
@RequestMapping("add")public ResponseEntity<Student> add(@RequestBody Student student) {Student saved = addAndCache(student);return ResponseEntity.ok(saved);}@CachePut(value = "student", key = "#result.id")public Student addAndCache(Student student) {studentService.save(student);return student;}
删除-单个接口
@RequestMapping("deleteById")// 删除单个缓存//@CacheEvict(value = "student",key = "#id")@CacheEvict(value = "student",key = "#p0")public ResponseEntity<String> deleteById(@RequestParam("id")String id){studentService.removeById(id);return ResponseEntity.ok("删除成功");}
删除-全部接口
@RequestMapping("deleteAll")// 删除student下的全部缓存@CacheEvict(value = "student", allEntries = true)public ResponseEntity<String> deleteAll(){studentService.remove(null);return ResponseEntity.ok("删除成功");}
更新接口
@RequestMapping("update")// 删除单个缓存//@CacheEvict(value = "student",key = "#student.id")@CacheEvict(value = "student",key = "#p0.id")public ResponseEntity<Student> update(@RequestBody Student student){studentService.updateById(student);return ResponseEntity.ok(student);}
查询-单个接口
@RequestMapping("getById")@Cacheable(value = "student",key = "#id")//@Cacheable(value = "student",key = "#p0")//@Cacheable(value = "student",key = "#id",unless = "#result == null")//@Cacheable(value = "student",key = "#p0",condition = "#id.length() > 3")public ResponseEntity<Student> getById(@RequestParam("id")String id){Student student = studentService.getById(id);return ResponseEntity.ok(student);}
查询-多个接口
@RequestMapping("getList")//@Cacheable(value = "student",key = "#student.id+'_'+#student.name+'_'+#student.age")@Cacheable(value = "student",key = "#p0.id+'_'+#p0.name+'_'+#p0.age")public ResponseEntity<List<Student>> getList(@RequestBody Student student){LambdaQueryWrapper<Student> wrapper = new LambdaQueryWrapper<>();wrapper.eq(Student::getId,student.getId()).eq(Student::getName,student.getName()).eq(Student::getAge,student.getAge());List<Student> list = studentService.list(wrapper);return ResponseEntity.ok(list);}
总体的代码如下:
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.springcachedemo.entity.Student;
import com.example.springcachedemo.service.StudentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;import java.util.List;@RestController
@Slf4j
public class Controller {@Autowiredprivate StudentService studentService;@RequestMapping("add")public ResponseEntity<Student> add(@RequestBody Student student) {Student saved = addAndCache(student);return ResponseEntity.ok(saved);}@CachePut(value = "student", key = "#result.id")public Student addAndCache(Student student) {studentService.save(student);return student;}@RequestMapping("deleteById")// 删除单个缓存//@CacheEvict(value = "student",key = "#id")@CacheEvict(value = "student",key = "#p0")public ResponseEntity<String> deleteById(@RequestParam("id")String id){studentService.removeById(id);return ResponseEntity.ok("删除成功");}@RequestMapping("deleteAll")// 删除student下的全部缓存@CacheEvict(value = "student", allEntries = true)public ResponseEntity<String> deleteAll(){studentService.remove(null);return ResponseEntity.ok("删除成功");}@RequestMapping("update")// 删除单个缓存//@CacheEvict(value = "student",key = "#student.id")@CacheEvict(value = "student",key = "#p0.id")public ResponseEntity<Student> update(@RequestBody Student student){studentService.updateById(student);return ResponseEntity.ok(student);}@RequestMapping("getById")@Cacheable(value = "student",key = "#id")//@Cacheable(value = "student",key = "#p0")//@Cacheable(value = "student",key = "#id",unless = "#result == null")//@Cacheable(value = "student",key = "#p0",condition = "#id.length() > 3")public ResponseEntity<Student> getById(@RequestParam("id")String id){Student student = studentService.getById(id);return ResponseEntity.ok(student);}@RequestMapping("getList")//@Cacheable(value = "student",key = "#student.id+'_'+#student.name+'_'+#student.age")@Cacheable(value = "student",key = "#p0.id+'_'+#p0.name+'_'+#p0.age")public ResponseEntity<List<Student>> getList(@RequestBody Student student){LambdaQueryWrapper<Student> wrapper = new LambdaQueryWrapper<>();wrapper.eq(Student::getId,student.getId()).eq(Student::getName,student.getName()).eq(Student::getAge,student.getAge());List<Student> list = studentService.list(wrapper);return ResponseEntity.ok(list);}}
Spring-cache整合布隆过滤器
布隆过滤器作为防止缓存穿透或者缓存击穿的主要方案,在很多项目中都有应用
下面是基于Redission在上面的基础上做的布隆过滤器
- pom中添加Redission的依赖
<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.21.3</version></dependency>
- 添加Redision和布隆过滤器相关的配置
import org.redisson.Redisson;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class RedissonConfig {public static final String BLOOM_FILTER_KEY = "myBloom";@Beanpublic RedissonClient redissonClient() {Config config = new Config();config.useSingleServer().setAddress("redis://192.168.14.12:6379").setDatabase(0);return Redisson.create(config);}@Beanpublic RBloomFilter<String> studentBloomFilter(RedissonClient redissonClient) {RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter(BLOOM_FILTER_KEY);bloomFilter.tryInit(1_000_000L, 0.01); // 预估 100w 数据,误判率 1%return bloomFilter;}
}
- 注入布隆过滤器的判断方法
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBloomFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;@Service
@Slf4j
public class StudentBloomService {@Autowiredprivate RBloomFilter<String> studentBloomFilter;/*** 判断 studentId 是否可能存在*/public boolean mightExist(String id) {log.info("bloom filter 判断 id 是否可能存在: {}", id);return studentBloomFilter.contains(id);}
}
- 在相关的接口中添加布隆过滤器
需要注意的是:布隆过滤器的元素,只能增加,不能删除,只能重建
添加接口改为:
@RequestMapping("add")public ResponseEntity<Student> add(@RequestBody Student student) {Student saved = addAndCache(student);// 插入布隆过滤器studentBloomFilter.add(String.valueOf(student.getId()));return ResponseEntity.ok(saved);}@CachePut(value = "student", key = "#result.id")public Student addAndCache(Student student) {studentService.save(student);return student;}
查询接口改为:
@RequestMapping("getById")//@Cacheable(value = "student",key = "#id")//@Cacheable(value = "student",key = "#p0")//@Cacheable(value = "student",key = "#id",unless = "#result == null")//@Cacheable(value = "student",key = "#p0",condition = "#id.length() > 3")@Cacheable(value = "student", key = "#id", condition = "@studentBloomService.mightExist(#id)")public ResponseEntity<Student> getById(@RequestParam("id")String id){Student student = studentService.getById(id);return ResponseEntity.ok(student);}
- 总的代码为:
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.springcachedemo.entity.Student;
import com.example.springcachedemo.service.StudentService;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBloomFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;import java.util.List;@RestController
@Slf4j
@RequestMapping("bloom")
public class BloomController {@Autowiredprivate StudentService studentService;@Autowiredprivate RBloomFilter<String> studentBloomFilter;@RequestMapping("add")public ResponseEntity<Student> add(@RequestBody Student student) {Student saved = addAndCache(student);// 插入布隆过滤器studentBloomFilter.add(String.valueOf(student.getId()));return ResponseEntity.ok(saved);}@CachePut(value = "student", key = "#result.id")public Student addAndCache(Student student) {studentService.save(student);return student;}@RequestMapping("deleteById")// 删除单个缓存//@CacheEvict(value = "student",key = "#id")@CacheEvict(value = "student",key = "#p0")public ResponseEntity<String> deleteById(@RequestParam("id")String id){studentService.removeById(id);// ⚠️ 注意:布隆过滤器 **不能删除**,只能重建!return ResponseEntity.ok("删除成功");}@RequestMapping("deleteAll")// 删除student下的全部缓存@CacheEvict(value = "student", allEntries = true)public ResponseEntity<String> deleteAll(){studentService.remove(null);// ⚠️ 注意:布隆过滤器 **不能删除**,只能重建!return ResponseEntity.ok("删除成功");}@RequestMapping("update")// 删除单个缓存//@CacheEvict(value = "student",key = "#student.id")@CacheEvict(value = "student",key = "#p0.id")public ResponseEntity<Student> update(@RequestBody Student student){studentService.updateById(student);// ⚠️ 注意:布隆过滤器 **不能删除**,只能重建!return ResponseEntity.ok(student);}@RequestMapping("getById")//@Cacheable(value = "student",key = "#id")//@Cacheable(value = "student",key = "#p0")//@Cacheable(value = "student",key = "#id",unless = "#result == null")//@Cacheable(value = "student",key = "#p0",condition = "#id.length() > 3")@Cacheable(value = "student", key = "#id", condition = "@studentBloomService.mightExist(#id)")public ResponseEntity<Student> getById(@RequestParam("id")String id){Student student = studentService.getById(id);return ResponseEntity.ok(student);}@RequestMapping("getList")//@Cacheable(value = "student",key = "#student.id+'_'+#student.name+'_'+#student.age")@Cacheable(value = "student",key = "#p0.id+'_'+#p0.name+'_'+#p0.age")public ResponseEntity<List<Student>> getList(@RequestBody Student student){LambdaQueryWrapper<Student> wrapper = new LambdaQueryWrapper<>();wrapper.eq(Student::getId,student.getId()).eq(Student::getName,student.getName()).eq(Student::getAge,student.getAge());List<Student> list = studentService.list(wrapper);return ResponseEntity.ok(list);}}