Spring Data redis
1 2 3
| dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-redis' }
|
application.properties 에 아래와 같이 설정 한 후 Bean 객체 생성이 필요하다.
1 2 3
| redis.host=localhost redis.port=6379 redis.timeout=0
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Slf4j @Configuration public class RedisConfig extends CachingConfigurerSupport { @Value("${redis.host}") private String host; @Value("${redis.port}") private int port;
@Bean public RedisConnectionFactory redisConnectionFactory() { return new LettuceConnectionFactory(host, port); } }
|
redis 서버와 연결을 위한 객체를 생성하는 RedisConnectionFactory,
Spring Data Redis 프로젝트를 사용해서 CrudRepository 구현체를 통해
redis 에 저장할 도메인 객체를 정의할 수 있다.
아래와 같이 @RedisHash 어노테이션을 사용한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @AllArgsConstructor @ToString @RedisHash("point") public class Point { @Id private String id; private Long amount; private LocalDateTime refreshTime;
public void refresh(Long amount, LocalDateTime refreshTime) { if (refreshTime.isAfter(this.refreshTime)) { this.amount = amount; this.refreshTime = refreshTime; } } }
|
1 2
| public interface PointRedisRepository extends CrudRepository<Point, String> { }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| @Slf4j @RestController @RequestMapping("/repository") @RequiredArgsConstructor public class RedisRepositoryController {
private final PointRedisRepository repository;
@GetMapping("/{id}") public Point getPointById(@PathVariable String id) { return repository.findById(id) .orElseThrow(() -> new IllegalArgumentException("invalid id:" + id)); }
@PostMapping public Point addPoint(@RequestBody Point point) { return repository.save(point); }
@DeleteMapping("/{id}") public void removePoint(@PathVariable String id) { repository.deleteById(id); }
@PatchMapping("/{id}") public Point updatePoint(@PathVariable String id, @RequestBody Point point) { Point entity = repository.findById(id) .orElseThrow(() -> new IllegalArgumentException("invalid id:" + id)); entity.update(point); entity= repository.save(entity); return entity; } }
|
CrudRepository 통해 객체타입의 데이터를 redis 에 저장하면 hash와 set 데이터구조로 관리한다.
아래와 같이 curl 로 2개의 point 를 저장하고 redis-cli 로 내부값을 살펴보면
2개의 hash 타입 데이터, 1개의 set 타입 데이터가 저장된 것을 확인할 수 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| curl --location 'http://127.0.0.1:8080/repository' \ --header 'Content-Type: application/json' \ --data '{ "id": "1", "amount": "1000", "refreshTime": "2023-03-22T11:11:11.000" }'
curl --location 'http://127.0.0.1:8080/repository' \ --header 'Content-Type: application/json' \ --data '{ "id": "2", "amount": "2000", "refreshTime": "2023-03-22T22:22:22.000" }'
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| $ redis-cli
127.0.0.1:6379> keys *
127.0.0.1:6379> smembers point
127.0.0.1:6379> HGETALL point:1
127.0.0.1:6379> HGETALL point:2
|
RedisTemplate
위에서 생성했던 RedisConnectionFactory 빈 객체를 주입해서 RedisTemplate 구성할 수 있다.
1 2 3 4 5 6
| @Bean public StringRedisTemplate stringRedisTemplate(@Autowired RedisConnectionFactory redisConnectionFactory) { StringRedisTemplate stringRedisTemplate = new StringRedisTemplate(); stringRedisTemplate.setConnectionFactory(redisConnectionFactory); return stringRedisTemplate; }
|
범용적으로 사용하기 위해 객체를 json 으로 직렬화 저장하는 경우가 많은데, 이 때 StringRedisTemplate 을 사용하면 좋다.
테스트를 위한 컨트롤러를 생성
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| @Slf4j @Controller @RequestMapping("/template") public class RedisTemplateController { @Autowired StringRedisTemplate stringRedisTemplate;
@GetMapping("/sample") public void sample(@RequestParam(required = false) String key, Model model) { log.info("sample invoked"); Set<String> keys; if (StringUtils.hasText(key)){ keys = stringRedisTemplate.keys(key); } else { keys= stringRedisTemplate.keys("*"); } model.addAttribute("keys", new ArrayList<>(keys)); }
@GetMapping("/insert") public String insert(@RequestParam String key, @RequestParam String value) { log.info("insert invoked, {}:{}", key, value); stringRedisTemplate.opsForValue().set(key, value); stringRedisTemplate.opsForValue().get(key); return "redirect:/template/sample"; }
@GetMapping("/deleteAll") public String deleteAll(Model model) { log.info("deleteAll called()...."); stringRedisTemplate.delete(stringRedisTemplate.keys("*")); return "redirect:/template/sample"; } }
|
CrudRepository를 사용해서 redis에 데이터를 삽입해도 되지만 StringRedisTemplate 혹은 그냥 RedisTemplate의 메서드로도 충분이 삽입 가능하다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <!DOCTYPE html> <html class="no-js" lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> sample_radis
<form action="/redis/insert"> key: <input type="text" name="key" > value: <input type="text" name="value"> <button type="submit">submit</button> </form> <button onclick="location.href='/redis/deleteAll'">delete all</button>
<p th:each="p:${keys}">[[${p}]]</p> </body> </html>
|
지금까지 저장된 키 목록이 출력된다.
RedisTemplate 직렬화
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @Bean public RedisTemplate<String, Object> redisTemplate(@Autowired RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); redisTemplate.setKeySerializer(new StringRedisSerializer()); return redisTemplate; }
@Bean(name = "jacksonRedisTemplate") public RedisTemplate<String, Object> jacksonRedisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(redisConnectionFactory);
GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(serializer); return template; }
|
ValueSerializer 를 설정하지 않거나 GenericJackson2JsonRedisSerializer 를 사용할 수 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @PostMapping("/normal") public void addUserData(@RequestBody UserData userData) { String key = userData.getUsername(); redisTemplate.opsForValue().set(key, userData); }
@GetMapping("/normal/{username}") public UserData getUserData(@PathVariable String username) { return (UserData) redisTemplate.opsForValue().get(username); }
@PostMapping("/jackson") public void addUserDataJackson(@RequestBody UserData userData) { String key = userData.getUsername(); jacksonRedisTemplate.opsForValue().set(key, userData); }
@GetMapping("/jackson/{username}") public UserData getUserDataJackson(@PathVariable String username) { return (UserData) jacksonRedisTemplate.opsForValue().get(username); }
|
위 API 를 통해 테스트하고 redis 에 들어가 value 를 조회하면 아래와 같이 출력된다.
기본 ValueSerializer 의 경우 객체 바이트 배열로 저장된다.
1
| ��sr com.example.redis.model.UserData,�3a&�LagetLjava/lang/Integer;LdesctLjava/lang/String;Lemailq~Lnicknameq~Lusernameq~xpsrjava.lang.Integer⠤���8Ivaluexrjava.lang.Number������xptSoftware Developertjohn@example.comtjohnnytjohn_doe
|
GenericJackson2JsonRedisSerializer 를 사용할 경우 @class 정보를 가지는 json 객체로 저장된다.
1
| {"@class":"com.example.redis.model.UserData","nickname":"johnny","username":"john_doe","email":"john@example.com","age":30,"desc":"Software Developer"}
|
@class 정보 없이 순수 json 으로 저장하기 위해 GenericJackson2JsonRedisSerializer 에 ObjectMapper 를 지정하는 경우도 있다.
1
| GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(objectMapper);
|
Redisson 분산락
https://github.com/redisson/redisson
Redis 기반 분산락 기능을 지원하는 라이브러리.
Pub/Sub 기반의 락을 사용하기에, 계속해서 CPU 자원을 사용하는 스핀락보다 효율적이다.
1 2
| implementation 'org.redisson:redisson-spring-boot-starter:3.30.0'
|
1 2 3 4 5 6
| @Bean public RedissonClient redissonClient() { Config config = new Config(); config.useSingleServer().setAddress("redis://localhost:6379"); return Redisson.create(config); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| @Slf4j @Service @RequiredArgsConstructor public class DistributedLockService {
private final RedissonClient redissonClient; private Integer count = 0;
public void executeWithLock(String lockKey) { RLock lock = redissonClient.getLock(lockKey); boolean isLocked = false; try { isLocked = lock.tryLock(100, 10, TimeUnit.SECONDS); if (isLocked) { Thread.sleep(100); count += 1; log.info("Lock acquired. Executing protected code. counter:{}", count); } else { log.info("Could not acquire lock."); } } catch (InterruptedException e) { e.printStackTrace(); } finally { if (isLocked) { lock.unlock(); log.info("Lock released."); } } }
public void executeWithoutLock() { try { Thread.sleep(100); count += 1; log.info("Lock acquired. Executing protected code. counter:{}", count); } catch (InterruptedException e) { e.printStackTrace(); } } }
|
해당 라이브러리의 주의해야할 점은 LOCK TTL 동안 임계코드 실행이 완료되지 않는다면 중복실행될 수 있다.
TTL 로 인해 Lock 이 해제되어도 어플리케이션 코드는 지속 실행되다 lock.unlock() 시점에야 TTL 을 넘겼음을 알 수 있다.
데드락 상황을 감수하고 TTL 을 무제한으로 설정한다면 확실하게 임계영역을 지킬 수 있다.
AOP 분산락
try, catch, finally 형태의 분산락 구조를 AOP 형태로 구성하는것이 대부분.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface DistributedLock { String value(); }
@Slf4j @Aspect @Component @RequiredArgsConstructor public class DistributedLockAspect { private final RedissonClient redissonClient;
@Around("@annotation(DistributedLock)") public Object executeWithLock(ProceedingJoinPoint joinPoint) throws Throwable { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); DistributedLock distributedLock = signature.getMethod().getAnnotation(DistributedLock.class); String lockKey = distributedLock.value(); RLock lock = redissonClient.getLock(lockKey); log.info("Lock acquired. Executing protected code."); try { lock.lock(); Object result = joinPoint.proceed(); return result; } finally { lock.unlock(); log.info("Lock released."); } } }
@DistributedLock("executeWithAopLock") public void executeWithAopLock() { log.info("business code invoked"); for (int i = 0; i < 10; i++) { count += 1; } log.info("business end"); }
|
데모코드
Redis demo
https://github.com/Kouzie/spring-boot-demo/tree/main/redis-demo