作者 | Sunny

Coder

一、前言

这次要介绍的是日常被大家忽略的 Spring 隐藏大杀器,这就是 spring-context 组件中的 cache 缓存模块,它也算是 spring 家族中非常核心的模块了:

1、Spring 缓存模块的架构设计

Spring 缓存模块的架构设计十分简单清晰,整体上可以分为 3 层:

(1)业务接入层:通过 AOP 环绕注解可以方便地开启和维护缓存。

(2)缓存管理层:通过 CacheManager 解耦业务接入层和缓存存储层,可以方便、快速地定制缓存存储方式。

(3)缓存存储层:Spring 制定了标准的缓存存储接口,只要实现这套接口,任何缓存存储方式都能轻松接入;无论是本地缓存、还是分布式缓存,对于业务接入方来说是无感的。

二、Spring 缓存注解

1、开启缓存:@EnableCaching

在 SpringBoot 应用启动类(@SpringBootApplication 标注的类)上添加@EnableCaching 注解,一键开启 SpringBoot 以注解驱动的缓存管理能力。

SpringBoot 默认提供一个 Concurrent Hashmap  来管理缓存,当然我们也可以重写  CacheManager 来注册外部缓存提供方。如果你不需要诸如:缓存失效,使用默认的就足够了。

缓存按名字分区。缓存区可以配置成单值或列表,所以我们可以在一个方法上同时操作多个缓存区中的键。多分区操作在复杂的业务中是可取的,但原则上,我们不应该这样做。

2、缓存化:@Cacheable(value="缓存区", key="缓存键")

在方法上添加@Cacheable 注解,说明方法的计算结果是可缓存的,第一次计算完成后将计算结果写到缓存中,后续访问该方法则直接返回缓存中的值;这样可以通过减少高时间复杂度方法的计算频次,来提高系统的性能。

3、更新缓存:@CachPut(value="缓存区", key="缓存键")

更新缓存区中这个键的值。

4、删除缓存:@CachEvict(value="缓存区", key="缓存键")

从缓存区中删除这个键。

5、更新或删除缓存:@Caching(@Cacheable、@CachPut、@CachEvict)

可组合使用@Cacheable、@CachPut、@CachEvict。

6、其他属性

属性 作用
cacheNames value 的别名
keyGenerator 指定 Key 生成策略
cacheManager 指定缓存管理器
cacheResolver 指定缓存查询器
condition 缓存可用条件,使用 SpringEL 表达式,满足条件则缓存
unless 缓存否决条件,使用 SpringEL 表达式,满足条件则不缓存
allEntries 操作所有的键
beforeInvocation 在调用方法前触发缓存操作

注意:缓存仅适用于读多写少的场景,如果更新缓存的频率很高,并且对缓存值的一致性要求很高,那么就不应该用缓存。

三、Spring 缓存应用案例

1、从零开始

通过 findUser 接口查询用户,没有开启缓存功能:

@SpringBootApplication
@RestController
public class SpringCacheApplication {@Autowiredprivate UserDao userDao;public static void main(String[] args) {SpringApplication.run(SpringCacheApplication.class, args);}@GetMapping("/user")public Object findUser(@RequestParam String name) {return userDao.find(name);}
}

UserDao 数据访问层,简单地用入参创建并返回一个新的 User 对象:

@Service
public class UserDao {public Optional<User> find(String name) {var user = User.builder().name(name).build();return Optional.of(user);}
}

User 实体定义,每次创建 User 对象时,会带上一个随机的版本号:

@Data
@Builder
public class User {private String name;@Builder.Defaultprivate int version = new Random().nextInt();
}

这个没有缓存能力的接口,每次被访问都会执行 UserDao#find 方法,且每次返回的 User 都是新对象,可以看到它们的版本号都不一样:

API 请求参数 响应
1)findUser name=sunny {
"name": "sunny",
"version": 304436976
}
2)findUser 同1 {
"name": "sunny",
"version": -898908830
}

2、缓存化

通过@EnableCaching 开启 SpringBoot 应用的缓存能力:

@SpringBootApplication
@RestController
@EnableCaching
public class SpringCacheApplication

通过@Cacheable 开启 UserDao#find 方法的缓存化能力:

@Service
public class UserDao {@Cacheable(cacheNames = {"user"})public Optional<User> find(String name) {var user = User.builder().name(name).build();return Optional.of(user);}
}

