【SpringBoot高级篇】SpringBoot集成cache(LinkedHashMap)本地缓存

Spring Boot与缓存

JSR107

Java Caching定义了5个核心接口,分别是CachingProvider, CacheManager, Cache, Entry Expiry

  • CachingProvider定义了创建、配置、获取、管理和控制多个CacheManager。一个应用可 以在运行期访问多个CachingProvider。
  • CacheManager定义了创建、配置、获取、管理和控制多个唯一命名的Cache,这些Cache 存在于CacheManager的上下文中。一个CacheManager仅被一个CachingProvider所拥有。
  • Cache是一个类似Map的数据结构并临时存储以Key为索引的值。一个Cache仅被一个 CacheManager所拥有。
  • Entry是一个存储在Cache中的key-value对。
  • Expiry 每一个存储在Cache中的条目有一个定义的有效期。一旦超过这个时间,条目为过期 的状态。一旦过期,条目将不可访问、更新和删除。缓存有效期可以通过ExpiryPolicy设置。

Spring缓存抽象

Spring从3.1开始定义了org.springframework.cache.Cache 和org.springframework.cache.CacheManager接口来统一不同的缓存技术; 并支持使用JCache(JSR-107)注解简化我们开发;

  • Cache接口为缓存的组件规范定义,包含缓存的各种操作集合;
  • Cache接口下Spring提供了各种xxxCache的实现;如RedisCache,EhCacheCache , ConcurrentMapCache等;
  • 每次调用需要缓存功能的方法时,Spring会检查检查指定参数的指定的目标方法是否 已经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。
  • 使用Spring缓存抽象时我们需要关注以下两点;
    1. 确定方法需要被缓存以及他们的缓存策略
    2. 从缓存中读取之前缓存存储的数据

重要概念&缓存注解

@EnableCaching

开启缓存功能,一般放在启动类上。

@CacheConfig

当我们需要缓存的地方越来越多,你可以使用@CacheConfig(cacheNames = {"cacheName"})注解在 class 之上来统一指定value的值,这时可省略value,如果你在你的方法依旧写上了value,那么依然以方法的value值为准。

@Cacheable

根据方法对其返回结果进行缓存,下次请求时,如果缓存存在,则直接读取缓存数据返回;如果缓存不存在,则执行方法,并把返回的结果存入缓存中。一般用在查询方法上。
查看源码,属性值如下:
在这里插入图片描述

属性/方法名解释
value缓存名,必填,它指定了你的缓存存放在哪块命名空间
cacheNames与 value 差不多,二选一即可
key可选属性,可以使用 SpEL 标签自定义缓存的key
keyGeneratorkey的生成器。key/keyGenerator二选一使用
cacheManager指定缓存管理器
cacheResolver指定获取解析器
condition条件符合则缓存
unless条件符合则不缓存
sync是否使用异步模式,默认为false

@CachePut

使用该注解标志的方法,每次都会执行,并将结果存入指定的缓存中。其他方法可以直接从响应的缓存中读取缓存数据,而不需要再去查询数据库。一般用在新增方法上。
查看源码,属性值如下:
在这里插入图片描述

属性/方法名解释
value缓存名,必填,它指定了你的缓存存放在哪块命名空间
cacheNames与 value 差不多,二选一即可
key可选属性,可以使用 SpEL 标签自定义缓存的key
keyGeneratorkey的生成器。key/keyGenerator二选一使用
cacheManager指定缓存管理器
cacheResolver指定获取解析器
condition条件符合则缓存
unless条件符合则不缓存

@CacheEvict

使用该注解标志的方法,会清空指定的缓存。一般用在更新或者删除方法上
查看源码,属性值如下:
在这里插入图片描述

属性/方法名解释
value缓存名,必填,它指定了你的缓存存放在哪块命名空间
cacheNames与 value 差不多,二选一即可
key可选属性,可以使用 SpEL 标签自定义缓存的key
keyGeneratorkey的生成器。key/keyGenerator二选一使用
cacheManager指定缓存管理器
cacheResolver指定获取解析器
condition条件符合则缓存
allEntries是否清空所有缓存,默认为 false。如果指定为 true,则方法调用后将立即清空所有的缓存
beforeInvocation是否在方法执行前就清空,默认为 false。如果指定为 true,则在方法执行前就会清空缓存

