읽기락(Shared Lock) | 쓰리락(Exclusive Lock) | |
---|---|---|
읽기락(Shared Lock) | O | 대기 |
쓰기락(Exclusive Lock) | 대기 | 대기 |
SELECT ... FOR SHARE
SELECT ... FOR UPDATE
또는 UPDATE
, DELETE
쿼리SELECT
는 nonblocking consistent read로 동작한다. MySQL :: MySQL 5.7 Reference Manual :: 14.7.2.3 Consistent Nonlocking Reads비관적 락은 Repeatable read 또는 Serializable 정도의 격리성 수준을 제공한다. 비관적 락은 트랜잭션이 시작될 때 Shared Lock 또는 Exclusive Lock을 거는 방식이다.
Lock이 종료되기 전까지 다른 트랜잭션의 접근을 막기 때문에 동시성을 제어할 수 있다. 하지만, 모든 요청에 대해서 동시성에 문제가 발생한다고 예측하는 락이기 때문에 성능상의 문제가 있다.
낙관적 락은 Version 등을 사용하여 동시성을 제어하게 된다. Version을 사용한 경우 값을 읽어 들인 시점과 값을 갱신하는 시점의 Version이 같지 않다면 갱신을 실패하게 된다. 낙관적 락의 경우 갱신 실패시에 대해서 개발자가 직접 실패 로직을 작성해야 한다.
예제 코드
public Optional<Post> findById(Long postId, Boolean requiredLock) {
var sql = String.format("""
SELECT * FROM %s WHERE id = :postId
""", TABLE);
if (requiredLock) {
sql += "FOR UPDATE";
}
var params = new MapSqlParameterSource()
.addValue("postId", postId);
var nullablePost = namedParameterJdbcTemplate.queryForObject(sql, params, POST_ROW_MAPPER);
return Optional.ofNullable(nullablePost);
}
public Post update(Post post) {
var sql = String.format("""
UPDATE %s set
memberId = :memberId,
contents = :contents,
likeCount = :likeCount,
createdDate = :createdDate,
createdAt = :createdAt,
version = :version + 1
WHERE id = :id AND version = :version
""", TABLE);
var params = new BeanPropertySqlParameterSource(post);
var updatedCount = namedParameterJdbcTemplate.update(sql, params);
// TODO: RuntimeException 대신 실패 로직 작성
if (updatedCount == 0) throw new RuntimeException("갱신 실패");
return post;
}
requiredLock
이 true
인 경우 비관적 락이다. MySQL의 for update
를 이용.version
칼럼이 있다. 낙관적 락을 사용할 경우 해당 칼럼을 이용하여 동시성 제어를 수행한다.@Transactional
public void likePost(Long postId) {
// INFO: 데이터를 조회, 업데이트하는 과정이 같이 있기 때문에 동시성 문제 발생 가능성이 크다.
// INFO: LOCK을 이용해서 동시성을 제어하는 방법 @Transactional 어노테이션과 MySQL 쓰기락 사용
Post post = postRepository.findById(postId, true).orElseThrow();
post.incrementCount();
postRepository.save(post);
}
public void likePostByOptimisticLock(Long postId) {
Post post = postRepository.findById(postId, false).orElseThrow();
post.incrementCount();
postRepository.save(post);
}
위의 메소드는 비관적락, 아래의 메소드는 낙관적 락을 사용한 방식이다.
비관적락에는 @Transactional
어노테이션과 for update
를 통해서 동시성을 제어하므로 낙관적락에 비해서 성능이 낮아지게 된다.
위의 예제에서는 낙관적 Lock을 사용할 때에 해당 테이블(Post)에 version column을 추가하였다. 하지만, 위와 같은 방법은 사용자가 더 많아지고 동시성 제어 문제가 생길 수 있는 경우가 더 많아지게 되는 경우 문제가 더 많이 발생하기 쉽다.
즉. 하나의 레코드를 점유하게 될 때 쓰기 지점에서 병목 현상 발생
별도의 테이블을 만들어 부하를 어느정도 해결해 볼 수 있다. (게시물 좋아요 테이블의 생성)
create table postlike
(
id int auto_increment
primary key,
memberId int not null,
postId int not null,
createdAt datetime not null,
constraint member_post_unique_key
unique (memberId, postId)
);
(member, post
)의 unique key를 이용해서 같은 post에 대해서 좋아요 중복 방지 가능
새로운 문제… 조회시 매번 새로운 count 쿼리 연산이 발생
읽기 시점 병목 현상 발생
좋아요 수는 높은 정합성을 요구하지는 않는 데이터이다.
게시물 테이블의 컬럼에 좋아요 수를 캐싱할 수 있다. (Redis도 이용 가능)