본문 바로가기
Database

[MariaDB] 동시성 이슈

by qoth_0 2023. 11. 22.
728x90
반응형

  • 트랜잭션
    • 정의
      • 트랜잭션이란 하나의 논리적인 작업 단위로 처리되어야 하는 하나 이상의 SQL 문의 집합
      • 예시1) 은행 계좌 간에 이체를 할 때, 금액을 한 계좌에서 빼고 다른 계좌에 더하는 두 가지 연산은 한 작업의 단위로 처리되어야 하는 트랜잭션

        → A 고객의 계좌에서 10만원을 빼서 B고객의 계좌에 옮기는 경우, UPDATE 두가지가 필요

        → 이때 A 고객 계좌에서 빼는건 성공, B고객의 계좌에 더하는건 실패한다면 문제가생김

        → 따라서 이 두 UPDATE 쿼리 중 하나라도 에러가 발생하면 모두 취소시키기 위해 트랜잭션으로 묶어야 한다.

      • 예시2) 주문을 하고 order테이블에 주문을 생성하고, item테이블에서 재고까지 빼주는 작업이 한 단위로 처리되어야 하는 트랜잭션

        → order INSERT, item UPDATE 마찬가지

    • commit
      • COMMIT 명령은 한 트랜잭션의 모든 변경사항을 데이터베이스에 영구적으로 저장 - 모두 성공 시
    • rollback
      • ROLLBACK 명령은 트랜잭션의 변경사항을 모두 취소하고, 데이터베이스를 트랜잭션 시작 이전의 상태로 되돌리는 것 - 하나라도 오류가 발생한다면
    • COMMIT, ROLLBACK 실습
      • sql툴(workbench)에서 auto_commit 모드 해제
      • 예시) insert into author(id, name, email) values(10, 'test', 'test@naver,com'); - 성공 insert into post(title, contents, author_id) values('hello', 'hello is', 100); - 에러
        • 위 코드 실행후 commit하면 첫번째 쿼리만 확정
        • 위 코드 실행후 rollback하면 모두 취소
        insert into author(id, name, email) values(10, 'test', 'test@naver,com');
        insert into post(title, contents, author_id) values('hello', 'hello is', 100);
        select * from author;
        첫번째 insert만 성공 - 여기서 commit하면 첫번째 쿼리만 확정
        rollback 전 임시저장 상태지만 조회 가능
        rollback;
        select * from author;
        rollback되어 조회 불가능

      • 예시) insert into board_test.author(id, name, email) values(1, 'test', 'test@naver,com'); commit; insert into board_test.author(id, name, email) values(2, 'test2', 'test2@naver,com'); insert into board_test.post(title, contents, author_id) values('hello', 'hello is', 10);
        • 첫번째 쿼리는 commit되어 확정
        • 두번째 쿼리는 실행은 되나 세번째 쿼리 실행이후 rollback을 하게 되면 함께 rollback되므로, 하나의 논리적인 transaction으로 볼 수 있음
        • 일반적으로는 사용자가 위와 같은 쿼리를 실행시킬수는 없으니, 프로그램에서 논리적인 트랜잭션을 지정하고, 전체 commit 또는 전체 rollback하는 명령을 실행

    동시성이슈 - 보통 update가 문제가 됨

    여러 트랜잭션을 동시에 실행하면 데이터베이스의 정합성에 문제가 생김

    -dirty read : 커밋되지 않은 데이터가 읽힘(read uncommited 격리성)

    이 것을 해결하기 위한 격리성 : read commited(커밋된것만 읽음)

    -phantom read : 내가 실행한 트랜잭션의 결과가 아닌 다른 사람이 트랜잭션을 도중에 실행한 결과가 떠서 원래의 결과와 다른 것이 조회가 되는것 (없엇던 내용이 생김 - 상대방도 커밋완료한 내용) - insert쪽의 문제

    -unrepeatable read : 반복이 안되는 문제(값이 달라지는 문제) - update 문제

    이 둘을 해결하기위한 격리성 : repeatable read : 이것은 내트랜잭션에서 처음에 읽엇던 결과를 도중에 트랜잭션이 변경해도 원래 내용을 유지하겠다는 내용 - 실제로는 변화가 되어있는 것을 무시(read의 일관성만 보장, 정합성은 문제가 됨)

    -한 트랜잭션에서의 read는 일관성 보장하지만 실질은 이미 변경되면 정합성 문제가 있는 것

    → 내 트랜잭션 실행 전 후로 update가 실행되면 문제가 됨

    → 라킹전략

    • 공유락(shared lock) : 내가먼저 read했으면 다른사람드른 read는 할 수 있어도 update를 못하게 하는 것 (앞단에서 치고오는 것은 막아줌)

      → select for share

    • 타락(exclusive lock) : 해당 row에 대해 완전하게 막음(read도 불가), 성능이 떨어짐

      → select for update

    → 데드락(dead lock) : 교착상태, 락킹 시 발생하는 이슈

    author 테이블, post 테이블이있고, a트랜잭션과 b트랜잭션이 모두 두 테이블을 서로 다른 순서로 사용한다고 가정. a트랜잭션은 author테이블에 lock을 걸고, b트랜잭션은 post 테이블에 lock을 걸고 서로 다른 테이블에 대기상태임. 둘다 기존 테이블에서 다른 테이블로 이동해야 commit이 되며 트랜잭션이 끝나는데 lock이 걸려있어서 이동이 안되고, 트랜잭션이 완료되지 않는 상황이 발생함

    ⇒ mariaDB는 교착상태가 일정시간 유지되면 rollback을 실행시킴

    ⇒ 개발 시 역방향으로 서비스가 전개되지 않도록 순방향 설계를 할 수도 있음(서로 다른 서비스가 교착상태에 애초에 빠지지 않도록)

    ⇒ spring에선 라킹전략을 ‘비관적인 락(패써미스틱락?)’이라고 부름 (이미 update에서 문제가 생길것으로 비관적으로 간주하고 락을 실행하는 것이기 때문)

    • 낙관적인 락(옵티미스트락) : 문제 안생길 것이라 가정하고 실행 - 버전관리

      update를 날릴때 where로 현재버전을 함께 관리함 - where의 버전보다 다른 트랜잭션으로 update가 된 버전이 생겨 버전이 다르면 오류가 생김 - 사용자 경험이 떨어짐

    -Serializable : 트랜잭션 순차 실행, 동시성 이슈가 없음, 성능이 매우 떨어짐 - 데이터베이스의 기본 전략으로 선택될 경우가 적음

    • DB 동시성 이슈
      • 트랜잭션이 동시에 실행됐을때 발생할 수 있는 문제 관련한 상황을 DB동시성 문제라함
      • dirty read
        • 한 트랜잭션이 다른 트랜잭션이 수정 중인 데이터를 읽을 수 있는 문제
        • 아직 commit되지 않은 데이터가 읽힘으로서, 추후 rollback 될 가능성이 있는 데이터 read
        • 해결책 : Read Committed 격리성(커밋한 데이터만 읽을 수 있따) - 모든 DB에 기본적으로 설정되어 있음
      • Non-Repeatable Read - READcommitted 격리성을 취해도 발생
        • mariaDB의 기본 설정은 Repeatable Read
        • 한 트랜잭션에서 동일한 조회 쿼리를 두 번 이상 실행할때에, 그 중간에 다른 트랜잭션에서 데이터를 수정하여 한 트랜잭션의 결과가 다르게 나타나는 문제 예시) 재고 업데이트 전 현재 재고 조회 -> 수량업데이트 -> 변경된 재고 수량을 다시 조회(한트랜잭션에서). 그러나 만일 이를 중간에 누가 수정을 가하면 재조회시 오차 발생하여 에러.
        • 해결책 : Repeatable Read 격리성 -> 한 트랜잭션이 조회하는 동안 다른 트랜잭션이 update를 하더라도 현재 트랜잭션의 read값이 변경되지 않도록 하는 격리성. 그러나, 만약 다른 트랜잭션에서 update를 통해 값을 변경해버렸다면, read한 값 자체에 문제 발생. => 타 트랜잭션의 update를 막기 위한 select for update 쿼리(배타적락) 필요
      • Phantom Read - READcommitted 격리성을 취해도 발생
        • 한 트랜잭션이 같은 조회쿼리를 여러 번 실행했을 때, 그 중간에 다른 트랜잭션에서 새로운 데이터를 추가/삭제하여 다르게 나타나는 문제
        • 해결책 : Repeatable Read 격리성. 팬텀(유령) 읽기 또한 repeatable Read격리성으로 해결이 가능하나, 이 부분은 DB마다 차이가 있어 phantom read를 해결하기 위해 Serializable 격리수준이 필요할수도 있음. 다만, DB에서 주로 발생하는 문제는 동시에 수정하는 상황이므로, 수정에 초점을 두고 해결 전략을 살펴봐도 좋을것.
      • 사실 동시성 문제는 read만이 문제는 아니고, read이후 DB에 어떠한 수정사항을 가할때도 read의 오차로 인한 또다른 오차가 발생하여 DB 전체에 영향이 발생하므로 DB 전체에 대한 동시성이라 보면 될것.
    • DB 격리수준
      • DB 동시성 문제를 해결하기 위한 격리수준
      • Read Uncommitted
        • 즉, 데이터가 변경되었다면, 커밋되지 않았다 하더라도 읽을 수 있도록 하는 격리수준
        • dirty read 발생 가능
      • Read Committed
        • 다른 트랜잭션이 커밋된 데이터만 읽을 수 있는 격리수준.
        • 다만, 나의 트랜잭션에 여러 select 문이 있을 경우에, 그 사이에 다른 트랜잭션에서 update 또는 insert 등을 발생시키고 commit하게 될시 phantom read 또는 non-repeatable-read 발생가능
      • Repeatable Read - mariaDB의 기본 설정 (중간에 데이터가 추가되거나 업데이트되는 것을 막아줌)
        • 한 번 읽은 데이터는 같은 트랜잭션 내에서는 항상 같은 값을 갖도록 하는 격리수준 나의 트랜잭션에서 먼저 read하는 동안 다른 트랜잭션에서는 변경,추가 하더라도 같은 read값을 보장하는 것. -> Non-Repeatable Read과 Phantom read를 해결
        • repeatable read를 하더라도 두가지 문제가 발생할 가능성 존재
          • 나의 트랜잭션이 read하는 동안 타 트랜잭션에서 update하게 되면 read해온 값이 달라지는 문제 발생
            • 공유락(shared lock - select for share)
              • 두 다른 트랜잭션이 동시에 read하는 것은 가능하여 lost update 문제 가능성 존재

                ex)상품주문의 최종 수량이 1개 -> transaction에 read && update가 있을때 -> 내 tran에서 1 read -> 타 트랜잭션이 1 read -> 내 tran에서 0으로 update -> 타 tran에서 0으로 update -> 최종 수량에 오류 발생

            • 배타락(exclusive lock - select for update)
              • read부터 lock을 걸어 lost update 문제 해결
      • Serializable - 최상위
        • 동시에 실행되는 여러 트랜잭션들을 순차적으로 실행한 것과 같은 결과를 보장 -> 즉 데이터베이스 차원에서 동시에 특정 데이터에 접근하는 것을 차단
        • 한 사용자만 쓰거나, 다른 유저는 늦은 사용자 경험을 갖게 됨
    • DB 동시성 관련 실무 해결책
      • 위와 같은 동시성 이슈는 일반적이지는 않은 상황이지만, 쇼핑몰이벤트 또는 예매 시스템에서는 빈번하게 발생할수 있는 가능성 존재
      • 일반적으로 update 관련한 이슈이고, update를 하기 위해서는 select하여 현재 count값 get -> 구매수량을 뺀 new_count값으로 update가 한 트랜잭션으로 동작
      • Spring에서의 전략
        • optimistic lock
          • 버전정보 활용하여 update시에 정합성 체크
          • update item set stock_count = new_count where id=1 and version = 1; 만일, version이 맞지 않다면 0rows affected
        • pessimistic lock
          • 공유락(lock - PESSIMISTIC_READ)
          • 베타락(lock - PESSIMISTIC_WRITE)
            • 특정행에 대해 lock을 걸어 read조차 막음으로서 update시에 발생하는 이슈 원천 차단.
            • Serializable수준의 격리를 특정테이블과 특정데이터에 적용가능

      좋은 시스템을 설계하기 위해 이런 세세한 설계가 필요

      나중에 프로젝트 할 때 테스트 해보기 - 동시성 이슈 해결

      • queue사용
        • 이벤트, 예매 상황에서 고려될수 있는 가장 일반적인 방식
        • 성능의 저하를 감수해야 하지만, 시스템의 정확도와 사용자경험의 향상을 꾀할수 있음
      • Redis 사용
        • 싱글스레드 기반 key-value 시스템

Uploaded by N2T

728x90
반응형

'Database' 카테고리의 다른 글

[MariaDB] GROUP BY  (0) 2023.11.24
[MariaDB] JOIN  (1) 2023.11.22
[MariaDB] 흐름제어  (0) 2023.11.22
[MariaDB] 제약조건  (0) 2023.11.22
[MariaDB] 연산자  (1) 2023.11.22