@Caching

该注解可以实现同一个方法上同时使用多种注解。可从其源码看出
在这里插入图片描述

@Caching(
            cacheable = {
                    @Cacheable(value="emp",key = "#lastName")
            },
            put = {
                    @CachePut(value="emp",key = "#result.id"),
                    @CachePut(value="emp",key = "#result.email")
            }
    )
    public Employee getEmpByLastName(String lastName) {
   		 return employeeMapper.getEmpByLastName(lastName);
    }

Spring 支持的常用 CacheManager 如下:

CacheManager描述
SimpleCacheManager使用简单的 Collection 来存储缓存
ConcurrentMapCacheManager使用 java.util.ConcurrentHashMap 来实现缓存
NoOpCacheManager仅测试用,不会实际存储缓存
EhCacheCacheManger使用EhCache作为缓存技术。EhCache 是一个纯 Java 的进程内缓存框架,特点快速、精干,是 Hibernate 中默认的 CacheProvider,也是 Java 领域应用最为广泛的缓存
JCacheCacheManager支持JCache(JSR-107)标准的实现作为缓存技术
CaffeineCacheManager使用 Caffeine 作为缓存技术。用于取代 Guava 缓存技术。
RedisCacheManager使用Redis作为缓存技术
HazelcastCacheManager使用Hazelcast作为缓存技术
CompositeCacheManager用于组合 CacheManager,可以从多个 CacheManager 中轮询得到相应的缓存

本地缓存

为什么要用本地缓存

  • 本地缓存基于本地环境的内存,访问速度非常快,对于一些变更频率低、实时性要求低的数据,可以放在本地缓存中,提升访问速度
  • 使用本地缓存能够减少和Redis类的远程缓存间的数据交互,减少网络I/O开销,降低这一过程中在网络通信上的耗时

本地缓存方案选型

1、 使用ConcurrentHashMap实现本地缓存
缓存的本质就是存储在内存中的KV数据结构,对应的就是jdk中线程安全的ConcurrentHashMap,但是要实现缓存,还需要考虑淘汰、最大限制、缓存过期时间淘汰等等功能;

优点是实现简单,不需要引入第三方包,比较适合一些简单的业务场景。缺点是如果需要更多的特性,需要定制化开发,成本会比较高,并且稳定性和可靠性也难以保障。对于比较复杂的场景,建议使用比较稳定的开源工具。

2、基于Guava Cache实现本地缓存
Guava是Google团队开源的一款 Java 核心增强库,包含集合、并发原语、缓存、IO、反射等工具箱,性能和稳定性上都有保障,应用十分广泛。Guava Cache支持很多特性:

  • 支持最大容量限制
  • 支持两种过期删除策略(插入时间和访问时间)
  • 支持简单的统计功能
  • 基于LRU算法实现

3、Caffeine
Caffeine是基于java8实现的新一代缓存工具,缓存性能接近理论最优。可以看作是Guava Cache的增强版,功能上两者类似,不同的是Caffeine采用了一种结合LRU、LFU优点的算法:W-TinyLFU,在性能上有明显的优越性

4、 Ehcache
Ehcache是一个纯Java的进程内缓存框架,具有快速、精干等特点,是Hibernate中默认的CacheProvider。同Caffeine和Guava Cache相比,Ehcache的功能更加丰富,扩展性更强:

  • 支持多种缓存淘汰算法,包括LRU、LFU和FIFO
  • 缓存支持堆内存储、堆外存储、磁盘存储(支持持久化)三种
  • 支持多种集群方案,解决数据共享问题

本地缓存解决方案-Caffeine Cache

Google Guava Cache是一种非常优秀本地缓存解决方案,提供了基于容量,时间和引用的缓存回收方式。基于容量的方式内部实现采用LRU算法,基于引用回收很好的利用了Java虚拟机的垃圾回收机制。其中的缓存构造器CacheBuilder采用构建者模式提供了设置好各种参数的缓存对象,缓存核心类LocalCache里面的内部类Segment与jdk1.7及以前的ConcurrentHashMap非常相似,都继承于ReetrantLock,还有六个队列,以实现丰富的本地缓存方案。 ​ 通俗的讲,Guva是google开源的一个公共java库,类似于Apache Commons,它提供了集合,反射,缓存,科学计算,xml,io等一些工具类库。cache只是其中的一个模块。使用Guva cache能够方便快速的构建本地缓存。

