diff --git a/build.gradle b/build.gradle
index 0ddf12f22..8251ad57a 100644
--- a/build.gradle
+++ b/build.gradle
@@ -65,9 +65,11 @@ dependencies {
// Testing
testCompile "junit:junit:$junitVersion"
testCompile "org.springframework:spring-test:$springVersion"
+ testCompile "org.springframework:spring-jdbc:$springVersion"
testCompile "org.mockito:mockito-all:$mockitoVersion"
testCompile("javax.annotation:jsr250-api:1.0", optional)
testCompile("com.thoughtworks.xstream:xstream:1.4.4", optional)
+ testCompile("javax.transaction:jta:1.1")
}
sourceCompatibility = 1.6
diff --git a/docs/src/reference/docbook/index.xml b/docs/src/reference/docbook/index.xml
index f319bd27e..33376a1d0 100644
--- a/docs/src/reference/docbook/index.xml
+++ b/docs/src/reference/docbook/index.xml
@@ -21,6 +21,11 @@
Hickey
SpringSource
+
+ Christoph
+ Strobl
+ Pivotal
+
diff --git a/docs/src/reference/docbook/reference/redis.xml b/docs/src/reference/docbook/reference/redis.xml
index 273b647d8..0840f4008 100644
--- a/docs/src/reference/docbook/reference/redis.xml
+++ b/docs/src/reference/docbook/reference/redis.xml
@@ -450,7 +450,7 @@
Support for Spring Cache Abstraction
- Spring Redis provides an implementation for Spring 3.1 cache abstraction
+ Spring Redis provides an implementation for Spring cache abstraction
through the org.springframework.data.redis.cache package. To use Redis as a backing implementation, simply add RedisCacheManager to your configuration:
]]>
+
+
+ By default RedisCacheManager will lazily initialize RedisCache whenever a Cache is requested. This can be changed by predefining a Set of cache names.
+
+
+ By default RedisCacheManager will not participate in any ongoing transaction. Use setTransactionAware to enable transaction support.
+
diff --git a/src/main/java/org/springframework/data/redis/cache/RedisCacheManager.java b/src/main/java/org/springframework/data/redis/cache/RedisCacheManager.java
index 2238dcdde..93831c3b5 100644
--- a/src/main/java/org/springframework/data/redis/cache/RedisCacheManager.java
+++ b/src/main/java/org/springframework/data/redis/cache/RedisCacheManager.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2011-2013 the original author or authors.
+ * Copyright 2011-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -13,65 +13,100 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
package org.springframework.data.redis.cache;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Map;
+import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
+import org.springframework.cache.transaction.AbstractTransactionSupportingCacheManager;
+import org.springframework.cache.transaction.TransactionAwareCacheDecorator;
+import org.springframework.dao.DataAccessException;
+import org.springframework.data.redis.connection.RedisConnection;
+import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
/**
- * CacheManager implementation for Redis. By default saves the keys directly, without appending a prefix (which acts as
- * a namespace). To avoid clashes, it is recommended to change this (by setting 'usePrefix' to 'true'). For performance
- * reasons, the current implementation uses a set for the keys in each cache.
+ * {@link CacheManager} implementation for Redis. By default saves the keys directly, without appending a prefix (which
+ * acts as a namespace). To avoid clashes, it is recommended to change this (by setting 'usePrefix' to 'true').
+ * By default {@link RedisCache}s will be lazily initialized for each {@link #getCache(String)} request unless a set of
+ * predefined cache names is provided.
+ *
+ * Setting {@link #setTransactionAware(boolean)} to {@code true} will force Caches to be decorated as
+ * {@link TransactionAwareCacheDecorator} so values will only be written to the cache after successful commit of
+ * surrounding transaction.
*
* @author Costin Leau
+ * @author Christoph Strobl
+ * @author Thomas Darimont
*/
-public class RedisCacheManager implements CacheManager {
+public class RedisCacheManager extends AbstractTransactionSupportingCacheManager {
- // fast lookup by name map
- private final ConcurrentMap caches = new ConcurrentHashMap();
- private final Collection names = Collections.unmodifiableSet(caches.keySet());
+ private final Log logger = LogFactory.getLog(RedisCacheManager.class);
+
+ @SuppressWarnings("rawtypes")//
private final RedisTemplate template;
private boolean usePrefix = false;
private RedisCachePrefix cachePrefix = new DefaultRedisCachePrefix();
+ private boolean loadRemoteCachesOnStartup = false;
+ private boolean dynamic = true;
// 0 - never expire
private long defaultExpiration = 0;
private Map expires = null;
+ /**
+ * Construct a {@link RedisCacheManager}.
+ *
+ * @param template
+ */
+ @SuppressWarnings("rawtypes")
public RedisCacheManager(RedisTemplate template) {
+ this(template, Collections. emptyList());
+ }
+
+ /**
+ * Construct a static {@link RedisCacheManager}, managing caches for the specified cache names only.
+ *
+ * @param template
+ * @param cacheNames
+ * @since 1.2
+ */
+ @SuppressWarnings("rawtypes")
+ public RedisCacheManager(RedisTemplate template, Collection cacheNames) {
this.template = template;
+ setCacheNames(cacheNames);
}
+ @Override
public Cache getCache(String name) {
- Cache c = caches.get(name);
- if (c == null) {
- long expiration = computeExpiration(name);
- c = new RedisCache(name, (usePrefix ? cachePrefix.prefix(name) : null), template, expiration);
- caches.put(name, c);
+ Cache cache = super.getCache(name);
+ if (cache == null && this.dynamic) {
+ return createAndAddCache(name);
}
- return c;
+ return cache;
}
- private long computeExpiration(String name) {
- Long expiration = null;
- if (expires != null) {
- expiration = expires.get(name);
+ /**
+ * Specify the set of cache names for this CacheManager's 'static' mode.
+ * The number of caches and their names will be fixed after a call to this method, with no creation of further cache
+ * regions at runtime.
+ */
+ public void setCacheNames(Collection cacheNames) {
+
+ if (!CollectionUtils.isEmpty(cacheNames)) {
+ for (String cacheName : cacheNames) {
+ createAndAddCache(cacheName);
+ }
+ this.dynamic = false;
}
- return (expiration != null ? expiration.longValue() : defaultExpiration);
- }
-
- public Collection getCacheNames() {
- return names;
}
public void setUsePrefix(boolean usePrefix) {
@@ -104,4 +139,89 @@ public class RedisCacheManager implements CacheManager {
public void setExpires(Map expires) {
this.expires = (expires != null ? new ConcurrentHashMap(expires) : null);
}
+
+ /**
+ * If set to {@code true} {@link RedisCacheManager} will try to retrieve cache names from redis server using
+ * {@literal KEYS} command and initialize {@link RedisCache} for each of them.
+ *
+ * @param loadRemoteCachesOnStartup
+ * @since 1.2
+ */
+ public void setLoadRemoteCachesOnStartup(boolean loadRemoteCachesOnStartup) {
+ this.loadRemoteCachesOnStartup = loadRemoteCachesOnStartup;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.cache.support.AbstractCacheManager#loadCaches()
+ */
+ @Override
+ protected Collection extends Cache> loadCaches() {
+
+ Assert.notNull(this.template, "A redis template is required in order to interact with data store");
+ return loadRemoteCachesOnStartup ? loadAndInitRemoteCaches() : Collections. emptyList();
+ }
+
+ private Cache createAndAddCache(String cacheName) {
+ addCache(createCache(cacheName));
+ return super.getCache(cacheName);
+ }
+
+ @SuppressWarnings("unchecked")
+ private RedisCache createCache(String cacheName) {
+ long expiration = computeExpiration(cacheName);
+ return new RedisCache(cacheName, (usePrefix ? cachePrefix.prefix(cacheName) : null), template, expiration);
+ }
+
+ private long computeExpiration(String name) {
+ Long expiration = null;
+ if (expires != null) {
+ expiration = expires.get(name);
+ }
+ return (expiration != null ? expiration.longValue() : defaultExpiration);
+ }
+
+ private List loadAndInitRemoteCaches() {
+
+ List caches = new ArrayList();
+
+ try {
+ Set cacheNames = loadRemoteCacheKeys();
+ if (!CollectionUtils.isEmpty(cacheNames)) {
+ for (String cacheName : cacheNames) {
+ if (null == super.getCache(cacheName)) {
+ caches.add(createCache(cacheName));
+ }
+ }
+ }
+ } catch (Exception e) {
+ if(logger.isWarnEnabled()){
+ logger.warn("Failed to initialize cache with remote cache keys.",e);
+ }
+ }
+
+ return caches;
+ }
+
+ @SuppressWarnings("unchecked")
+ private Set loadRemoteCacheKeys() {
+ return (Set) template.execute(new RedisCallback>() {
+
+ @Override
+ public Set doInRedis(RedisConnection connection) throws DataAccessException {
+
+ // we are using the ~keys postfix as defined in RedisCache#setName
+ Set keys = connection.keys(template.getKeySerializer().serialize("*~keys"));
+ Set cacheKeys = new LinkedHashSet();
+
+ if (!CollectionUtils.isEmpty(keys)) {
+ for (byte[] key : keys) {
+ cacheKeys.add(template.getKeySerializer().deserialize(key).toString().replace("~keys", ""));
+ }
+ }
+
+ return cacheKeys;
+ }
+ });
+ }
}
diff --git a/src/test/java/org/springframework/data/redis/cache/RedisCacheManagerUnitTests.java b/src/test/java/org/springframework/data/redis/cache/RedisCacheManagerUnitTests.java
new file mode 100644
index 000000000..38edef84c
--- /dev/null
+++ b/src/test/java/org/springframework/data/redis/cache/RedisCacheManagerUnitTests.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2014 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.redis.cache;
+
+import static org.hamcrest.core.Is.*;
+import static org.hamcrest.core.IsNull.*;
+import static org.hamcrest.core.IsSame.*;
+import static org.junit.Assert.*;
+import static org.mockito.Matchers.*;
+import static org.mockito.Mockito.*;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.internal.matchers.IsCollectionContaining;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.springframework.cache.Cache;
+import org.springframework.data.redis.connection.RedisConnection;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+
+/**
+ * @author Christoph Strobl
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class RedisCacheManagerUnitTests {
+
+ private @Mock RedisConnection redisConnectionMock;
+ private @Mock RedisConnectionFactory redisConnectionFactoryMock;
+
+ @SuppressWarnings("rawtypes")//
+ private RedisTemplate redisTemplate;
+ private RedisCacheManager cacheManager;
+
+ @SuppressWarnings("rawtypes")
+ @Before
+ public void setUp() {
+
+ when(redisConnectionFactoryMock.getConnection()).thenReturn(redisConnectionMock);
+
+ redisTemplate = new RedisTemplate();
+ redisTemplate.setConnectionFactory(redisConnectionFactoryMock);
+ redisTemplate.afterPropertiesSet();
+
+ cacheManager = new RedisCacheManager(redisTemplate);
+ }
+
+ /**
+ * @see DATAREDIS-246
+ */
+ @Test
+ public void testGetCacheReturnsNewCacheWhenRequestedCacheIsNotAvailable() {
+
+ Cache cache = cacheManager.getCache("not-available");
+ assertThat(cache, notNullValue());
+ }
+
+ /**
+ * @see DATAREDIS-246
+ */
+ @Test
+ public void testGetCacheReturnsExistingCacheWhenRequested() {
+
+ Cache cache = cacheManager.getCache("cache");
+ assertThat(cacheManager.getCache("cache"), sameInstance(cache));
+ }
+
+ /**
+ * @see DATAREDIS-246
+ */
+ @Test
+ public void testCacheInitSouldNotRequestRemoteKeysByDefault() {
+
+ cacheManager.afterPropertiesSet();
+ Mockito.verifyZeroInteractions(redisConnectionMock);
+ }
+
+ /**
+ * @see DATAREDIS-246
+ */
+ @Test
+ public void testCacheInitShouldFetchAllCacheKeysWhenLoadingRemoteCachesOnStartupIsEnabled() {
+
+ cacheManager.setLoadRemoteCachesOnStartup(true);
+ cacheManager.afterPropertiesSet();
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(byte[].class);
+ verify(redisConnectionMock, times(1)).keys(captor.capture());
+ assertThat(redisTemplate.getKeySerializer().deserialize(captor.getValue()).toString(), is("*~keys"));
+ }
+
+ /**
+ * @see DATAREDIS-246
+ */
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testCacheInitShouldInitializeRemoteCachesCorrectlyWhenLoadingRemoteCachesOnStartupIsEnabled() {
+
+ Set keys = new HashSet(Arrays.asList(redisTemplate.getKeySerializer()
+ .serialize("remote-cache~keys")));
+ when(redisConnectionMock.keys(any(byte[].class))).thenReturn(keys);
+
+ cacheManager.setLoadRemoteCachesOnStartup(true);
+ cacheManager.afterPropertiesSet();
+
+ assertThat(cacheManager.getCacheNames(), IsCollectionContaining.hasItem("remote-cache"));
+ }
+
+ /**
+ * @see DATAREDIS-246
+ */
+ @Test
+ public void testCacheInitShouldNotInitialzeCachesWhenLoadingRemoteCachesOnStartupIsEnabledAndNoCachesAvailableOnRemoteServer() {
+
+ when(redisConnectionMock.keys(any(byte[].class))).thenReturn(Collections. emptySet());
+ cacheManager.setLoadRemoteCachesOnStartup(true);
+ cacheManager.afterPropertiesSet();
+
+ assertThat(cacheManager.getCacheNames().isEmpty(), is(true));
+ }
+
+ /**
+ * see DATAREDIS-246
+ */
+ @Test
+ public void testCacheManagerShouldNotDynamicallyCreateCachesWhenInStaticMode() {
+
+ cacheManager.setCacheNames(Arrays.asList("spring", "data"));
+ assertThat(cacheManager.getCache("redis"), nullValue());
+ }
+
+ /**
+ * see DATAREDIS-246
+ */
+ @Test
+ public void testCacheManagerShouldRetrunRegisteredCacheWhenInStaticMode() {
+
+ cacheManager.setCacheNames(Arrays.asList("spring", "data"));
+ assertThat(cacheManager.getCache("spring"), notNullValue());
+ }
+
+ /**
+ * see DATAREDIS-246
+ */
+ @Test
+ public void testPuttingCacheManagerIntoStaticModeShouldNotRemoveAlreadyRegisteredCaches() {
+
+ cacheManager.getCache("redis");
+ cacheManager.setCacheNames(Arrays.asList("spring", "data"));
+ assertThat(cacheManager.getCache("redis"), notNullValue());
+ }
+
+}
diff --git a/src/test/java/org/springframework/data/redis/cache/TransactionalRedisCacheManagerTestBase.java b/src/test/java/org/springframework/data/redis/cache/TransactionalRedisCacheManagerTestBase.java
new file mode 100644
index 000000000..508f03007
--- /dev/null
+++ b/src/test/java/org/springframework/data/redis/cache/TransactionalRedisCacheManagerTestBase.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2014 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.redis.cache;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.annotation.Rollback;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * The idea would have been to provide {@link Configuration} here and just set {@link Rollback} accordingly in extending
+ * classes. Unfortunately this does not work when running build with gradle under java6. It was also not possible to add
+ * {@link Configuration} here and import it using {@link ContextConfiguration#classes()}.
+ *
+ * Therefore {@link ContextConfiguration} had to be duplicated in
+ * {@link TransactionalRedisCacheManagerWithCommitUnitTests} and
+ * {@link TransactionalRedisCacheManagerWithRollbackUnitTests}.
+ *
+ * @author Christoph Strobl
+ */
+public abstract class TransactionalRedisCacheManagerTestBase {
+
+ static class FooService {
+
+ private @Autowired BarRepository repo;
+
+ @Transactional
+ public String foo() {
+ return "foo" + repo.bar();
+ }
+ }
+
+ static class BarRepository {
+
+ @Cacheable("bar")
+ public String bar() {
+ return "bar";
+ }
+
+ }
+
+}
diff --git a/src/test/java/org/springframework/data/redis/cache/TransactionalRedisCacheManagerWithCommitUnitTests.java b/src/test/java/org/springframework/data/redis/cache/TransactionalRedisCacheManagerWithCommitUnitTests.java
new file mode 100644
index 000000000..3cd6be73e
--- /dev/null
+++ b/src/test/java/org/springframework/data/redis/cache/TransactionalRedisCacheManagerWithCommitUnitTests.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2014 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.redis.cache;
+
+import static org.mockito.Matchers.*;
+import static org.mockito.Mockito.*;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+
+import javax.sql.DataSource;
+
+import org.hamcrest.core.IsEqual;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cache.CacheManager;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.cache.TransactionalRedisCacheManagerTestBase.BarRepository;
+import org.springframework.data.redis.cache.TransactionalRedisCacheManagerTestBase.FooService;
+import org.springframework.data.redis.connection.RedisConnection;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+import org.springframework.jdbc.datasource.DataSourceTransactionManager;
+import org.springframework.test.annotation.Rollback;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import org.springframework.test.context.transaction.AfterTransaction;
+import org.springframework.test.context.transaction.TransactionConfiguration;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * @author Christoph Strobl
+ */
+@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration
+@Transactional
+@TransactionConfiguration(transactionManager = "transactionManager")
+public class TransactionalRedisCacheManagerWithCommitUnitTests {
+
+ @SuppressWarnings("rawtypes")//
+ protected @Autowired RedisTemplate redisTemplate;
+ protected @Autowired FooService transactionalService;
+
+ @Configuration
+ @EnableCaching
+ public static class Config {
+
+ @Bean
+ public PlatformTransactionManager transactionManager() throws SQLException {
+
+ DataSourceTransactionManager txmgr = new DataSourceTransactionManager();
+ txmgr.setDataSource(dataSource());
+ txmgr.afterPropertiesSet();
+
+ return txmgr;
+ }
+
+ @Bean
+ public DataSource dataSource() throws SQLException {
+
+ DataSource dataSourceMock = mock(DataSource.class);
+ when(dataSourceMock.getConnection()).thenReturn(mock(Connection.class));
+
+ return dataSourceMock;
+ }
+
+ @Bean
+ public CacheManager cacheManager() {
+
+ RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate());
+ cacheManager.setTransactionAware(true);
+ return cacheManager;
+ }
+
+ @SuppressWarnings({ "rawtypes" })
+ @Bean
+ public RedisTemplate redisTemplate() {
+
+ RedisConnection connectionMock = mock(RedisConnection.class);
+ RedisConnectionFactory factoryMock = mock(RedisConnectionFactory.class);
+
+ when(factoryMock.getConnection()).thenReturn(connectionMock);
+
+ RedisTemplate template = new RedisTemplate();
+ template.setConnectionFactory(factoryMock);
+
+ return template;
+ }
+
+ @Bean
+ public FooService fooService() {
+ return new FooService();
+ }
+
+ @Bean
+ public BarRepository barRepository() {
+ return new BarRepository();
+ }
+ }
+
+ @AfterTransaction
+ public void assertThatValuesHaveBeenAddedToRedis() {
+
+ ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(byte[].class);
+ ArgumentCaptor valueCaptor = ArgumentCaptor.forClass(byte[].class);
+
+ verify(redisTemplate.getConnectionFactory().getConnection(), times(1)).zAdd(keyCaptor.capture(), eq(0D),
+ valueCaptor.capture());
+
+ Assert.assertThat(new StringRedisSerializer().deserialize(keyCaptor.getValue()).toString(),
+ IsEqual.equalTo("bar~keys"));
+ }
+
+ /**
+ * @see DATAREDIS-246
+ */
+ @Rollback(false)
+ @Test
+ public void testValuesAddedToCacheWhenTransactionIsCommited() {
+ transactionalService.foo();
+ }
+}
diff --git a/src/test/java/org/springframework/data/redis/cache/TransactionalRedisCacheManagerWithRollbackUnitTests.java b/src/test/java/org/springframework/data/redis/cache/TransactionalRedisCacheManagerWithRollbackUnitTests.java
new file mode 100644
index 000000000..ad32c6252
--- /dev/null
+++ b/src/test/java/org/springframework/data/redis/cache/TransactionalRedisCacheManagerWithRollbackUnitTests.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2014 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.redis.cache;
+
+import static org.mockito.Matchers.*;
+import static org.mockito.Mockito.*;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+
+import javax.sql.DataSource;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cache.CacheManager;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.cache.TransactionalRedisCacheManagerTestBase.BarRepository;
+import org.springframework.data.redis.cache.TransactionalRedisCacheManagerTestBase.FooService;
+import org.springframework.data.redis.connection.RedisConnection;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.jdbc.datasource.DataSourceTransactionManager;
+import org.springframework.test.annotation.Rollback;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import org.springframework.test.context.transaction.AfterTransaction;
+import org.springframework.test.context.transaction.TransactionConfiguration;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * @author Christoph Strobl
+ */
+@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration
+@Transactional
+@TransactionConfiguration(transactionManager = "transactionManager")
+public class TransactionalRedisCacheManagerWithRollbackUnitTests {
+
+ @SuppressWarnings("rawtypes")//
+ protected @Autowired RedisTemplate redisTemplate;
+ protected @Autowired FooService transactionalService;
+
+ @Configuration
+ @EnableCaching
+ public static class Config {
+
+ @Bean
+ public PlatformTransactionManager transactionManager() throws SQLException {
+
+ DataSourceTransactionManager txmgr = new DataSourceTransactionManager();
+ txmgr.setDataSource(dataSource());
+ txmgr.afterPropertiesSet();
+
+ return txmgr;
+ }
+
+ @Bean
+ public DataSource dataSource() throws SQLException {
+
+ DataSource dataSourceMock = mock(DataSource.class);
+ when(dataSourceMock.getConnection()).thenReturn(mock(Connection.class));
+
+ return dataSourceMock;
+ }
+
+ @Bean
+ public CacheManager cacheManager() {
+
+ RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate());
+ cacheManager.setTransactionAware(true);
+ return cacheManager;
+ }
+
+ @SuppressWarnings({ "rawtypes" })
+ @Bean
+ public RedisTemplate redisTemplate() {
+
+ RedisConnection connectionMock = mock(RedisConnection.class);
+ RedisConnectionFactory factoryMock = mock(RedisConnectionFactory.class);
+
+ when(factoryMock.getConnection()).thenReturn(connectionMock);
+
+ RedisTemplate template = new RedisTemplate();
+ template.setConnectionFactory(factoryMock);
+
+ return template;
+ }
+
+ @Bean
+ public FooService fooService() {
+ return new FooService();
+ }
+
+ @Bean
+ public BarRepository barRepository() {
+ return new BarRepository();
+ }
+ }
+
+ @AfterTransaction
+ public void assertThatValuesNeverAddedToRedis() {
+
+ verify(redisTemplate.getConnectionFactory().getConnection(), times(0)).zAdd(any(byte[].class), eq(0D),
+ any(byte[].class));
+ }
+
+ /**
+ * @see DATAREDIS-246
+ */
+ @Rollback(true)
+ @Test
+ public void tesValuesNotAddedToCacheWhenTransactionIsRolledBack() {
+ transactionalService.foo();
+ }
+
+}