Spring Cache 注意事项

Last Modified: 2022/12/02

概述

Spring cache 为我们提供了缓存一致性抽象,开发者通过声明式的注解即可完成模板化的缓存操作。具体使用可以参考我的上一篇文章“Spring cache annotations”。本文和大家一起去学习 Spring cache 的高级使用技巧和使用过程中可能遇到的一些坑。

1. Spring Cache 的陷阱

1.1 无参函数之键值陷阱

Spring 4.x 及以上版本默认使用 SimpleKeyGenerator(以下简称 SKG)键生成器,需要注意的 SKG 使用方法所有的参数生成键值,当方法不包含任何参数的时候,将会使用 SimpleKey.EMPTY 作为键值。

@Cacheable(cacheNames="aCache")
public int getUserAge() { return 20; }
@Cacheable(cacheNames="aCache")
public int getNum() { return 30; }
// 依次调用 getUserAge 和 getNum
getUserAge(); // returns 20
getNum(); // returns 20 or 30 ?

相信你一定知道 getNum() 的返回值。

1.2 对象参数之键值陷阱

@Cacheable(cacheNames="userInfoCache")
public UserInfo getUserInfo(User user) { return aUser; }

在说明陷阱之前,我们假设使用的是 SKG 生成器,同时我们假设 User 对象关联了很多其它的对象。这很容易理解,例如:一个用户有车、有房、有老婆,上有老,下有小,在系统中这些都表现为 User 对象的关联对象,User 对象不是一个对象而是拖家带口的一个对象图。 再来看看 SKG 的生成器是怎么生成键值的:

public static Object generateKey(Object... params) {
    // ... 此处省去不重要的代码
	return new SimpleKey(params);
}
// 再来看看 SimpleKey
public class SimpleKey implements Serializable {
    public SimpleKey(Object... elements) {
        // ... 此处省去不重要的代码
        this.params = new Object[elements.length];
        // ... 此处省去不重要的代码
    }
}

细心的你一定发现了 SimpleKey 对象内部引用了被调用方法的所有参数,拿上面的例子来看,也就是引用了 User 对象,从而间接引用了 User 对象的关联对象,这将会占用很多的内存。 对于上面的例子,我们可以通过明确的指定键值来改进:

@Cacheable(cacheNames="userInfoCache", key="#user.id")
public UserInfo getUserInfo(User user) { return aUser; }

2. 缓存方法的同步问题

@Cacheable(value = "aCache", key = "#id")
public synchronized List<Order> findUserOrders(int id) {...}

如果有多线程同时调用findUserOrders(10),是否能保证只有第一个线程执行真正的方法调用,其它线程从缓存中读取呢? 答案是不能,因为该方法会被 aop 代理,然而代理方法不是同步的,如果多个线程同时调用,它们都会先检查缓存,假如缓存中此时还没有值,那么它们都会调用该方法。 幸运的是,spring 已经提供了解决方案。

@Cacheable(value = "aCache", key = "#id", sync=true)
public List<Order> findUserOrders(int id) {...}

3. 缓存和事务的纠缠

Spring cache 使得缓存变得简单透明,但是当缓存注解和事务一起使用的时候,你需要都多长点心眼。事务可能会提交也可能会回滚,可能会出现缓存更新了,但是最终的事务回滚了,造成缓存数据和真实数据不一致。 解决方案是当事务提交时,才执行相应的缓存操作,否则不执行。有一些缓存,例如,ehcache 直接提供了这种支持:

EhCacheCacheManager m = new EhCacheCacheManager();
cacheManager.setCacheManager(m);
cacheManager.setTransactionAware(true);

对于那些不支持事务处理的 CacheManager,则可以使用 Spring 提供的 TransactionAwareCacheManagerProxy 类来包装。

@Bean
public CacheManager cacheManager() {
	SimpleCacheManager m = new SimpleCacheManager();
	m.setCaches(Collections.singletonList(new ConcurrentMapCache("userCache")));
	m.initializeCaches(); 
	return new TransactionAwareCacheManagerProxy(m);
}

TransactionAwareCacheManagerProxy 返回的 Cache 对象,是经过装饰的对象,对应的实现类为 TransactionAwareCacheDecorator。经过装饰之后的 Cache 对象,缓存操作(evict/put/clear) 会发生在事务提交之后。以 evict(key) 为例:

public void evict(final Object key) {
    if (TransactionSynchronizationManager.isSynchronizationActive()) {
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
            @Override
            public void afterCommit() {
                // 事务提交之后才真正的执行 evict 操作
                TransactionAwareCacheDecorator.this.targetCache.evict(key);
            }
        });
    } else {
        // 如果没有事务,则直接 evict。
        this.targetCache.evict(key);
    }
}

如果我们使用 spring boot,并且引入了 spring-boot-starter-data-redis,我们可以使用 RedisCacheManager,该 RedisCacheManager 继承了 AbstractTransactionSupportingCacheManager,因此 RedisCacheManager 是支持事务的。可以像下面这样配置:

@Bean
public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer() {
    return (builder) -> builder
            .transactionAware()
            .withCacheConfiguration("someCache", RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(10)));
}
有问题吗?点此反馈!

温馨提示:反馈需要登录