transaction이 성공하는 예시
discord 명령어
transaction이 실패하는 예시1
transaction이 실패하는 예시2
정리하자면
트랜잭션 내부에서 완전히 잘못된(사용할 수 없는) 명령어(syntax error)를 사용하면 트랜잭션은 Discard된다.
트랜잭션 내부에서 레디스 자료구조를 잘못 사용한 명령어는 트랜잭션에 영향을 주지 않는다. (다른 명령들은 정상적으로 실행, 반환됩니다.)
SessionCallback
을 사용하는 방법과 @Transactional
어노테이션을 사용하는 방법이있다, 나는 비즈니스 로직 전반적으로 mysql과 redis의 트랜잭션이 동기화되기를 원하고 Spring transaction의 propagation의 이점도 얻어갈 수 있도록 @Transactional
방식을 사용하기로 하였다.Spring Data Redis
에서는 PlatformTransactionManager
가 구현되어 있지 않지만 mysql을 사용하기때문에 이미 PlatformTransactionManager
가 spring boot autoconfiguration의 도움을 받아bean으로 등록되어있다.redis config
@Bean
public RedisTemplate<?, ?> redisTemplate(RedisConnectionFactory redisConnectionFactory){
final RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setHashKeySerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setEnableTransactionSupport(true); // <=
return redisTemplate;
}
redisTemplate.setEnableTransactionSupport(true)
이부분이다.@Transactional
annotation으로 redis transaction을 지원받을 수 있다.test code
RedisTestService
@Service
@Transactional
@RequiredArgsConstructor
public class RedisTestService {
private final RedisTemplate<String, Object> redisTemplate;
public void addStringWithError(String key, Object value) {
redisTemplate.opsForValue().set(key, value);
throw new RuntimeException();
}
public void addString(String key, Object value) {
redisTemplate.opsForValue().set(key, value);
}
public Object getFromString(String key) {
return redisTemplate.opsForValue().get(key);
}
}
RedisTransactionTest
@SpringBootTest
public class RedisTransactionTest {
@Autowired
RedisTestService redisTestService;
@Autowired
RedisTemplate<String, Object> redisTemplate;
@Test
@DisplayName("transaction안에서 exception이 발생한다면 해당 transaction이 discord되는지 확인")
public void rollbackTest () throws Exception
{
String key = "hello";
Assertions.assertThatThrownBy(() ->{
redisTestService.addStringWithError(key, "aaa");
}).isInstanceOf(RuntimeException.class);
Object result = redisTemplate.opsForValue().get(key);
Assertions.assertThat(result).isNull();
}
@Test
@DisplayName("transaction이 올바르게 exec를 실행시키는지 테스트")
public void commitTest () throws Exception
{
String key = "hello1";
String value = "aaa";
redisTestService.addString(key, value);
Object result = redisTemplate.opsForValue().get(key);
Assertions.assertThat(result).isNotNull();
Assertions.assertThat(result.toString()).isEqualTo(value);
}
@Test
@DisplayName("transaction안에서 새로 집어넣은 key에 대해서 조회시 null조회 test")
@Transactional
public void nullTest () throws Exception
{
String key = "test";
String value = "value";
redisTestService.addString(key, value);
Object result = redisTestService.getFromString(key);
Assertions.assertThat(result).isNull();
}
@Test
@DisplayName("transaction안에서 기존에 존재하던 key에 대해서 조회시 not null test")
public void nonNullTest () throws Exception
{
String key = "test";
String value = "value";
redisTestService.addString(key, value);
Object result = redisTestService.getFromString(key);
Assertions.assertThat(result).isNotNull();
Assertions.assertThat(result.toString()).isEqualTo(value);
}
}
rollbackTest
addStringWithError
에서 인위적으로 RuntimeException
을 던져봤고 해당 key가 들어가지 않는 것을 검증commitTest
nullTest
@Transactional
어노테이션을 활용하여 addString
함수와 getFromString
함수를 하나의 transaction으로 묶었다.nonNullTest
@Transaction
을 제거하여 addString
과 getFromString
를 별개의 트랜잭션으로 분류하였고 getFromString
호출시에 addString
의 트랜잭션은 이미 종료되었을 것으로 생각, 해당 key가 이미 redis에 정상적으로 insert된 것을 기대하고 두번째 getFromString
호출, 정상적으로 데이터가 조회되는 것을 확인하였다.Redis Pipeline이란?
executePipelined
//pop a specified number of items from a queue
List<Object> results = stringRedisTemplate.executePipelined(
new RedisCallback<Object>() {
public Object doInRedis(RedisConnection connection) throws DataAccessException {
StringRedisConnection stringRedisConn = (StringRedisConnection)connection;
for(int i=0; i< batchSize; i++) {
stringRedisConn.rPop("myqueue");
}
return null;
}
});
private void upload() {
redisTemplate.executePipelined((RedisCallback<Object>) connection ->{
seasonRepository.findAll().forEach(season -> {
String hashKey = RedisKeyManager.getHashKey(season.getId());
String zSetKey = RedisKeyManager.getZSetKey(season.getId());
rankRepository.findAllBySeasonId(season.getId()).forEach(rank -> {
RankRedis rankRedis = RankRedis.from(rank);
connection.hSet(keySerializer().serialize(hashKey),
hashKeySerializer().serialize(rank.getUser().getId().toString()),
hashValueSerializer().serialize(rankRedis));
if (rank.getWins() + rankRedis.getLosses() != 0){
connection.zAdd(keySerializer().serialize(zSetKey), rank.getPpp(),
valueSerializer().serialize(rank.getUser().getId().toString()));
}
});
});
return null;
});
}