Cache operations within the same transaction should be visible.

https://github.com/zhangheng0027/testCacheTransaction.git com.example.testcachetransaction.TestCacheTransactionApplicationTests#test02


@Log4j2
@CacheConfig(cacheNames = "cacheTransaction")
@Component
public class CacheTransaction {

    private static final AtomicInteger ai = new AtomicInteger(0);

    @Lazy
    @Resource
    CacheTransaction self;


    @Cacheable(key = "#s")
    public String cacheStr(String s) {
        return s + ai.incrementAndGet();
    }

    @Transactional
    public String cacheTransaction(String s) {
        String s1 = self.cacheStr(s);
        log.info("first cache: {}", s1);
        String s2 = self.cacheStr(s);
        log.info("second cache: {}", s2);

        return s2;
    }

}

    @Test
    public void test02() {
        Objects.requireNonNull(redisTemplate.getConnectionFactory()).getConnection().serverCommands().flushDb();
        cacheTransaction.cacheTransaction("abc");
        System.out.println(cacheTransaction.cacheStr("abc"));
    }

result:

2023-09-27T11:36:30.320+08:00  INFO 25076 --- [    Test worker] c.e.t.CacheTransaction                   : first cache: abc1
2023-09-27T11:36:30.323+08:00  INFO 25076 --- [    Test worker] c.e.t.CacheTransaction                   : second cache: abc2

Comment From: me0106

This is actually the operation of two data sources: jdbc and redis. Jdbc transactions do not guarantee repeatable reads of Redis data. If you need to take a snapshot of the cache, it should be saved to a local variable at the beginning of the transaction.

Comment From: zhangheng0027

This is actually the operation of two data sources: jdbc and redis. Jdbc transactions do not guarantee repeatable reads of Redis data. If you need to take a snapshot of the cache, it should be saved to a local variable at the beginning of the transaction.

But spring provides transaction support for cache, see org.springframework.cache.transaction.TransactionAwareCacheDecorator

Comment From: me0106

This is actually the operation of two data sources: jdbc and redis. Jdbc transactions do not guarantee repeatable reads of Redis data. If you need to take a snapshot of the cache, it should be saved to a local variable at the beginning of the transaction.

But spring provides transaction support for cache, see org.springframework.cache.transaction.TransactionAwareCacheDecorator

you are right. TransactionAwareCacheDecorator will delay caching operations until the transaction is completed. If there is no corresponding key in the cache,that's actually calling the real method. Before the transaction is completed All caches will not take effect. The reason for returning different values each time is that the 'cacheStr' method has side effects.

The cache is not visible because there is no cache at all.

Comment From: zhangheng0027

This is actually the operation of two data sources: jdbc and redis. Jdbc transactions do not guarantee repeatable reads of Redis data. If you need to take a snapshot of the cache, it should be saved to a local variable at the beginning of the transaction.

But spring provides transaction support for cache, see org.springframework.cache.transaction.TransactionAwareCacheDecorator

you are right. TransactionAwareCacheDecorator will delay caching operations until the transaction is completed. If there is no corresponding key in the cache,that's actually calling the real method. Before the transaction is completed All caches will not take effect. The reason for returning different values each time is that the 'cacheStr' method has side effects.

The cache is not visible because there is no cache at all.

Yes, it will be cached when the transaction is committed, which ensures isolation between different transactions, but the same transaction should not be isolated.

The following is my local solution, but I did not consider the propagation of transactions.


public class TransactionAwareCacheDecoratorIH extends TransactionAwareCacheDecorator {

    private static final ThreadLocal<Map<Object, TCache>> transactionCache = new ThreadLocal<>();

    public TransactionAwareCacheDecoratorIH(Cache targetCache) {
        super(targetCache);
    }