开启缓存后,再访问这个 API 会出现什么变化呢?通过下面的测试表格,可以清楚看到,相同的参数,UserDao#find 方法只会计算一次;如果命中了缓存,直接返回缓存中的值:

API 请求参数 响应 命中缓存
1)findUser name=sunny {
"name": "sunny",
"version": -89376086
}
x
2)findUser 同1 同1
3)findUser name=snow {
"name": "snow",
"version": 585307717
}
x
4)findUser 同3 同3

3、更新缓存

通过 updateUser 接口更新用户:

@SpringBootApplication
@RestController
@EnableCaching
public class SpringCacheApplication {@GetMapping("/user/update")public Object updateUser(@RequestParam String name) {return userDao.update(name);}
}

在 UserDao 中添加 update 方法,更新用户,同时用@CachePut 注解更新缓存:

@Service
public class UserDao {@CachePut(cacheNames = {"user"})public Optional<User> update(String name) {var user = User.builder().name(name).build();return Optional.of(user);}
}

调用更新 API 之后,可以看到,缓存也更新了:

API 请求参数 响应 命中缓存
1)findUser name=sunny {
"name": "sunny",
"version": 462247435
}
x
2)findUser 同1 同1
3)updateUser name=sunny {
"name": "sunny",
"version": 835860828
}

命中并更新
4)findUser name=sunny 同3
5)findUser name=sunny 同3

4、删除缓存

通过 deleteUser 接口删除用户:

@SpringBootApplication
@RestController
@EnableCaching
public class SpringCacheApplication {@GetMapping("/user/delete")public Object deleteUser(@RequestParam String name) {return userDao.delete(name);}
}

在 UserDao 中添加 delete 方法,删除用户,同时用@CachePut 注解删除缓存:

@Service
public class UserDao {@CacheEvict(cacheNames = {"user"})public Optional<User> delete(String name) {var user = User.builder().name(name).build();return Optional.of(user);}
}

调用删除 API 之后,可以看到,旧的缓存也被删除了:

API 请求参数 响应 命中缓存
1)findUser name=sunny {
"name": "sunny",
"version": -1188919326
}
x
2)findUser 同1 同1
3)deleteUser name=sunny {
"name": "sunny",
"version": 1818509273
}

命中并删除
4)findUser name=sunny null x

四、Key 生成策略

Key 是创建、更新、删除缓存的主键。Spring 提供了一套默认生成策略,当然我们也可以很方便地自定义生成策略。

Key 与业务主键应当保持一致。

1、默认策略

SpringCache 自带的默认策略 SimpleKeyGenerator

(1)方法参数个数=0,key=0。

(2)方法参数个数=1,key=方法参数。

(3)方法参数个数>1,key=Arrays#deepHashCode(参数列表),即递归累计所有参数的 hashCode;如果参数是数组类型,会递归累计所有数组元素的 hashCode;如果参数是自定义对象(如封装的复杂查询对象),使用自定义对象的 hashCode,这种情况一般需要重写 hashCode 方法。

自定义默认策略

扩展 KeyGenerator 接口,重写 generate 方法:

public class CustomKeyGenerator implements KeyGenerator {@Overridepublic Object generate(Object target, Method method, Object... params) {return String.format("%s_%s_%s",target.getClass().getSimpleName(),method.getName(),StringUtils.arrayToDelimitedString(params, "_"));}
}

然后,将自定义的默认策略注册到 IoC 容器中,即可替代 Spring 自动装配的SimpleKeyGenerator :

@Configuration
public class CachingConfig extends CachingConfigurerSupport {@Bean("customKeyGenerator")public KeyGenerator keyGenerator() {return new CustomKeyGenerator();}
}

2、自定义策略

通过 SpringEL 表达式语言来指定缓存的 Key。如:

@Cacheable(cacheNames = {"user"}, key = "#user.name")

更多 SpringEL 用法可以参考官方文档:Spring Expression Language (SpEL)。

五、缓存管理器 CacheManager

1、缓存管理器的作用

(1)作为缓存的容器,管理缓存的创建和销毁。

(2)通过不同的缓存区去隔离不同业务的缓存。

2、自定义缓存管理器:Caffeine

定义缓存区配置:

