Reputation: 2610
I'm trying to understand how Spring's caching works, especially together with transactions and more threads.
Let's have a service caching its results
public class ServiceWithCaching {
@Cacheable(value="my-cache")
public String find() {
...load from DB
}
@CacheEvict(value="my-cache", allEntries=true)
public void save(String value) {
...save to DB
}
}
Now consider a test that runs two parallel threads. One of them use a transaction to save a value, the second one reads a value.
service.save("initial"); // initial state
assert service.find() == "initial"; // load cache
CountDownLatch latch = new CountDownLatch(1);
Thread saveThread = new Thread(() -> {
TransactionDefinition transactionDefinition = new DefaultTransactionDefinition();
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager, transactionDefinition);
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
service.save("test"); // evict cache
latch.await();
}
});
});
saveThread.start();
Thread readThread = new Thread(() -> {
service.find(); // load cache
latch.countDown();
});
readThread.start();
saveThread.join();
assert service.find() == "test";
The assert fails because service.find()
returns "initial". This is because the second thread loads previously evicted cache before the first thread commit the transaction.
The result is:
Is there any Spring-way how to solve this problem?
Upvotes: 3
Views: 2259
Reputation: 7991
Well, after reviewing your code above, it would seem correct, but there are few subtleties related to thread timing that I believe is causing your test to fail. I.e. your test has possible race condition(s) (e.g. check-then-act
) despite your attempt to coordinate the threads (i.e. read
, save
and main
) properly.
Technically, and specifically, your thread coordination logic does not guarantee that the interleaving of the threads' actions by the JRE (combined with the OS thread scheduler) will lead to the expected result.
Consider the following...
Let:
R == Reader Thread
S == Save Thread
M == Main Thread
Then the following interleaving of thread operations is possible:
T0. M @ S.start()
T1. M @ R.start()
T2. S @ transactionTemplate.execute() // Starts a (local) Transaction context
T3. S @ txCallback.doInTransactionWithoutResult()
T4. S @ cache.evict() // Evicts all entries
T5. S @ service.save("test")
T6. S @ db.insert(..) // block call to the DB
T7. R @ server.find()
T8. R @ cache.get() // results in cache miss due to eviction in T4
T9. R @ db.load(key) // loads "initial" since TX in T6 has not committed yet
T10. R @ cache.put(key, "initial");
T11. R @ latch.countDown()
T12. S @ db.insert(..) // returns updateCount == 1
T13. S @ tx.commit();
T14. S @ latch.await(); // proceeds
T15. M @ saveThread.join() // waits for S to terminate, then...
T16. M @ assert service.find() == "test" // cache hit; returns "initial"; assertion fails.
First, as you know, Thread.start()
does not cause a thread to run. start()
signals to the runtime that the thread is "ready" to be scheduled and ran by the OS. You can manipulate thread priorities, but that is not going to help much, nor solve your race condition.
Second, you may be able to fix your test by switching the latch.await()
call and latch.countDown()
in your reader and save threads like so...
Thread saveThread = new Thread(() -> {
...
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
service.save("test"); // evict all entries in cache
latch.countDown();
}
});
});
And then...
Thread readThread = new Thread(() -> {
latch.await();
service.find();
});
readThread.join();
However, since you pre-load the cache before starting any threads...
service.save("initial"); // initial state
assert service.find() == "initial"; // load cache
And then proceed to call service.find()
after the saveThread
terminates, there is not really any point for the readThread
since the main
thread can serve as the "reader" thread. So then...
saveThread.join();
assert service.find() == "test";
Again, I am not 100% certain this is exactly what happening in your case, but it is possible.
I have coded a similar test (based on your test code above) here. There are a few differences.
First, I made use of a simple, but elegant concurrent testing framework called MultithreadedTC in order to maintain exact and precise control over the threads.
Second, I used Spring's @Transactional
annotation support rather than programmatic transaction management as you have done in your test.
Finally, I used an embedded HSQL database (DataSource
) along with the DataSourcePlatformTransactionManager
to test the transactional behavior in a caching context. The SQL initialization scripts are here (schema) and here (data).
Be sure to declare the appropriate dependencies on your classpath if you run this test.
This test passed as expected, so I would say Spring's Cache Abstraction functions correctly in the context of caching, providing things are coordinated properly between multiple threads.
There are few other things to keep in mind.
The @CacheEvict
annotation is a post-method invocation operation (i.e. "after" AOP advice, which is "default" behavior) meaning it will evict entries from the cache only upon successful execution of the method. You can change this behavior by specifying the beforeInvocation
attribute on the @CacheEvict
annotation.
When combining multiple types of advice to a application service method (e.g. Transactional or Caching) you may need to specify the order in which the advice executes to achieve proper application behavior.
Keep in mind if multiple threads are calling the same @Cacheable
method, you may need to properly synchronize the operation using the sync
attribute (see here for more details). If you need to coordinate between multiple cache-based operations (for example an @Cacheable
method and @CacheEvict
method) that may be called concurrently, then you will need to synchronize the methods using service object's monitor.
Let's see, what else???
Hope this helps!
-John
Upvotes: 3