Caffeine是使用Java8对Guava缓存的重写版本,在Spring Boot 2.0中将取代Guava。如果出现Caffeine,CaffeineCacheManager将会自动配置。

代码实现: 基于SpringBoot(2.3.2.RELEASE)+mybatis-plus(3.4.3)实现
在这里插入图片描述

sql脚本

CREATE TABLE `tb_user` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
  `user_name` varchar(100) DEFAULT NULL COMMENT '姓名',
  `phone` varchar(15) DEFAULT NULL COMMENT '手机号',
  `province` varchar(50) DEFAULT NULL COMMENT '省份',
  `city` varchar(50) DEFAULT NULL COMMENT '城市',
  `salary` int DEFAULT NULL,
  `hire_date` datetime DEFAULT NULL COMMENT '入职日期',
  `dept_id` bigint DEFAULT NULL COMMENT '部门编号',
  `birthday` datetime DEFAULT NULL COMMENT '出生日期',
  `photo` varchar(200) DEFAULT NULL COMMENT '照片路径',
  `address` varchar(300) DEFAULT NULL COMMENT '现在住址',
  PRIMARY KEY (`id`),
  KEY `fk_dept` (`dept_id`)
) ENGINE=InnoDB AUTO_INCREMENT=61 DEFAULT CHARSET=utf8mb3;

pom

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.3</version>
</dependency>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.14</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

application.yml

# DataSource Config
spring:
  datasource:
    #   数据源基本配置
    url: jdbc:mysql://localhost:3306/study_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
    initialization-mode: always #表示始终都要执行初始化,2.x以上版本需要加上这行配置
    type: com.alibaba.druid.pool.DruidDataSource
    #   数据源其他配置
    initialSize: 5
    minIdle: 5
    maxActive: 20
    maxWait: 60000
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    poolPreparedStatements: true
    #   配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
    filters: stat,wall,log4j
    maxPoolPreparedStatementPerConnectionSize: 20
    useGlobalDataSourceStat: true
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500

# Logger Config
logging:
  level:
    cn.zysheep.mapper: debug

缓存配置类

@Configuration
@EnableCaching
public class CaffeineCacheConfig {
    @Bean
    public CacheManager cacheManager(){
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        //Caffeine配置
        Caffeine<Object, Object> caffeine = Caffeine.newBuilder()
                //最后一次写入后经过固定时间过期
                .expireAfterWrite(10, TimeUnit.SECONDS)
                //maximumSize=[long]: 缓存的最大条数
                .maximumSize(1000);
        cacheManager.setCaffeine(caffeine);
        return cacheManager;
    }
}

Caffeine配置说明:

  • initialCapacity=[integer]: 初始的缓存空间大小
  • maximumSize=[long]: 缓存的最大条数
  • maximumWeight=[long]: 缓存的最大权重
  • expireAfterAccess=[duration]: 最后一次写入或访问后经过固定时间过期
  • expireAfterWrite=[duration]: 最后一次写入后经过固定时间过期
  • refreshAfterWrite=[duration]: 创建缓存或者最近一次更新缓存后经过固定的时间间隔,刷新缓存
  • weakKeys: 打开key的弱引用
  • weakValues:打开value的弱引用
  • softValues:打开value的软引用
  • recordStats:开发统计功能 注意:
  • expireAfterWrite和expireAfterAccess同事存在时,以expireAfterWrite为准。
  • maximumSize和maximumWeight不可以同时使用
  • weakValues和softValues不可以同时使用

业务代码

1、User 实体

@TableName(value ="tb_user")
@Data
public class User implements Serializable {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String userName;
    private String phone;
    private String province;
    private String city;
    private Integer salary;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
    private Date hireDate;
    private Long deptId;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
    private Date birthday;
    private String photo;
    private String address;
}

2、UserMapper

public interface UserMapper extends BaseMapper<User> {

}

3、UserService

public interface UserService extends IService<User> {

    List<User> getUserByName(String name);
}

4、UserServiceImpl

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
    implements UserService{

}

5、UserController