@Configuration
public class CachingConfig extends CachingConfigurerSupport {enum CacheConfig {/* 缓存区列表 ----------- */user(1, 3),product,;/* 构造函数 ----------- */CacheConfig() {}CacheConfig(int maxSize, int ttl) {this.maxSize = maxSize;this.ttl = ttl;}/* 缓存区配置项 ----------- */int maxSize = 10000;int ttl = 30 + new Random().nextInt() % 30;}
}

定义缓存管理器:

@Configuration
public class CachingConfig extends CachingConfigurerSupport {@Primary@Bean("caffeineCacheManager")public CacheManager caffeineCacheManager() {var cacheManager = new SimpleCacheManager();var caches = new ArrayList<CaffeineCache>();for (var config : CacheConfig.values()) {var cache = new CaffeineCache(config.name(),Caffeine.newBuilder().recordStats().maximumSize(config.maxSize).expireAfterWrite(config.ttl, SECONDS).build());caches.add(cache);}cacheManager.setCaches(caches);return cacheManager;}
}

六、分布式缓存与多级缓存策略

1、分布式缓存

本地缓存的问题:

(1)如果只有一个实例,实例的内存压力会很大。

(2)如果有多个实例,本地缓存不能共享给其他实例,每个实例只能重复去持久层查询本地不存在的缓存。

解决方案:分布式缓存。在本地缓存之上添加一层共享的分布式缓存,本地缓存只与分布式缓存交互。

2、多级缓存

得益于 SpringCache 高度简洁的抽象,可以使用装饰器轻松地封装多级缓存,且每一层封装都可以直接重用 SpringCache 机制:

七、应对缓存故障的常用方案

1、缓存不一致

持久层与缓存层的数据不一致。

解决方案:

(1)正确的更新缓存。参考:缓存更新的套路。

(2)通过异步修复,保证缓存的最终一致性。

2、缓存穿透

查询一个不存在的数据,每次都会穿透缓存层和持久层。

解决方案:

(1)设置默认值。

(2)布隆过滤器。

3、缓存雪崩

大量缓存瞬时失效,请求涌入持久层。

解决方案:

(1)设置均匀的过期时间。

(2)设置多级缓存,降低雪崩概率。

(3)保证分布式缓存高可用。

总结

本文介绍了 Spring Framework 的缓存架构设计和基本概念,通过案例和代码展示 Spring 缓存的主要用法,同时也简单介绍了分布式多级缓存的应用思路、以及应对缓存故障的常用方案;使用 Spring 内置的缓存方案可以让 SpringBoot 应用快速拥有缓存能力,但在引入缓存的同时也要注意和避规它带来的副作用。

全文完


以下文章您可能也会感兴趣:

  • 从 30 分钟到 1 分钟 - 一个 Scala 项目的编译速度优化

  • Hadoop 生态之 MapReduce 及 Hive 简介

  • 从对称加密到非对称加密再到认证中心 -- https 的证书申请

  • 文字描述符了解一下

  • 简单聊聊 TCP 的可靠性

  • 一篇文章带你搞懂 Swagger 与 SpringBoot 整合

  • 延时队列:基于 Redis 的实现

  • 你真的懂 Builder 设计模式吗?论如何实现真正安全的 Builder 模式

  • 锁优化的简单思路

  • iOS开发:Archive、ipa 和 App 包瘦身

我们正在招聘 Java 工程师,欢迎有兴趣的同学投递简历到 rd-hr@xingren.com 。

