동시성제어

duckbill413·2023년 12월 12일
0

MySQL

목록 보기
5/5
post-thumbnail

동시성 제어

  • 동시성 제어를 위해서는 주로 락을 통해서 제어하게 된다.
  • 락을 통해 동시성을 제어할 때는, 락의 범위를 최소화 하는 것이 중요하다.
  • MySQL에서는 트랜잭션의 커밋 혹은 롤백 시점에 잠금이 풀린다. 즉, 트랜잭션이 곧 락의 범위이다.
    • 외부 네트워크 및 IO 발생 시에는 최대한 트랜잭션 범위 외에서 처리하는 것이 좋다.
    • MySQL은 쓰기락, 읽기락이 존재
    • MySQL 읽기락과 쓰기락
      읽기락(Shared Lock)쓰리락(Exclusive Lock)
      읽기락(Shared Lock)O대기
      쓰기락(Exclusive Lock)대기대기
      • 읽기락(Shared Lock)이 잠금을 획득한 상태에서 다른 읽기락이 들어온다면 서로 잠금을 공유하게 된다.
      • 쓰기락(Exclusive Lock)이 데이터를 점유한 상태에서는 다른 읽기, 쓰기 요청을 대기 시킨다.
      • 읽기락 SQL: SELECT ... FOR SHARE
      • 쓰기락 SQL: SELECT ... FOR UPDATE 또는 UPDATE, DELETE 쿼리
      • 매번 Lock이 잠금하게 된다면 성능저하가 발생할 수 밖에 없다. 따라서, MySQL에서는 일발 SELECT는 nonblocking consistent read로 동작한다. MySQL :: MySQL 5.7 Reference Manual :: 14.7.2.3 Consistent Nonlocking Reads
    • Record Lock: MySQL에서 Lock은 row가 아니라 index를 잠근다. 즉, Index가 없는 조건으로 Locking Read시 불필요한 데이터들이 잠길 수 있다.
    • TODO
      • Java에서의 동시성 이슈 제어방법
      • 분산환경에서의 동시성 이슈 제어방법
      • MySQL의 넥스트 키락이 등장한 배경
      • MySQL 외래키로 인한 잠금
      • MySQL 데드락

낙관적 락과 비관적 락

  • 비관적 락(Pessimistic Lock)
  • 낙관적 락(Optimistic Lock)

비관적 락 (Pessimistic Lock)

비관적 락은 Repeatable read 또는 Serializable 정도의 격리성 수준을 제공한다. 비관적 락은 트랜잭션이 시작될 때 Shared Lock 또는 Exclusive Lock을 거는 방식이다.

Lock이 종료되기 전까지 다른 트랜잭션의 접근을 막기 때문에 동시성을 제어할 수 있다. 하지만, 모든 요청에 대해서 동시성에 문제가 발생한다고 예측하는 락이기 때문에 성능상의 문제가 있다.

낙관적 락 (Optimistic 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;
}
  • findById 에서 requiredLocktrue인 경우 비관적 락이다. MySQL의 for update를 이용.
  • 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도 이용 가능)

profile
같이 공부합시다~

0개의 댓글