26

Spring 缓存大法

 3 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzUxOTE5MTY4MQ%3D%3D&%3Bmid=2247484529&%3Bidx=1&%3Bsn=2a37072f8eb5338d9174bc219e7e2bb7
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

作者 | Sunny

jYzi2eM.jpg!web

Coder

一、前言

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

m2emAjI.jpg!web

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

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

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

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

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

zAfuuyj.jpg!web

二、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)如果有多个实例,本地缓存不能共享给其他实例,每个实例只能重复去持久层查询本地不存在的缓存。

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

6JZzuqq.jpg!web

2、多级缓存

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

ZNz6jaf.jpg!web

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

1、缓存不一致

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

解决方案:

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

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

2、缓存穿透

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

解决方案:

(1)设置默认值。

(2)布隆过滤器。

3、缓存雪崩

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

解决方案:

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

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

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

总结

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

全文完

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

我们正在招聘 Java 工程师,欢迎有兴趣的同学投递简历到 [email protected]

umq6n23.jpg!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK