Offset 페이징은 게시판에서 버튼을 눌러서 웹 페이지를 이동하는 경우 사용되는 방식이다.
SELECT *
FROM table
ORDER BY column
OFFSET 10
LIMIT 5;
OFFSET
은 일반적으로 LIMIT
과 함께 사용된다.
예를 들어 OFFSET 10
는 처음 10개의 행을 건너뛰고, LIMIT 5
은 그 이후 5개의 행을 반환하도록 지정하는 것을 의미한다.
이런 Offset 방식은 처음부터 데이터를 읽은 후 Limit으로 자르는 방식이기 때문에 대용량 시스템에서 이런 방식은 비효율적일 수 있다.
또한 Offset 방식은 데이터 중복의 문제가 생길 수 있다.
Offset 페이징 방식은 데이터베이스에서 최신의 데이터를 가져오게 된다.
만약 실시간으로 새로운 데이터가 추가된다면, 사용자는 다음 페이지에서 기존에 이미 봤던 정보를 다시 보게 되면서 서비스 상의 문제점을 인지하게 될 것이다.
Cursor 페이지는 실시간으로 대용량의 데이터를 다룰 경우 사용하는 방식이다. 유튜브나 페이스북, 트위터 등 에서 무한 스크롤을 지원할 때 사용되고 있다.
SELECT *
FROM table
WHERE column > last_value
ORDER BY column
LIMIT 5;
Cursor
는 last_value
처럼 특정 변수를 기준으로 새로운 데이터들을 탐색하고 Limit
으로 그 이후 5개의 행을 반환하도록 지정하는 방식으로 동작한다.
Cursor 페이징 방식은 Cursor를 사용하여 직접 페이지를 탐색하기 때문에 데이터의 크기와 관련없이 일관된 성능을 유지할 수 있다는 장점을 가지고 있다.
아래는 Offset 페이징과 Cursor 페이징의 시간복잡도를 표현한 차트이다.
더 이상 나눌 수 없는 작업의 단위.
즉, 한꺼번에 수행되어야만 하는 작업의 단위를 의미한다.
스프링에서 @Transactional
이라는 어노테이션을 사용하면 간단하게 트랜잭션을 구현할 수 있다.
@Transactional
public void performTransactionalOperation() {
User user = new User("John", 21);
myRepository.save(user);
}
}
@Transactional
을 사용한 메서드에서 예외가 발생하지 않고 정상적으로 수행되면 결과가 commit 되고, 예외가 발생한다면 rollback 된다.
트랜잭션 격리 수준이란 동시에 여러 트랜잭션이 처리될 때, 트랜잭션들이 서로의 변경된 데이터를 어느 수준까지 볼 수 있는지 결정하는 것을 의미한다.
트랜잭션의 격리 수준은 다음과 네 가지가 사용된다.
이런 트랜잭션의 격리 수준에 따라 문제들에 대해서 자세히 알아보자.
Dirty Read | Non Repeatable Read | Phantom Read | |
---|---|---|---|
Read Uncommitted | O | O | O |
Read Committed | X | O | O |
Repeatable Read | X | X | O |
Serializable Read | X | X | X |
락(Lock)은 동시성 제어를 위해 트랜잭션 처리의 순차성을 보장하기 위한 방법이다.
이렇게 락을 통해 동시성을 제어할 때, 락의 범위를 최소화하는 것이 중요하다.
락의 범위가 크면 클수록 락을 획득하기 위해 대기하는 트랜잭션이 늘어나기 때문에 성능에 영향을 줄 수 있기 때문이다.
스프링의 경우 락의 범위가 커질수록 커넥션 풀(Conntection Pool)을 점유하는 시간이 길어지고, 최악의 경우에는 커넥션 풀이 고갈되는 상황으로 이어질 수 있다.
MySQL에서는 트랜잭션의 커밋 혹은 롤백 시점에 잠금이 풀린다.(트랜잭션 = 락의 범위)
MySQL에서 제공하는 락의 종류는 두가지로, 읽기락(Shared Lock), 쓰기락(Exclusive Lock)을 제공한다.
읽기락(Shared Lock) | 쓰기락(Exclusive Lock) | |
---|---|---|
읽기락(Shared Lock) | O | 대기 |
쓰기락(Exclusive Lock) | 대기 | 대기 |
MySQL에서 읽기락은 SELECT ... FOR SHARE
을 통해 획득할 수 있고, 쓰기락은 SELECT ... FOR UPDATE, DELETE
등을 통해 획득할 수 있다.
하지만 매번 잠금이 발생할 경우, 성능 저하를 피할 수 없기 때문에 MySQL에서는 일반 SELECT는 nonblocking consistent read로 동작한다.
잠금을 걸 때 주의할 점은 MySQL에서 잠금은 row가 아니라 인덱스를 잠근다는 점이다.
따라서 인덱스가 없는 조건으로 잘못 Locking Read시 불필요한 데이터들이 잠길 수 있으니 주의해야 한다.
예를 들어
SELECT * FROM POST WHERE contents = 'string' FOR UPDATE; #contents columns은 인덱스 x 가정
그리고 다음을 통해 락을 조회해보면 인덱스가 없는 columns이라서 모든 record가 잠긴 것을 확인 할 수 있다.
SELECT * FROM performance_schema.data_locks;