Spring Version: 5.1.7.RELEASE
When Spring transactions are enabled, the Cache
operation is proxied by TransactionAwareCacheDecorator
. Only the get()
operation is invoked directly. The put()
, evict()
, and clear()
operations will be delayed until after the transaction has been committed (via a custom TransactionSynchronizationAdapter.afterCommit()
callback).
When username='zhangsan'
is cached, the updateAndRefresh()
method will return the old cached value because userRepository.getByUsername()
is executed before the transaction is committed.
Even if beforeInvocation=true
, CacheAspectSupport.processCacheEvicts()
also only sets cache operation to ThreadLocal
.
Is there is a bug for @CacheEvict(beforeInvocation = true, ...)
when executing within a transaction?
class UserRepository {
@Cacheable(value = "userCache", key = "#username")
User getByUsername( String username );
@CacheEvict(value = “userCache”, key = "#user.username", beforeInvocation = true)
void save( User user );
}
class UserService {
@Transactional
User updateAndRefresh( User user ) {
userRepository.save(user);
return userRepository.getByUsername( user.getUsername() );
}
}
CacheAspectSupport.java
private void performCacheEvict(
CacheOperationContext context, CacheEvictOperation operation, @Nullable Object result) {
Object key = null;
for (Cache cache : context.getCaches()) {
if (operation.isCacheWide()) {
logInvalidating(context, operation, null);
doClear(cache);
}
else {
if (key == null) {
key = generateKey(context, result);
}
logInvalidating(context, operation, key);
doEvict(cache, key);
}
}
}
TransactionAwareCacheDecorator.java
@Override
public void evict(final Object key) {
if (TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
TransactionAwareCacheDecorator.this.targetCache.evict(key);
}
});
}
else {
this.targetCache.evict(key);
}
}
Comment From: sbrannen
Good catch!
Is there is a bug for
@CacheEvict(beforeInvocation = true, ...)
when executing within a transaction?
According to its class-level Javadoc, I would say that TransactionAwareCacheDecorator
behaves as expected:
Cache decorator which synchronizes its
put
,evict
andclear
operations with Spring-managed transactions (through Spring'sTransactionSynchronizationManager
, performing the actual cache put/evict/clear operation only in the after-commit phase of a successful transaction.
However, there is definitely a conflict here with the documentation for @CacheEvict(beforeInvocation = true, ...)
, which states:
Whether the eviction should occur before the method is invoked.
Setting this attribute to
true
, causes the eviction to occur irrespective of the method outcome (i.e., whether it threw an exception or not).
Unfortunately, it does not appear that it would be easy to make that work within a transaction given the current API for org.springframework.cache.Cache.evict(Object)
. Specifically, there is no way to propagate the beforeInvocation = true
semantics to the Cache
implementation via the current evict(Object)
method, and that's basically what TransactionAwareCacheDecorator
would need in order to know that it should perform the eviction immediately.
@snicoll, what are your thoughts on the matter?
Comment From: wusthuke
I tried to resolved follow, record Evict beforeInvocation to ThreadLocal :
EvictCacheThreadLocalContext.java
package org.springframework.cache;
public class EvictCacheThreadLocalContext {
private static final ThreadLocal<Boolean> evictBeforeInvocation = ThreadLocal.withInitial(() -> false);
public static boolean isEvictBeforeInvocation() {
return evictBeforeInvocation.get();
}
public static void setEvictBeforeInvocation(boolean beforeInvocation) {
evictBeforeInvocation.set(beforeInvocation);
}
public static void remove() {
evictBeforeInvocation.remove();
}
}
CacheAspectSupport.java
private void performCacheEvict(
CacheOperationContext context, CacheEvictOperation operation, @Nullable Object result) {
Object key = null;
for (Cache cache : context.getCaches()) {
if (operation.isCacheWide()) {
logInvalidating(context, operation, null);
doClear(cache);
} else {
if (key == null) {
key = generateKey(context, result);
}
logInvalidating(context, operation, key);
try {
EvictCacheThreadLocalContext.setEvictBeforeInvocation(operation.isBeforeInvocation());
doEvict(cache, key);
} finally {
EvictCacheThreadLocalContext.remove();
}
}
}
}
TransactionAwareCacheDecorator.java
@Override
public void evict(final Object key) {
if (EvictCacheThreadLocalContext.isEvictBeforeInvocation()) {
this.targetCache.evict(key);
return;
}
if (TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
TransactionAwareCacheDecorator.this.targetCache.evict(key);
}
});
} else {
this.targetCache.evict(key);
}
}
Comment From: sbrannen
Thanks for the example workaround, @wusthuke.
A ThreadLocal
should work for propagating that boolean flag, but I'm not sure that we want to go that route in terms of the implementation.
We will think it over.
Comment From: jhoeller
After some back and forth, I've introduced a new boolean evictIfPresent(Object key)
operation on the Cache
abstraction which implies immediate execution semantics and provides an indication for whether the key was present (as supported by all cache providers, so this is also worth exposing for sophisticated programmatic interaction purposes).
In case of @CacheEvict.beforeInvocation=true
, the cache interceptor delegates to evictIfPresent
now: not using the returned indication but relying on immediate execution semantics (in particular the explicit pass-through in TransactionAwareCacheDecorator
).
Comment From: snicoll
I've introduced a new boolean evictIfPresent(Object key) operation on the Cache abstraction which implies immediate execution semantics and provides an indication for whether the key was present
@jhoeller I don't see any commit linked to this issue and it is still open. Am I missing something?
Comment From: sbrannen
Juergen hasn't committed that change yet.
During the team call yesterday, we discussed that the proposal likely would not cover the case when allEntries
and beforeInvocation
are both set to true
. So, it's my understanding that Juergen is further investigating the issue.
Comment From: wusthuke
Juergen hasn't committed that change yet.
During the team call yesterday, we discussed that the proposal likely would not cover the case when
allEntries
andbeforeInvocation
are both set totrue
. So, it's my understanding that Juergen is further investigating the issue.
@sbrannen I verified 5.2.0.RELEASE, and found that this implementation has a little problem. when cache is empty and database exists, after UserService.update(id, userInfo)
, cache will stored old userInfo
AbstractCacheInvoker my modify as
if (immediate) {
cache.evictIfPresent(key);
cache.evict(key);
}