使用spring中的cacheable注解需要注意的点,列举了如下:
对一个方法增加缓存是很简单的一件事,只需要简单加上@Cacheable
注解就OK了。
@Cacheable(value = "reservationsCache")
public List 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 getReservationsForRestaurant( Restaurant restaurant ) {
}
这里使用了 SpEL 语法来定义了一个自定义的cache key:restaurant的id,这是一个简单的Long类型,即便之后Restaurant实体内的数据再多,也不会影响该key。实际上消耗多少内存是取决于VM的,但是不难想象Restaurant实体的数据肯定会比Long多很多数据。在经过这样的cache key的调整优化后,我们项目节省了数百MB的空间,使得能让我们缓存更多有用的数据。
简而言之,不应该仅仅注意cache key的单一性,也应该注意cache key的实际消耗大小。使用key属性或者自定义键生成器,可以对cache key进行更细粒度的控制。
对于非常耗时的方法,你会想到尽可能优化缓存命中率。当方法被多个线程访问到时,理想情况下是第一次访问进行实际的方法内部处理,之后的访问都走缓存。在一个典型的场景下,你会对方法申明synchronized。但是,下面的代码不会做如你预期的事情:
@Cacheable(value = "reservationsCache", key = "#restaurand.id")
public synchronized List getReservationsForRestaurant( Restaurant restaurant ) {
}
在方法上使用@Cacheable
注解时,具体的cache代码会包在方法体的外面(使用AOP),这意味着在缓存查找后,在方法内部或者方法本身 任何形式的同步都会发生。也就是说,在调用该方法时,会首先进行缓存的查找,如果没有命中缓存,那么就会锁上,方法开始执行。那么想象这样一种场景,当很多个相同请求同时来临,那么所有缓存都不会命中,然后上锁,方法一个接一个执行,方法执行完才会将结果存到cache里。
对于这种问题,解决方案是使用双重检查锁定的手动缓存,或者将synchronization包到cacheable方法外面。对于后者,会需要根据你具体的AOP代理策略增加一个额外的bean。
在Spring框架的4.3版本里,提供了对于同步缓存的支持:可以在@Cacheable
里定义sync属性,来确保只有一个线程能够构造缓存的值,如下面的例子所示:
@Cacheable(value = "reservationsCache", key = "#restaurand.id", sync = true)
public List getReservationsForRestaurant( Restaurant restaurant ) {
}
使用@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
方法。后一种方法不从缓存中查询数据,但是会将数据更新到两个缓存库中,即使该值已经存在于缓存中,也会对其进行更新。
在这种情况下,最重要的是充分利用缓存注解上的条件属性 (condition
和unless
)。在示例中,仅仅非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的缓存注释非常简单,但是有时可能不清楚发生了什么,这可能会导致奇怪的结果。 希望通过这篇文章可以给您一些见识,以便可以轻松避免某些陷阱。