    protected static void init() {
        if (TransactionSynchronizationManager.isSynchronizationActive()) {
            Map<Object, TCache> map = transactionCache.get();
            if (map == null) {
                TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                    @Override
                    public void afterCompletion(int status) {
                        transactionCache.remove();
                    }
                });
                map = new HashMap<>();
                transactionCache.set(map);
            }
        }
    }

    protected static TCache getTransactionCache(String key) {
        if (TransactionSynchronizationManager.isSynchronizationActive()) {
            init();
            return transactionCache.get().get(key);
        }
        return null;
    }

    protected void putInTransactionCache(Object key, String op, Object value) {
        if (TransactionSynchronizationManager.isSynchronizationActive()) {
            init();
            TCache tCache = new TCache(op);
            tCache.setValue(value);
            transactionCache.get().put(key, tCache);
        }
    }

    protected void clearTransactionCache() {
        if (TransactionSynchronizationManager.isSynchronizationActive()) {
            init();
            transactionCache.get().clear();
            transactionCache.get().put("clear::" + this.getName(), new TCache("clear"));
        }
    }


    @Override
    @Nullable
    public ValueWrapper get(Object key) {
        TCache byInTransactionCache = getTransactionCache(String.valueOf(key));
        if (null == byInTransactionCache)
            return super.get(key);
        if ("put".equals(byInTransactionCache.getType()))
            return new SimpleValueWrapper(byInTransactionCache.getValue());
        return null;
    }

    @Override
    public <T> T get(Object key, @Nullable Class<T> type) {
        ValueWrapper valueWrapper = get(key);
        if (null == valueWrapper)
            return null;
        return (T) valueWrapper.get();
    }

    @Override
    @Nullable
    @SneakyThrows
    public <T> T get(Object key, Callable<T> valueLoader) {
        ValueWrapper valueWrapper = get(key);
        if (null != valueWrapper)
            return (T) valueWrapper.get();
        return valueLoader.call();
    }

    @Override
    public void put(final Object key, @Nullable final Object value) {
        if (TransactionSynchronizationManager.isSynchronizationActive()) {
            putInTransactionCache(key, "put", value);
        }
        super.put(key, value);
    }

    @Override
    public void evict(final Object key) {
        if (TransactionSynchronizationManager.isSynchronizationActive()) {
            putInTransactionCache(key, "evict", null);
        }
        super.evict(key);
    }

    @Override
    public void clear() {
        if (TransactionSynchronizationManager.isSynchronizationActive()) {
            clearTransactionCache();
        }
        super.clear();
    }

    @Getter
    static public class TCache {

        /** 操作类型 **/
        String type;

        @Getter
        @Setter
        Object value;

        TCache(String type) {
            this.type = type;
        }

    }

}

Comment From: snicoll

@me0106 thanks for helping.

@zhangheng0027 unfortunately, you can't expect that level of support for TransactionAwareCacheDecorator, especially with two transactional resources.

Comment From: zhangheng0027

@me0106 thanks for helping.

@zhangheng0027 unfortunately, you can't expect that level of support for TransactionAwareCacheDecorator, especially with two transactional resources.

oh,no. This is one transaction, not two transactions

Comment From: zhangheng0027

@me0106 thanks for helping.

@zhangheng0027 unfortunately, you can't expect that level of support for TransactionAwareCacheDecorator, especially with two transactional resources.

I can provide another example to illustrate the inconsistency between cache and db

Comment From: zhangheng0027

@me0106 thanks for helping. @zhangheng0027 unfortunately, you can't expect that level of support for TransactionAwareCacheDecorator, especially with two transactional resources.

I can provide another example to illustrate the inconsistency between cache and db

https://github.com/zhangheng0027/testCacheTransaction.git com.example.testcachetransaction.service.impl.TUserServiceImplTest#test01

@Test
    public void test01() {

        TUser user = new TUser();
        user.setAge(15);
        user.setName("Tom");
        tUserService.save(user);

        // load to cache
        tUserService.getUser(user.getName());

        user.setAge(16);
        tUserService.updateAndGetUser(user);

    }

@Log4j2
@Service
@CacheConfig(cacheNames = "t_user")
public class TUserServiceImpl extends ServiceImpl<TUserMapper, TUser>
    implements TUserService {


    @Lazy
    @Resource
    TUserServiceImpl self;

    @Override
    @Cacheable(key = "#name")
    public TUser getUser(String name) {
        return lambdaQuery().eq(TUser::getName, name).last("limit 1").one();
    }

    @Override
    @CachePut
    public TUser updateUser(TUser user) {
        lambdaUpdate().set(TUser::getAge, user.getAge())
                .eq(TUser::getName, user.getName()).update();
        return user;
    }

    @Override
    @Transactional
    public TUser updateAndGetUser(TUser user) {
        TUser user1 = self.getUser(user.getName());
        log.info("update before user: {}", user1);

        self.updateUser(user);
        TUser user2 = self.getUser(user.getName());
        log.info("update after user: {}", user2);

        TUser one = lambdaQuery().eq(TUser::getName, user.getName()).last("limit 1").one();
        log.info("db user: {}", one);

        return user2;
    }


}

