一个声明式事务和缓存的坑

由于某些原因,没有直接使用spring的申明式缓存,而是用了自己的缓存,例如这个系统的空间缓存,然而却遇到了一些问题:

假设有以下方法:

@Transactional(propagation = Propagation.REQUIRED)
public void addSpace(Space space) throws LogicException {
	spaceDao.insert(space);
	spaceCache.reloadCache();
	logic();
}
public void reloadCache(){
	List<Space> spaces = spaceDao.selectByParam(new SpaceQueryParam());
	cache.clear();
	cache.addAll(spaces);
}

咋一看是没有问题的,但是当logic()方法失败之后,整个add事务将会被回滚,此时reloadCache查询的数据依旧是回滚之前的数据,这就导致了缓存中的数据和实际数据不一致的问题(曾经遇到过,然而还是吃了泡狗屎)。 为了解决这个问题,需要在事务提交成功之后再刷新缓存。

通过查看文档可以知道,当当前事务存在的情况下利用

TransactionSynchronizationManager.registerSynchronization(TransactionSynchronization synchronization)

可以注册一个事务回调对象,然后根据status判断是否提交成功,例如:

TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {

	@Override
	public void afterCompletion(int status) {
		if (status == TransactionSynchronization.STATUS_COMMITTED) {
        		reloadCache();
		}
	}

});

但是这里面还有一个坑,TransactionSynchronization的文档中明确提到了:

NOTE: The transaction will have been committed or rolled back already, but the transactional resources might still be active and accessible. As a consequence, any data access code triggered at this point will still “participate” in the original transaction, allowing to perform some cleanup (with no commit following anymore!), unless it explicitly declares that it needs to run in a separate transaction. Hence: Use PROPAGATION_REQUIRES_NEW for any transactional operation that is called from here.

也就是说reloadCache需要新开一个事务查询数据,如果还是依旧采用老的事务,那么会有Connection is closed异常,另外,我曾错误的认为

TransactionTemplate template = new TransactionTemplate(platformTransactionManager, definition);
	return template.execute(new TransactionCallback<T>() {

		@Override
		public T doInTransaction(TransactionStatus status) {
			return function.apply(status);
		}
	});

会开一个新的事务,然而查看源码之后,发现它仍然会采用当前事务,除非显示的指定了新事务:

DefaultTransactionDefinition dtd = new DefaultTransactionDefinition();
dtd.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
TransactionTemplate template = new TransactionTemplate(platformTransactionManager, dtd);

其实这也解决不了缓存不一致的问题,例如我清空了缓存,此时一个读线程开始重新加载数据,从数据库中取出数据之后还未放入缓存中,此时另一个写操作很快的完成了事务, 并且清空了缓存(其实上一个写操作已经清空了缓存),读线程此时开始将数据放入缓存中,但此时缓存中的数据并不是写操作之后的数据,而是上一个写操作之后的数据, 此时再有线程过来查询数据,发现缓存中已经存在了数据,直接返回了该数据。 具体可见:

http://coolshell.cn/articles/17416.html

然而,在测试的过程中发现(我采用的是Caffeine缓存),缓存的get和invalidate(清空缓存操作),是锁桶的,也就是说,我在查询数据的过程中清空缓存不会立即生效,而是等待查询完成之后才会生效, 此时用户看到的虽然是旧数据,但缓存中的数据其实已经被清空了,除非时刻发生上述操作,否则下次从缓存取数据还是取的最新数据。

当然,这只是用CacheLoader来做缓存的一种表现,如果用其他方式缓存,仍然需要面对这个问题。