2023-08-07-TIL
2023-08-07-TIL
Today I Learned
Self-Invocation and Spring AOP with @Transactional
@Transactional은 Spring AOP 프록시로 동작한다. 빈을 주입받는 쪽은 실제 객체가 아니라 프록시를 들고 있고, 프록시가 메서드 호출을 가로채 트랜잭션을 시작하고 끝낸다. 그런데 같은 클래스 안에서 this.method()로 다른 메서드를 부르면 호출이 프록시를 거치지 않는다. 그래서 호출되는 메서드에 붙은 @Transactional이나 @Async가 적용되지 않는다. 이것이 self-invocation 문제다.
Spring 레퍼런스의 프록시 메커니즘 문서는 해결책을 이 순서로 제시한다.
- 자기 호출이 생기지 않도록 코드를 나눈다. 트랜잭션이 필요한 메서드를 다른 빈으로 옮기는 방식이고, 문서가 가장 권하는 방법이다.
- 자기 자신을 주입(self injection)받아
this대신 그 참조로 호출한다. AopContext.currentProxy()를 쓴다. 코드가 Spring AOP에 강하게 묶이므로 권하지 않는다.
문서는 AspectJ 컴파일 타임·로드 타임 위빙은 바이트코드에 직접 어드바이스를 넣기 때문에 이 문제가 없다고도 덧붙인다.
- https://gmoon92.github.io/spring/aop/2019/04/01/spring-aop-mechanism-with-self-invocation.html
- https://stir.tistory.com/175
- https://woodcock.tistory.com/30
Redis DB and @Transactional?
Redis에 커맨드를 날리는 SET과 같은 연산이 Spring의 @Transactional 영향을 받을까? 기본 설정에서는 영향을 받지 않는다.
Spring Data Redis 문서에 따르면 RedisTemplate은 기본적으로 Spring이 관리하는 트랜잭션에 참여하지 않는다. setEnableTransactionSupport(true)로 켜면 @Transactional 범위 안의 쓰기 명령은 큐에 쌓였다가 커밋 시점에 Redis의 MULTI/EXEC로 한 번에 실행된다. 이때 KEYS 같은 읽기 명령은 별도 커넥션으로 바로 실행되므로, 트랜잭션 안에서 방금 쓴 값을 읽으면 아직 반영되지 않은 값이 보인다.
트랜잭션 지원을 켜지 않고 여러 명령을 묶고 싶다면 SessionCallback으로 같은 커넥션에서 multi()와 exec()를 직접 호출한다. 문서는 둘 사이에서 예외가 나면 커넥션이 트랜잭션 상태로 남을 수 있으니 discard()를 호출하라고 안내한다. Redis 트랜잭션은 RDB처럼 중간 실패 시 롤백해 주지 않으므로, 조건부 원자 연산이 필요하면 Lua 스크립트가 대안이 된다.
- https://www.vinsguru.com/redis-transaction-with-spring-boot/
- https://stackoverflow.com/questions/66981818/how-to-configure-transactional-support-to-redis-in-springboot
- https://velog.io/@cmsskkk/redis-transaction-spring-and-lua-pipeline
Redis Log
Redis에 실행된 커맨드를 조회하려면? 로그파일을 뒤져보거나 monitor 명령어로 실행된 명령어 및 처리내역을 상세하게 볼 수도 있다. 하지만 monitor 명령어는 종료하지 않으면 부하를 줄 수 있으므로 자제하는 것이 좋다.
Redis 서버 로그에는 기동, 영속화, 복제 같은 서버 이벤트가 남고 클라이언트가 보낸 명령 하나하나는 남지 않는다. 그래서 명령 단위로 보려면 MONITOR를 쓴다. MONITOR 문서는 이 명령을 서버가 처리하는 모든 명령을 실시간으로 흘려보내는 디버깅용 명령으로 소개하고, 모든 명령을 스트리밍하기 때문에 비용이 든다는 점을 별도 절로 경고한다.
- https://redis.com/ebook/part-2-core-concepts/chapter-5-using-redis-for-application-support/5-1-logging-to-redis/
- https://betterstack.com/community/guides/logging/how-to-start-logging-with-redis/
- https://linuxhint.com/reading-redis-logs/
조심해야할 Redis 명령어
keys, monitor…
Redis는 명령을 하나의 스레드에서 순서대로 처리하므로, 오래 걸리는 명령 하나가 그동안 다른 모든 요청을 막는다. KEYS 문서는 시간 복잡도를 키 개수에 비례하는 O(N)으로 적고, 운영 환경에서는 매우 조심해서 쓰고 애플리케이션 코드에서는 쓰지 말라고 경고한다. 키를 찾아야 한다면 커서로 조금씩 순회하는 SCAN을 쓰라고 안내한다. MONITOR는 위에서 본 대로 켜 두는 동안 서버 처리량을 떨어뜨린다. 문서에서 두 명령 모두 ACL 카테고리 @dangerous로 분류되어 있다.
Isolation Level
트랜잭션 격리 수준은 동시에 실행되는 트랜잭션이 서로의 변경을 어디까지 볼 수 있는지 정한다. MySQL 문서 기준으로 네 단계가 있다.
READ UNCOMMITTED: 커밋되지 않은 변경도 읽는다(dirty read).READ COMMITTED: 커밋된 변경만 읽지만, 같은 조회를 반복하면 결과가 달라질 수 있다.REPEATABLE READ: InnoDB의 기본값이다. 트랜잭션 안의 일반 조회는 첫 조회 때 만든 스냅샷을 계속 읽는다.SERIALIZABLE: 일반 조회도 공유 잠금을 거는 잠금 읽기로 바꿔 가장 엄격하게 격리한다.- https://brunch.co.kr/@purpledev/4
Lock wait timeout exceeded
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction은 다른 트랜잭션이 잡고 있는 행 잠금을 기다리다 포기했을 때 나는 오류다. MySQL 문서에 따르면 대기 시간은 innodb_lock_wait_timeout으로 정하고 기본값은 50초다. 시간이 지나면 트랜잭션 전체가 아니라 현재 문장만 롤백된다. 전체를 롤백하려면 --innodb-rollback-on-timeout으로 서버를 띄워야 한다.
원인은 대개 잠금을 오래 쥔 트랜잭션이다. 커밋하지 않고 열려 있는 세션이나, 트랜잭션 안에서 외부 API 호출처럼 오래 걸리는 작업을 하는 코드가 흔한 예다. 타임아웃 값을 늘리기보다 information_schema.innodb_trx 등에서 잠금을 쥔 트랜잭션을 찾고 트랜잭션 범위를 줄이는 것이 근본 대책이다.
- https://kapentaz.github.io/mysql/Lock-wait-timeout-exceeded;-try-restarting-transaction-%EB%B0%9C%EC%83%9D-%EC%9D%B4%EC%9C%A0%EC%99%80-%ED%95%B4%EA%B2%B0/#
- https://www.popit.kr/mysql-lock-%EC%83%81%ED%99%A9-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0/
댓글
아직 댓글이 없습니다