result

[    Test worker] c.e.t.service.impl.TUserServiceImpl : update before user: TUser [Hash = 85700, age=15, name=Tom]
[    Test worker] c.e.t.service.impl.TUserServiceImpl : update after user: TUser [Hash = 85700, age=15, name=Tom]
[    Test worker] c.e.t.service.impl.TUserServiceImpl : db user: TUser [Hash = 85731, age=16, name=Tom]

Comment From: zhangheng0027

@me0106 thanks for helping. @zhangheng0027 unfortunately, you can't expect that level of support for TransactionAwareCacheDecorator, especially with two transactional resources.

I can provide another example to illustrate the inconsistency between cache and db

https://github.com/zhangheng0027/testCacheTransaction.git com.example.testcachetransaction.service.impl.TUserServiceImplTest#test01

```java @Test public void test01() {

    TUser user = new TUser();
    user.setAge(15);
    user.setName("Tom");
    tUserService.save(user);

    // load to cache
    tUserService.getUser(user.getName());

    user.setAge(16);
    tUserService.updateAndGetUser(user);

}

```

```java @Log4j2 @Service @CacheConfig(cacheNames = "t_user") public class TUserServiceImpl extends ServiceImpl implements TUserService {

@Lazy
@Resource
TUserServiceImpl self;

@Override
@Cacheable(key = "#name")
public TUser getUser(String name) {
    return lambdaQuery().eq(TUser::getName, name).last("limit 1").one();
}

@Override
@CachePut
public TUser updateUser(TUser user) {
    lambdaUpdate().set(TUser::getAge, user.getAge())
            .eq(TUser::getName, user.getName()).update();
    return user;
}

@Override
@Transactional
public TUser updateAndGetUser(TUser user) {
    TUser user1 = self.getUser(user.getName());
    log.info("update before user: {}", user1);

    self.updateUser(user);
    TUser user2 = self.getUser(user.getName());
    log.info("update after user: {}", user2);

    TUser one = lambdaQuery().eq(TUser::getName, user.getName()).last("limit 1").one();
    log.info("db user: {}", one);

    return user2;
}

} ```

result

[ Test worker] c.e.t.service.impl.TUserServiceImpl : update before user: TUser [Hash = 85700, age=15, name=Tom] [ Test worker] c.e.t.service.impl.TUserServiceImpl : update after user: TUser [Hash = 85700, age=15, name=Tom] [ Test worker] c.e.t.service.impl.TUserServiceImpl : db user: TUser [Hash = 85731, age=16, name=Tom]

@snicoll Look at this example. This is a bug, open the issue please

Comment From: snicoll

As said before, you shouldn't expect changes during the transaction to be visible. TransactionAwareCacheDecorator is best efforts and will delay cache operations. If you need to get consistent cached data that is backed by a transactional data source, you need to use a second level cache.

Comment From: zhangheng0027

@snicoll The four basic principles of transactions, ACID, require transactions to ensure consistency. This bug violates the consistency of transactions.

Comment From: me0106

@snicoll The four basic principles of transactions, ACID, require transactions to ensure consistency. This bug violates the consistency of transactions.

ACID is only guaranteed in the same transaction within the same data source

Comment From: zhangheng0027

```java @Override @Transactional public TUser updateAndGetUser(TUser user) { TUser user1 = self.getUser(user.getName()); // output the value before the update log.info("update before user: {}", user1);

    // update database and cache
    self.updateUser(user);
    TUser user2 = self.getUser(user.getName());

    // output cached values after update
    log.info("update after user: {}", user2);

    TUser one = lambdaQuery().eq(TUser::getName, user.getName()).last("limit 1").one();
    // output db values after update
    log.info("db user: {}", one);

    return user2;
}

```

@snicoll The four basic principles of transactions, ACID, require transactions to ensure consistency. This bug violates the consistency of transactions.

ACID is only guaranteed in the same transaction within the same data source

Looking at my example, using @Transactional to start a transaction means that the entire method should be within the same transactional . You can predict log.info("update after user: {}", user2); result. It's different from our prediction.

The solution is to add cache within the current transaction, Record the modifications made to the transaction before this.

Comment From: me0106

@Transactional is only valid for Jdbc Transaction. SpringCache never guarantees the visibility of cache operations within a transaction, but simply delays cache operations until after the transaction is completed.

Spring Cache operations within the same transaction are not visible

As described in the comments, SpringCache even allows you to directly modify the cache. :)