@RestController
@RequestMapping("/api")
public class UserController {
    @Autowired
    private UserService userService;

}

6、启动类

@MapperScan("cn.zysheep.mapper")
@SpringBootApplication
public class CaffeineApplication {
    public static void main(String[] args) {
        SpringApplication.run(CaffeineApplication.class, args);
    }
}

快速体验缓存

不使用缓存

@GetMapping("getByNameNoCache")
public ResponseEntity getByNameNoCache(@RequestParam String username) {
    List<User> users = userService.getUserByNameNoCache(username);
    return ResponseEntity.ok(users);
}

public List<User> getUserByNameNoCache(String userName) {
   LambdaQueryWrapper<User> queryWrapper = Wrappers.<User>lambdaQuery().like(StringUtils.isNotBlank(userName), User::getUserName, userName);
   List<User> users = list(queryWrapper);
   log.info("从数据库中读取,而非从缓存读取!");
   log.info("users: {}", users);
   return users;
}

不使用缓存每次都会查询数据库
在这里插入图片描述

使用缓存

开启注解缓存 @EnableCaching

在这里插入图片描述
idea会识别缓存注解,显示缓存标识
在这里插入图片描述

标注缓存注解

  • @Cacheable: @Cacheble注解表示这个方法有了缓存的功能,方法的返回值会被缓存下来,下一次调用该方法前,会去检查是否缓存中已经有值,如果有就直接返回,不调用方法。如果没有,就调用方法,然后把结果缓存起来。这个注解一般用在查询方法上。
  • @CacheEvict: @CacheEvict注解的方法,会清空指定缓存。一般用在更新或者删除的方法上
  • @CachePut : @CachePut注解的方法,保证方法被调用,又希望结果被缓存。会把方法的返回值put到缓存里面缓存起来。它通常用在新增方法上。
  • @Caching :定义复杂的缓存规则
  • @CacheConfig:抽取缓存的公共配置
@Cacheable

@Cacheable标注的方法执行之前先来检查缓存中有没有这个数据,默认按照参数的值作为key去查询缓存, 如果存在就不再执行该方法,而是直接从缓存中获取结果进行返回,否则才会执行并将返回结果存入指定的缓存中。

当我们在声明 @Cacheable 时不指定 key 参数,则该缓存名下的所有 key 会使用 KeyGenerator 根据参数 自动生成。spring 有一个默认的 SimpleKeyGenerator ,在 spring boot 自动化配置中,这个会被默认注入。生成规则如下:

  • 如果该缓存方法没有参数,返回 SimpleKey.EMPTY,默认值是"",这意味着所有方法参数都被视为一个键,除非已经配置了自定义keyGenerator
  • 如果该缓存方法有一个参数,返回该参数的实例 ;
  • 如果该缓存方法有多个参数,返回一个包含所有参数的 SimpleKey ;
public @interface Cacheable {
    @AliasFor("cacheNames")
    String[] value() default {};

    @AliasFor("value")
    String[] cacheNames() default {}; 
    #指定缓存组件的名字;将方法的返回结果放在哪个缓存中,是数组的方式,可以指定多个缓存;

    String key() default "";
    # 缓存数据使用的key;可以用它来指定。默认是使用方法参数的值.(1-方法返回值)
    编写SpEL; #id;参数id的值   #a0  #p0  #root.args[0]

    String keyGenerator() default "";  
    # key的生成器;可以自己指定key的生成器的组件id,key/keyGenerator:二选一使用;

    String cacheManager() default ""; 
    # 指定缓存管理器;或者cacheResolver指定获取解析器

    String cacheResolver() default "";
    # 缓存解析器

    String condition() default ""; 
    # 指定符合条件的情况下才缓存;condition = "#id>0"  condition = "#a0>1":第一个参数的值>1的时候才进行缓存
    String unless() default "";  # 否定缓存;当unless指定的条件为true,方法的返回值就不会被缓存;可以获取到结果进行判断

    boolean sync() default false;  # 是否使用异步模式
}

Spring还为我们提供了一个root对象可以用来生成key。通过该root对象我们可以获取到以下信息。属性key用SpEL的写法