Spring 缓存大法相关推荐

  1. Spring 缓存注解@Cacheable 在缓存时候 ,出现了第一次进入调用 方法 ,第二次不调用的异常

    Spring 缓存注解@Cacheable 在缓存时候 ,出现了第一次进入调用 方法 ,第二次不调用的异常 参考文章: (1)Spring 缓存注解@Cacheable 在缓存时候 ,出现了第一次进入 ...

  2. 8 -- 深入使用Spring -- 5...1 启用Spring缓存

    8.5.1 启用Spring缓存 Spring配置文件专门为缓存提供了一个cache:命名空间,为了启用Spring缓存,需要在配置文件中导入cache:命名空间. 导入cache:命名空间之后,启用 ...

  3. spring缓存_有关Spring缓存性能的更多信息

    spring缓存 这是我们最后一篇关于Spring的缓存抽象的文章的后续文章 . 作为工程师,您可以通过了解所使用的某些工具的内部知识来获得宝贵的经验. 了解工具的行为有助于您在做出设计选择时变得更加 ...

  4. 简单的Spring Memcached – Spring缓存抽象和Memcached

    在任何读取繁重的数据库应用程序中,缓存仍然是最基本的性能增强机制之一. Spring 3.1发行版提供了一个很酷的新功能,称为Cache Abstraction . Spring Cache Abst ...

  5. Memcached集成Spring缓存环境构建

    2019独角兽企业重金招聘Python工程师标准>>> Memcached简要说明: Memcached 是一个高性能的分布式内存对象缓存系统,用于动态Web应用以减轻数据库负载.它 ...

  6. spring缓存注解@Cacheable和@CacheEvict,设置过期时间和批量模糊删除

    spring缓存注解@Cacheable和@CacheEvict,设置过期时间和批量模糊删除 配置 CacheManager 类 key前缀配置 RedisCache配置 RedisCache 模糊匹 ...

  7. Spring缓存注解@Cacheable、@CacheEvict、@CachePut使用

    从3.1开始,Spring引入了对Cache的支持.其使用方法和原理都类似于Spring对事务管理的支持.Spring Cache是作用在方法上的,其核心思想是这样的:当我们在调用一个缓存方法时会把该 ...

  8. Spring缓存注解【@Cacheable、@CachePut、@CacheEvict、@Caching、@CacheConfig】使用及注意事项

    文章目录 一.概述 二.缓存注解种类 三.优劣势说明 四.如何使用? 五.详细介绍介绍 1)@Cacheable(常用) 1.value/cacheNames 属性 2.key属性 3.keyGene ...

  9. Spring指南之使用Spring缓存数据(Spring Framework官方文档之缓存抽象详解)

    1.请参见官方文档Spring指南之使用 Spring 缓存数据 2.请参见Spring官方文档之缓存抽象 3.参见github代码 文章目录 一.简介 二.你将创造什么(What You Will ...

  10. (转)使用 Spring缓存抽象 支持 EhCache 和 Redis 混合部署

    背景:最近项目组在开发本地缓存,其中用到了redis和ehcache,但是在使用注解过程中发现两者会出现冲突,这里给出解决两者冲突的具体方案. spring-ehcache.xml配置: <?x ...

最新文章

  1. 【驱动】内核打印级别设置
  2. 2021年,神经科学AI有这几大趋势
  3. linux 远程控制权限,总结一下linux远程控制方法
  4. DataGridView中的rows.Count比实际行数多1的原因以及解决办法
  5. 怪怪设计论闲谈篇:职责与解耦的矛盾
  6. (70)信号发生器DDS正弦波设计(二)(第14天)
  7. SQL Tuning 基础概述05 - Oracle 索引类型及介绍
  8. Java基础之Maven
  9. for和select循环语句的应用实践
  10. linux 读取权限目录权限,文件的读取与写入权限《 Linux 文件与目录权限 》
  11. JavaVM和JNIEnv
  12. 主流计算机戴尔笔记本电脑,2017年50款笔记本电脑排行榜
  13. 2010考研数学二第(13)题——导数应用题
  14. 【视频目标检测】|Towards High Performance Video Object Detection
  15. 微信公众号之底部菜单
  16. Uav开发杂记-4-无人机开发的C-C++
  17. Linux_centos版初学(基础命令)
  18. 基于python的情感分析案例-用python实现文本情感分析
  19. DC/DC电源模块直流升压线性可调正负输出5v12v24v转0-±50v/±110v/±200v/±250v/±360v/±500v
  20. radmin自动启动服务器,Radmin自动连接器+服务端一键安装

热门文章

  1. SOC电源管理系统PMIC
  2. 一文带你全方位了解网卡
  3. SharePoint 网站登录不上,3次输入用户名/密码白页
  4. 告别动态规划,连刷40道动规算法题,我总结了动规的套路
  5. linux上wps能云同步吗,WPS For Linux 6634 再次更新发布-文档也要上云
  6. 宽带远程服务器无响应,宽带拨号上网服务器无响应是解决方法(图文)
  7. 5.13 利用图层的矢量蒙版打造浪漫情调 [原创Ps教程]
  8. SLAM ---- 误差测评 ATE、RPE、APE,与EVO
  9. 论文发表如何选择正确的期刊杂志?
  10. vue调用手机浏览器打开pdf_在微信中调用外部浏览器实现文件下载之解决