参考资料:

参考视频

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中添加如下依赖:

        
        
            cn.hutool
            hutool-http
            5.8.12
        
        
        
            org.springframework.boot
            spring-boot-starter-data-redis
        

application.yml中添加如下配置

server:
  port: 8081
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/test?characterEncoding=UTF-8
    username: root
    password: 123888
  # Redis 配置
  redis:
    host: 192.168.14.53
    port: 6379
    password: null
    database: 0
    timeout: 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
     */
    @Bean
    public KeyGenerator keyGenerator() {
        return new KeyGenerator() {
            @Override
            public 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
     */
    @Bean
    public RedisTemplate redisTemplate(org.springframework.data.redis.connection.RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        template.setKeySerializer(new StringRedisSerializer()); // key序列化
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); // value序列化
        return template;
    }
    /**
     * 设置CacheManager缓存规则
     * @param factory
     * @return
     */
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisSerializer 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 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 deleteById(@RequestParam("id")String  id){
        studentService.removeById(id);
        return ResponseEntity.ok("删除成功");
    }

        删除-全部接口

    @RequestMapping("deleteAll")
    //  删除student下的全部缓存
    @CacheEvict(value = "student", allEntries = true)
    public ResponseEntity deleteAll(){
        studentService.remove(null);
        return ResponseEntity.ok("删除成功");
    }

        更新接口

    @RequestMapping("update")
    //  删除单个缓存
    //@CacheEvict(value = "student",key = "#student.id")
    @CacheEvict(value = "student",key = "#p0.id")
    public ResponseEntity 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 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> getList(@RequestBody Student student){
        LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(Student::getId,student.getId())
                .eq(Student::getName,student.getName())
                .eq(Student::getAge,student.getAge());
        List 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 {
    @Autowired
    private StudentService studentService;
    @RequestMapping("add")
    public ResponseEntity 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 deleteById(@RequestParam("id")String  id){
        studentService.removeById(id);
        return ResponseEntity.ok("删除成功");
    }
    @RequestMapping("deleteAll")
    //  删除student下的全部缓存
    @CacheEvict(value = "student", allEntries = true)
    public ResponseEntity deleteAll(){
        studentService.remove(null);
        return ResponseEntity.ok("删除成功");
    }
    @RequestMapping("update")
    //  删除单个缓存
    //@CacheEvict(value = "student",key = "#student.id")
    @CacheEvict(value = "student",key = "#p0.id")
    public ResponseEntity 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 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> getList(@RequestBody Student student){
        LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(Student::getId,student.getId())
                .eq(Student::getName,student.getName())
                .eq(Student::getAge,student.getAge());
        List list = studentService.list(wrapper);
        return ResponseEntity.ok(list);
    }
}

Spring-cache整合布隆过滤器

        布隆过滤器作为防止缓存穿透或者缓存击穿的主要方案,在很多项目中都有应用

        下面是基于Redission在上面的基础上做的布隆过滤器

  • pom中添加Redission的依赖
        
            org.redisson
            redisson
            3.21.3
        

  • 添加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";
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer()
                .setAddress("redis://192.168.14.12:6379")
                .setDatabase(0);
        return Redisson.create(config);
    }
    @Bean
    public RBloomFilter studentBloomFilter(RedissonClient redissonClient) {
        RBloomFilter 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 {
    @Autowired
    private RBloomFilter studentBloomFilter;
    /**
     * 判断 studentId 是否可能存在
     */
    public boolean mightExist(String id) {
        log.info("bloom filter 判断 id 是否可能存在: {}", id);
        return studentBloomFilter.contains(id);
    }
}
  • 在相关的接口中添加布隆过滤器

        需要注意的是:布隆过滤器的元素,只能增加,不能删除,只能重建

        添加接口改为:

    @RequestMapping("add")
    public ResponseEntity 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 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 {
    @Autowired
    private StudentService studentService;
    @Autowired
    private RBloomFilter studentBloomFilter;
    @RequestMapping("add")
    public ResponseEntity 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 deleteById(@RequestParam("id")String  id){
        studentService.removeById(id);
        // ⚠️ 注意:布隆过滤器 **不能删除**,只能重建!
        return ResponseEntity.ok("删除成功");
    }
    @RequestMapping("deleteAll")
    //  删除student下的全部缓存
    @CacheEvict(value = "student", allEntries = true)
    public ResponseEntity deleteAll(){
        studentService.remove(null);
        // ⚠️ 注意:布隆过滤器 **不能删除**,只能重建!
        return ResponseEntity.ok("删除成功");
    }
    @RequestMapping("update")
    //  删除单个缓存
    //@CacheEvict(value = "student",key = "#student.id")
    @CacheEvict(value = "student",key = "#p0.id")
    public ResponseEntity 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 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> getList(@RequestBody Student student){
        LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(Student::getId,student.getId())
                .eq(Student::getName,student.getName())
                .eq(Student::getAge,student.getAge());
        List list = studentService.list(wrapper);
        return ResponseEntity.ok(list);
    }
}