名字位置描述示例
methodNameroot object当前被调用的方法名#root.methodName
methodroot object当前被调用的方法#root.method.name
targetroot object当前被调用的目标对象#root.target
targetClassroot object当前被调用的目标对象类#root.targetClass
argsroot object当前被调用的方法的参数列表#root.args[0]
cachesroot object当前方法调用使用的缓存列表(如@Cacheable(value={“cache1”, “cache2”})),则有两个cache#root.caches[0].name
argument nameevaluation context方法参数的名字. 可以直接 #参数名 ,也可以使用 #p0或#a0 的形式,0代表参数的索引;#username 、 #a0 、 #p0
resultevaluation context方法执行后的返回值(仅当方法执行之后的判断有效,如‘unless’,’cache put’的表达式 ’cache evict’的表达式beforeInvocation=false)#result

当我们要使用root对象的属性作为key时我们也可以将“#root”省略,因为Spring默认使用的就是root对象的属性。

测试

1、基本形式

@Cacheable(cacheNames = "user", key = "#id")
public List<User> getUserById(Long id) {
   LambdaQueryWrapper<User> queryWrapper = Wrappers.<User>lambdaQuery().like(Objects.nonNull(id), User::getId, id);
   List<User> users = list(queryWrapper);
   log.info("从数据库中读取,而非从缓存读取!");
   log.info("users: {}", users);
   return users;
}

第一次查库,后面相同参数的查询直接返回缓存数据,10s缓存过期,再次查询,会查库,然后缓存返回值数据

2、组合形式

