Spring Cache Annotation 使用的注意点和小技巧

使用spring中的cacheable注解需要注意的点,列举了如下:

默认Cache Key的注意点

对一个方法增加缓存是很简单的一件事,只需要简单加上@Cacheable注解就OK了。

@Cacheable(value = "reservationsCache")
public List<Reservation> getReservationsForRestaurant( Restaurant restaurant ) {
}

但是,由于默认使用太方便了,你可能不会清楚默认cache key是怎么生成的,导致一些理解上的问题。默认的key生成策略是创建了一个 SimpleKey,它包含方法调用的所有参数,需要这些参数都实现了hashCode()/equals() 方法,这在通常情况下是没有什么问题的,除非hashCode() 和 equals()会影响cache取值的性能。不过这种情况也很少见就是了。

需要注意的是,这里的参数是cache key不可缺少的一部分,会影响到所使用到的堆大小,因为cache key是被存放在堆里的。
我们举个例子:使用Restaurant这个类作为cache key,而Restaurant是一个复杂的实体类,包含很多数据,并且内部有好几个其他关联类的list。在cache entry存活的生命周期里,这些数据都一直占据着堆空间,即便并没有任何对象与之关联。

考虑到我们的Restaurant有一个唯一键Id,来识别指定的restaurant,我们可以将代码进行如下的适配:

@Cacheable(value = "reservationsCache", key = "#restaurant.id")
public List<Reservation> getReservationsForRestaurant( Restaurant restaurant ) {
}

这里使用了 SpEL 语法来定义了一个自定义的cache key:restaurant的id,这是一个简单的Long类型,即便之后Restaurant实体内的数据再多,也不会影响该key。实际上消耗多少内存是取决于VM的,但是不难想象Restaurant实体的数据肯定会比Long多很多数据。在经过这样的cache key的调整优化后,我们项目节省了数百MB的空间,使得能让我们缓存更多有用的数据。

简而言之,不应该仅仅注意cache key的单一性,也应该注意cache key的实际消耗大小。使用key属性或者自定义键生成器,可以对cache key进行更细粒度的控制。

Cacheable 注解在同步情况下需要注意的点

对于非常耗时的方法,你会想到尽可能优化缓存命中率。当方法被多个线程访问到时,理想情况下是第一次访问进行实际的方法内部处理,之后的访问都走缓存。在一个典型的场景下,你会对方法申明synchronized。但是,下面的代码不会做如你预期的事情:

@Cacheable(value = "reservationsCache", key = "#restaurand.id")
public synchronized List<Reservation> getReservationsForRestaurant( Restaurant restaurant ) {
}

在方法上使用@Cacheable 注解时,具体的cache代码会包在方法体的外面(使用AOP),这意味着在缓存查找后,在方法内部或者方法本身 任何形式的同步都会发生。也就是说,在调用该方法时,会首先进行缓存的查找,如果没有命中缓存,那么就会锁上,方法开始执行。那么想象这样一种场景,当很多个相同请求同时来临,那么所有缓存都不会命中,然后上锁,方法一个接一个执行,方法执行完才会将结果存到cache里。
对于这种问题,解决方案是使用双重检查锁定的手动缓存,或者将synchronization包到cacheable方法外面。对于后者,会需要根据你具体的AOP代理策略增加一个额外的bean。

Spring Framework 4.3

在Spring框架的4.3版本里,提供了对于同步缓存的支持:可以在@Cacheable里定义sync属性,来确保只有一个线程能够构造缓存的值,如下面的例子所示:

@Cacheable(value = "reservationsCache", key = "#restaurand.id", sync = true)
public List<Reservation> getReservationsForRestaurant( Restaurant restaurant ) {
}

将 @CachePut 和 @Cacheable 相结合来优化缓存使用

使用@Cacheable意味着既需要在缓存中进行查询,又需要存储结果到缓存中,使用@CachePut@CacheEvict(驱逐)注解可为你提供更细粒度的控制。你还可以使用@Caching注解在同一个方法上组合多个缓存相关的注解。
注意:要避免在同一个方法上同时使用@Cacheable@CachePut,因为这种行为可能会造成混乱。可以将它们结合使用,达到很好的效果,比如,我们看一下下面代码中的传统服务/数据库层次结构:

class UserService {
    // 只缓存unless="#false",满足#false的
    @Cacheable(value = "userCache", unless = "#result != null") //意思是如果是null才缓存
    public User getUserById( long id ) {
        return userRepository.getById( id );
    }
}

class UserRepository {
    @Caching(
        put = {
            @CachePut(value = "userCache", key = "'username:' + #result.username", condition = "#result != null"),
            @CachePut(value = "userCache", key = "#result.id", condition = "#result != null")
        }
    )
    @Transactional(readOnly = true)
    public User getById( long id ) {
        ...
    }
}

