内容简介:这次要介绍的是日常被大家忽略的 Spring 隐藏大杀器,这就是 spring-context 组件中的 cache 缓存模块,它也算是 spring 家族中非常核心的模块了:Spring 缓存模块的架构设计十分简单清晰,整体上可以分为 3 层:
作者 | 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 { @Autowired private 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.Default private 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 { @Override public 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 高度简洁的抽象,可以使用装饰器轻松地封装多级缓存,且每一层封装都可以直接重用 SpringC ache 机制:
七、应对缓存故障的常用方案
1、缓存不一致
持久层与缓存层的数据不一致。
解决方案:
(1)正确的更新缓存。参考:缓存更新的套路。
(2)通过异步修复,保证缓存的最终一致性。
2、缓存穿透
查询一个不存在的数据,每次都会穿透缓存层和持久层。
解决方案:
(1)设置默认值。
(2)布隆过滤器。
3、缓存雪崩
大量缓存瞬时失效,请求涌入持久层。
解决方案:
(1)设置均匀的过期时间。
(2)设置多级缓存,降低雪崩概率。
(3)保证分布式缓存高可用。
总结
本文介绍了 Spring Framework 的缓存架构设计和基本概念,通过案例和代码展示 Spring 缓存的主要用法,同时也简单介绍了分布式多级缓存的应用思路、以及应对缓存故障的常用方案;使用 Spring 内置的缓存方案可以让 SpringBoot 应用快速拥有缓存能力,但在引入缓存的同时也要注意和避规它带来的副作用。
全文完
以下文章您可能也会感兴趣:
我们正在招聘 Java 工程师,欢迎有兴趣的同学投递简历到 rd-hr@xingren.com 。
以上所述就是小编给大家介绍的《Spring 缓存大法》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- phpmyadmin getshell大法
- Echarts 系列之复制粘贴大法
- 安卓APP测试之HOOK大法
- iOS 模拟器调试大法了解一下?
- 图片和视频编辑之Matrix大法好
- 重启大法好!线上常见问题排查手册
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
UNIX环境高级编程
W.Richard Stevens Stephen A.Rago、Stephen A. Rago / 人民邮电出版社 / 2006-2 / 99.00元
本书是被誉为UNIX编程“圣经”的Advanced Programming in the UNIX Environment一书的更新版。在本书第一版出版后的十几年中,UNIX行业已经有了巨大的变化,特别是影响UNIX编程接口的有关标准变化很大。本书在保持了前一版的风格的基础上,根据最新的标准对内容进行了修订和增补,反映了最新的技术发展。书中除了介绍UNIX文件和目录、标准I/O库、系统数据文件和信......一起来看看 《UNIX环境高级编程》 这本书的介绍吧!