// spEL使用"T(Type)"来表示 java.lang.Class 实例,"Type"必须是类全限定名,"java.lang"包除外。
@Cacheable(cacheNames = "user", key = "T(String).valueOf(#id).concat('::').concat(#userName)")
public List<User> getUserByIdAndName(Long id, String userName) {
    LambdaQueryWrapper<User> queryWrapper = Wrappers.<User>lambdaQuery()
            .like(StringUtils.isNotBlank(userName), User::getUserName, userName)
            .eq(Objects.isNull(id), User::getId, id);
    List<User> users = list(queryWrapper);
    System.out.println("从数据库中读取,而非从缓存读取!");
    return users;

3、对象形式

@Cacheable(cacheNames = "user", key = "#user.userName")
public List<User> getUser(User user) {
   LambdaQueryWrapper<User> queryWrapper = Wrappers.<User>lambdaQuery().like(StringUtils.isNotBlank(user.getUserName()), User::getUserName, user.getUserName());
   List<User> users = list(queryWrapper);
   System.out.println("从数据库中读取,而非从缓存读取!");
   return users;
}

4、自定义key生成策略

@Component
public class CacheKeyGenerator implements KeyGenerator {
    @Override
    public Object generate(Object target, Method method, Object... params) {
        ObjectMapper JSON = new ObjectMapper();
        String key = null;
        try {
            key = target.getClass().getSimpleName() + "#" + method.getName() + "(" + JSON.writeValueAsString(params) + ")";
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return key;
    }
}

注意:官方说 key 和 keyGenerator 参数是互斥的,同时指定两个会导致异常。

原理

1、自动配置类;CacheAutoConfiguration

2、缓存的配置类

  • org.springframework.boot.autoconfigure.cache.GenericCacheConfiguration
  • org.springframework.boot.autoconfigure.cache.JCacheCacheConfiguration
  • org.springframework.boot.autoconfigure.cache.EhCacheCacheConfiguration
  • org.springframework.boot.autoconfigure.cache.HazelcastCacheConfiguration
  • org.springframework.boot.autoconfigure.cache.InfinispanCacheConfiguration
  • org.springframework.boot.autoconfigure.cache.CouchbaseCacheConfiguration
  • org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration
  • org.springframework.boot.autoconfigure.cache.CaffeineCacheConfiguration
  • org.springframework.boot.autoconfigure.cache.GuavaCacheConfiguration
  • org.springframework.boot.autoconfigure.cache. 【默认】
  • org.springframework.boot.autoconfigure.cache.NoOpCacheConfiguration

3、哪个配置类默认生效:SimpleCacheConfiguration

4、给容器中注册了一个CacheManagerConcurrentMapCacheManager

5、可以获取和创建ConcurrentMapCache类型的缓存组件;他的作用将数据保存在ConcurrentMap中;

运行流程
  1. 方法运行之前,先去查询Cache(缓存组件),按照cacheNames指定的名字获取;CacheManager先获取相应的缓存),第一次获取缓存如果没有Cache组件会自动创建。
  2. 去Cache中查找缓存的内容,使用一个key,默认就是方法的参数;key是按照某种策略生成的;默认是使用keyGenerator生成的,默认使用SimpleKeyGenerator生成key;SimpleKeyGenerator生成key的默认策略;如果没有参数;key=new SimpleKey();如果有一个参数:key=参数的值;如果有多个参数:key=new SimpleKey(params);
  3. 没有查到缓存就调用目标方法;
  4. 将目标方法返回的结果,放进缓存中
@CachePut

@CachePut也可以声明一个方法支持缓存功能。与@Cacheable不同的是使用@CachePut标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。

@CachePut:既调用方法,又更新缓存数据;同步更新缓存 修改了数据库的某个数据,同时更新缓存;前提是必须是同一个缓存,同一个key

运行时机:

  • 先调用目标方法
  • 将目标方法的结果缓存起来
测试

测试步骤
1、查询id=40用户;查到的结果会放在缓存中;缓存key的过期时间设置为(60*5)5分钟
2、更新id=40的用户信息;
3、再次查询id=40的用户信息;

添加缓存注解@CachePut

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
    implements UserService{

    private static final Logger log = LoggerFactory.getLogger(UserServiceImpl.class);

    /**
     * 查询用户信息,并缓存结果
     * @param id
     * @return
     */
    @Cacheable(cacheNames = "user", key = "#id")
    public User getUserById(Long id) {
        User user = getById(id);
        log.info("从数据库中读取,而非从缓存读取!");
        log.info("users: {}", user);
        return user;
    }

    /**
     * 更新用户信息
     * @param user
     * @return
     */
    @CachePut(cacheNames = "user", key = "#result.id")
    public User updateUser(User user) {
        log.info("user: {}", user);
        updateById(user);
        User user1 = getById(user.getId());
        return user1;
    }
}
@RestController
@RequestMapping("/api")
public class UserController {
    @Autowired
    private UserService userService;
    @GetMapping("getById")
    public ResponseEntity getById(@RequestParam Long id) {
        User user = userService.getUserById(id);
        return ResponseEntity.ok(user);
    }
    @PostMapping("updateUser")
    public ResponseEntity updateUser(User user) {
        return ResponseEntity.ok(userService.updateUser(user));
    }
}

1、PostMan 查询id=40用户;查到的结果会放在缓存中;缓存key的过期时间设置为(60*5)5分钟
在这里插入图片描述

2、更新id=40的用户信息;数据库数据已更新成功
在这里插入图片描述
在这里插入图片描述

3、再次查询id=40的用户信息;查询返回的还是上一次缓存的数据,缓存未更新
在这里插入图片描述

为什么?

1、因为缓存默认的key为参数的值,即第一次查询到的user缓存的key 为id的值40;
2、更新用户将方法的返回值放进缓存了;key是传入的user对象的值, 值是返回的User对象;是两个不同的缓存,所以总是显示为上一次缓存的数据

如何解决

只需要缓存相同的key就可以实现修改缓存数据;即取缓存的key和存缓存的key相同

key = "#result.id",key = "#user.id"

key = "#user.id":使用传入的参数的员工id;
key = "#result.id":使用返回后的id

注意:@Cacheable的key是不能用#result

@CachePut(cacheNames = "user", key = "#result.id")
public User updateUser(User user) {
    log.info("user: {}", user);
    updateById(user);
    User user1 = getById(user.getId());
    return user1;
}
@CacheEvict

@CacheEvict是用来标注在需要清除缓存元素的方法或类上的。当标记在一个类上时表示其中所有的方法的执行都会触发缓存的清除操作

测试
@CacheEvict(cacheNames = "user", beforeInvocation = true, key = "#id")
public String deleteUserById(Long id) {
    boolean b = removeById(id);
    int i = 1/0;
    return b?"删除成功":"删除失败";
}
  • key:指定要清除的数据
  • allEntries = true:指定清除这个缓存中所有的数据
  • beforeInvocation = false:缓存的清除是否在方法之前执行,默认代表缓存清除操作是在方法执行之后执行;如果出现异常缓存就不会清除
  • beforeInvocation = true:代表清除缓存操作是在方法运行之前执行,无论方法是否出现异常,缓存都清除
@Caching

@Caching 定义复杂的缓存规则

测试
@Caching(
            cacheable = {
                    @Cacheable(value="emp",key = "#lastName")
            },
            put = {
                    @CachePut(value="emp",key = "#result.id"),
                    @CachePut(value="emp",key = "#result.email")
            }
    )
    public Employee getEmpByLastName(String lastName) {
   		 return employeeMapper.getEmpByLastName(lastName);
    }
@CacheConfig

每次都要定义cacheNames ,和key,可以在类上定义公共配置,方便管理

@CacheConfig(cacheNames = "emp"/*,cacheManager = "employeeCacheManager"*/) //抽取缓存的公共配置

Spring cache默认使用的是ConcurrentMapCacheManager==ConcurrentMapCache;将数据保存在 ConcurrentMap<Object, Object>中

开发中使用缓存中间件;redis、memcached、ehcache;或者替换本地缓存为Guava或Caffeine

  • 5
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
Spring Boot 是一个开源的Java框架,它能够帮助开发者快速构建基于Spring的应用程序。为了提高应用程序的性能,Spring Boot提供了本地缓存的支持。本文将介绍如何使用ConcurrentLinkedHashMap实现Spring Boot本地缓存。 ConcurrentLinkedHashMap是一个线程安全的LRU缓存实现,它基于ConcurrentHashMap和LinkedHashMap。ConcurrentLinkedHashMap提供了以下特性: 1. 线程安全:ConcurrentLinkedHashMap是线程安全的,多个线程可以同时读写缓存。 2. LRU策略:ConcurrentLinkedHashMap使用LRU策略来管理缓存,当缓存满了之后,会将最近最少使用的缓存项删除。 3. 容量控制:ConcurrentLinkedHashMap提供了可配置的容量控制,开发者可以通过设置最大缓存大小来控制缓存的容量。 下面是使用ConcurrentLinkedHashMap实现Spring Boot本地缓存的步骤: 步骤1:添加依赖 在pom.xml文件中添加以下依赖: ``` <dependency> <groupId>com.googlecode.concurrentlinkedhashmap</groupId> <artifactId>concurrentlinkedhashmap-lru</artifactId> <version>1.4.2</version> </dependency> ``` 步骤2:创建缓存类 创建一个缓存类,它包含了ConcurrentLinkedHashMap实例和相关的方法。 ``` import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap; import org.springframework.stereotype.Component; import java.util.Map; @Component public class CacheService { private final Map<String, Object> cache; public CacheService() { cache = new ConcurrentLinkedHashMap.Builder<String, Object>() .maximumWeightedCapacity(1000) .build(); } public void put(String key, Object value) { cache.put(key, value); } public Object get(String key) { return cache.get(key); } public void remove(String key) { cache.remove(key); } public void clear() { cache.clear(); } } ``` 在上面的代码中,我们创建了一个CacheService类,它包含了ConcurrentLinkedHashMap实例和相关的方法。在构造函数中,我们使用ConcurrentLinkedHashMap.Builder创建了一个ConcurrentLinkedHashMap实例,并设置了最大容量为1000。 put、get、remove和clear方法分别用于向缓存中添加、获取、删除和清空数据。 步骤3:使用缓存类 在需要使用缓存的地方,我们可以通过依赖注入的方式获取CacheService实例,并使用其提供的方法进行缓存操作。 ``` import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; @RestController public class TestController { @Autowired private CacheService cacheService; @GetMapping("/cache/{key}") public Object cache(@PathVariable String key) { Object value = cacheService.get(key); if (value == null) { // 查询数据库或其他操作 value = "data from db"; cacheService.put(key, value); } return value; } } ``` 在上面的代码中,我们注入了CacheService实例,并在cache方法中使用了它的get和put方法进行缓存操作。如果缓存中不存在指定的key,则查询数据库或其他操作,然后将结果存入缓存中。 这样,我们就使用ConcurrentLinkedHashMap实现了Spring Boot本地缓存。ConcurrentLinkedHashMap提供了线程安全、LRU策略和容量控制等特性,能够帮助我们提高应用程序的性能。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李熠漾

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值