在我们的例子中,当调用userService.getUserById时,会使用 id 作为缓存键在缓存中进行查找。如果找不到任何值,则会转为调用userRepository.getById方法。后一种方法不从缓存中查询数据,但是会将数据更新到两个缓存库中,即使该值已经存在于缓存中,也会对其进行更新。

在这种情况下,最重要的是充分利用缓存注解上的条件属性 (conditionunless)。在示例中,仅仅非null的结果会被添加到repository的缓存中。如果没有非null的条件,那么就会有异常出现,因为无法确定#result.username的结果。另一方面,null值只会被存放在service的缓存中。最终结果时,非null值由repository缓存,null由service缓存。这种模式的缺点是缓存逻辑分散在多个bean上。

需要注意的是:如果我们之后要删除service上的condition条件,那么每次非null值的结果都会发生两次相同的缓存放置操作:首先是来自repository的@CachePut,然后是来自service的cache存储(因为service上有@Cacheable注解)。需要注意的是,只有第一次是缓存插入新条目,之后的那次是更新条目。这也许在大多数情况下并不会造成性能问题,但是如果一旦使用了cache replication,也就是cache put和cache update的行为不一致时(比如Ehcache),就会造成一些不可预估的副作用。

如果我们只希望在缓存中查找内容,而不希望缓存任何内容,则可以使用unless ="true"

缓存和事务

对于带事务的缓存,需要特别注意。看如下例子:

class UserRepository {
    @CachePut(value = "userCache", key = "#result.username")
    @Transactional(readOnly = true)
    User getByUsername( String username );

    @CacheEvict(value = “userCache”, key = "#p0.username"),
    @Transactional
    void save( User user );
}

那么我们再包一个外部事务:创建一个User,再获取它:

class UserService {
    @Transactional
    User updateAndRefresh( User user ) {
        userRepository.save(user);
        return userRepository.getByUsername( user.getUsername() );
    }
}

那么,首先会把该用户从缓存中删除,然后立即将其再次存储。但是,假设updateAndRefresh()方法不是事务的结束,并且之后又发生了异常,而异常将导致事务回滚,也就是说实际上该用户并没有被更新到数据库里,但是缓存已经被更新了。会导致系统处于不一致的状态。

解决方法是:可以通过将缓存操作绑定到正在运行的事务来避免此问题,并且仅在事务提交时才执行它们。可以使用 TransactionSynchronizationManager将缓存操作绑定到当前事务,Spring已经有一个 TransactionAwareCacheDecorator 来帮助我们做到这一点,它包装了所有Cache的实现,并确保任何put, evict or clear操作仅仅在成功提交当前事务之后执行(如果没有事务的话就立即执行)。

如果您手动执行缓存操作,则可以从CacheManager获取缓存并使用TransactionAwareCacheDecorator自行封装。

Cache transactionAwareUserCache( CacheManager cacheManager ) {
    return new TransactionAwareCacheDecorator(cacheManager.getCache("userCache"));
}

如果不使用缓存注解的话,那上面的方法是可行的。而如果你想使用缓存注解,并且需要有透明的事务支持,那么应当配置CacheManager以分发可识别事务的缓存。

一些CacheManager实现,例如EhCacheCacheManager,扩展了AbstractTransactionSupportingCacheManager并支持直接分发 事务感知缓存:

@Bean
public CacheManager cacheManager( net.sf.ehcache.CacheManager ehCacheCacheManager ) {
    EhCacheCacheManager cacheManager = new EhCacheCacheManager();
    cacheManager.setCacheManager( ehCacheCacheManager );
    cacheManager.setTransactionAware( true );
    return cacheManager;
}

对于其他的CacheManager实现,比方说SimpleCacheManager,你可以使用一个TransactionAwareCacheManagerProxy。

@Bean
public CacheManager cacheManager() {
   SimpleCacheManager cacheManager = new SimpleCacheManager();
   cacheManager.setCaches(Collections.singletonList(new ConcurrentMapCache(“userCache”)));

   //manually call initialize the caches as our SimpleCacheManager is not declared as a bean
   cacheManager.initializeCaches();
   return new TransactionAwareCacheManagerProxy(cacheManager);
}

TransactionAwareCacheDecorator可能是Spring缓存抽象基础架构中鲜为人知的功能,但这个功能其实是很有用的,特别是对于避免将缓存与事务结合在一起时出现一些非常难以调试的问题非常有用。

使用Spring的缓存注释非常简单,但是有时可能不清楚发生了什么,这可能会导致奇怪的结果。 希望通过这篇文章可以给您一些见识,以便可以轻松避免某些陷阱。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值