@Cacheable注解

本地缓存

在很多时候,需要对数据库中查询出来的数据进行缓存操作,避免频繁的查库给数据库带来不必要的压力,所以诞生了缓存。
常见的缓存中间件有 Redis、Memcache、Ehcache 等。比如常用的 Redis 其实是一种常见的 K-V 非关系型数据库,处理很多数据的缓存时,需要经过网络 IO 才能提供服务。与网络缓存对应的,就是本地缓存。其中比较经典的本地缓存实现方式,有:

ConcurrentHashMap、Guava Cache、Caffeine、Encache

参考文档:本地缓存:为什么要用本地缓存?用它会有什么问题?_Gimtom的博客-CSDN博客_本地缓存

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

其实在 Spring 中,已经有了对应的缓存实现类。Spring 从 3.1 开始就引入了对 Cache 的支持。定义了 org.springframework.cache.Cache 和 org.springframework.cache.CacheManager 接口来统一不同的缓存技术。
其使用方法和原理都类似于 Spring 对事务管理的支持。Spring Cache 是作用在方法上的,其核心思想是,当我们在调用一个缓存方法时会把该方法参数和返回结果作为一个键值对存在缓存中。

使用步骤

一、导入依赖

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

二、在启动类进行相关的配置

/**
* 开启缓存注解驱动,否则后面使用的缓存都是无效的
*/
@EnableCaching
@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}

或者执行一个配置类

/**
 * 或者进行 CacheConfig 一类的配置类
 * @author Real
 * Date: 2022/9/7 20:01
 */
@Slf4j
@EnableCaching
@Configuration
public class CacheConfig {
    @Bean("myKeyGenerator")
    public KeyGenerator keyGenerator() {
        return new KeyGenerator() {
            @Override
            public Object generate(Object target, Method method, Object... params) {
                return method.getName() + "[" + Arrays.asList(params).toString() + "]";
            }
        };
    }
}

三、在需要使用缓存的地方使用对应的注解

一般在 ServiceImpl 类上使用,或者在对应的方法上使用。

@Service
@CacheConfig(cacheNames = "deptCache", keyGenerator = "myKeyGenerator")
public class DepartmentServiceImpl extends ServiceImpl<DepartmentMapper, Department>
        implements DepartmentService {

    @Autowired
    private DepartmentMapper departmentMapper;

    @Override
    @Cacheable(cacheNames = "deptCache")
    public List<Department> getAllDept() {
        return departmentMapper.selectList(Wrappers.emptyWrapper());
    }

    @Override
    @Cacheable(cacheNames = "deptCache", key = "#id")
    public Department getById(Integer id) {
        return departmentMapper.selectById(id);
    }
}

这里需要注意的是:Spring 并不推荐在接口上使用 Cache* 注解

image.png
四、测试使用
第一次访问对应的方法,会使用 Mapper 接口查询数据库数据。
image.png
第二次访问同一个方法时,不走 Mapper 接口调用。
image.png
此外,还可以设置使用缓存的条件,不符合条件的将不会使用缓存。

@Override
@Cacheable(cacheNames = "deptCache", condition = "#id > 10")
public Department getDeptByCondition(Integer id) {
    return departmentMapper.selectById(id);
}

Cache SpEL available metadata

名称位置描述示例
methodNameroot对象当前被调用的方法名#root.methodname
methodroot对象当前被调用的方法#root.method.name
targetroot对象当前被调用的目标对象实例#root.target
targetClassroot对象当前被调用的目标对象的类#root.targetClass
argsroot对象当前被调用的方法的参数列表#root.args[0]
cachesroot对象当前方法调用使用的缓存列表#root.caches[0].name
argumentName执行上下文(avaluation context)当前被调用的方法的参数,如findArtisan(Artisan artisan),可以通过#artsian.id获得参数#artsian.id
result执行上下文(evaluation context)方法执行后的返回值(仅当方法执行后的判断有效,如 unless cacheEvict的beforeInvocation=false)#result

执行分析

在第二次使用到缓存的时候,在 Mapper 调用的方法上打断点,并不会触发 SqlSession 对象的创建和调用。
而在 CacheManager 接口上的 getValue 方法打上断点,却会出现响应:
image.png
真正调用的实现类是 ConcurrentMapCacheManager 实现类。
image.png
这个实现类的代码为:

@Nullable
public Cache getCache(String name) {
    Cache cache = (Cache)this.cacheMap.get(name);
    if (cache == null && this.dynamic) {
        synchronized(this.cacheMap) {
            cache = (Cache)this.cacheMap.get(name);
            if (cache == null) {
                cache = this.createConcurrentMapCache(name);
                this.cacheMap.put(name, cache);
            }
        }
    }
    return cache;
}

可以看到使用了一个简单的 DCL 来实现。创建 Cache 的方法也比较简单:

protected Cache createConcurrentMapCache(String name) {
    SerializationDelegate actualSerialization = this.isStoreByValue() ? this.serialization : null;
    return new ConcurrentMapCache(name, new ConcurrentHashMap(256), this.isAllowNullValues(), actualSerialization);
}

这里使用到了一个 SerializationDelegate 序列化委托对象,这个方法提供了一个方便的委托,具有预先安排的配置状态,可满足常见的序列化需求。

protected ConcurrentMapCache(String name, ConcurrentMap<Object, Object> store,
			boolean allowNullValues, @Nullable SerializationDelegate serialization) {
    super(allowNullValues);
    Assert.notNull(name, "Name must not be null");
    Assert.notNull(store, "Store must not be null");
    this.name = name;
    this.store = store;
    this.serialization = serialization;
}

默认使用一个 capacity 为 256 的 ConcurrentHashMap 对象来存储对应的值。

实现原理

Cache 接口包含缓存的各种操作集合,你操作缓存就是通过这个接口来操作的。 Cache 接口下 Spring 提供了各种 xxxCache 的实现,比如:RedisCache、EhCache、ConcurrentMapCache 等实现类。
CacheManager 定义了创建、配置、获取、管理和控制多个唯一命名的 Cache。这些 Cache 存在于 CacheManager 的上下文中。

public interface CacheManager {
    @Nullable
    Cache getCache(String name);

    Collection<String> getCacheNames();
}

每次调用需要缓存功能的方法时,Spring 会检查指定参数的指定目标方法是否已经被调用过,如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。
使用 Spring 缓存抽象时我们需要关注以下两点;

  • 确定方法需要被缓存以及他们的缓存策略。
  • 从缓存中读取之前缓存存储的数据。
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