트랜잭션과 잠금
- MySQL의 동시성에 영향을 미치는 잠금(Lock)과 트랜잭션, 트랜잭션의 격리 수준을 살펴볼 것.
- 트랜잭션은 작업의 완전성을 보장해 주기 위한 기능. 즉 논리적인 작업 셋을 완벽하게 처리하지 못했을 때, 원 상태로 복구해서 작업의 일부만 적용되는 현상이 발생하지 않게 만들어주는 기능.
- 잠금은 동시성을 제어하기 위한 기능이며 트랜잭션은 데이터의 정합성을 보장하기 위한 기능.
- 데이터의 정합성은 데이터들의 값이 서로 일치하는 상태. cf)데이터의 무결성 : 데이터값이 정확한 상태
- ex) 한 트랜잭션에서 동일한 레코드에 대한 조회 쿼리가 두 번 있으며 두 번의 조회 결과가 다른 경우 정합성이 지켜지지 않는 상태이다.
- ex) 고유한 프라이머리키의 값이 서로 다른 두 데이터가 동일할 때, 데이터의 무결성이 지켜지지 않는 상태이다.
- 데이터의 정합성은 데이터들의 값이 서로 일치하는 상태. cf)데이터의 무결성 : 데이터값이 정확한 상태
- 트랜잭션의 격리 수준은 하나의 트랜잭션 내에서 또는 여러 트랜잭션 간의 작업 내용을 어떻게 공유하고 차단할 것인지를 결정하는 레벨.
5-1 트랜잭션
5.1.1 MySQL에서의 트랜잭션
- 트랜잭션은 작업의 완전성을 보장해 주기 위한 기능이라고 설명하였다, 트랜잭션(논리적인 작업 셋)에 하나의 쿼리가 있든, 두 개 이상의 쿼리가 있든 관계없이 해당 작업 셋이 100% 적용되거나(commit) 아무것도 적용되지 않는 것(rollback)을 보장해주는 것.
- 만약 앞서 설명한 것을 보장해주지 않는다면, 즉 트랜잭션 기능이 없으면 어떻게 될까? MyISAM과 Memory 스토리지 엔진은 트랜잭션을 지원하지 않는다. MyISAM에서 특정 테이블에 프라이머리키가 3인 데이터를 생성하고, 이 후 프라이머리키가 1,2,3인 데이터들을 추가로 생성해주면 어떻게 될까 ? 트랜잭션 기능이 있는 InnoDB에서는 프라이머리 키 중복으로 해당 트랜잭션(1,2,3을 생성하는 작업)이 100% 롤백되어 조회 시 이전과 똑같이 프라이머리 키가 3인 데이터만 조회될 것이다. 반면에 MyISAM에서는 프라이머리키가 1,2인데이터가 삽입되고 3인 데이터를 넣을 때 에러가나게되며, 트랜잭션 기능이 없기 떄문에 삽입된 1,2데이터가 롤백되지 않고 남아있게 된다.(부분 업데이트로 쓰레기 데이터가 남아있게되며 처리가 필요하게 된다. 남겨둘지, 삭제할지) 이와 같이 데이터의 정합성?이 보장되지 않으면 개발자는 여러 케이스를 고려하여 분기처리를 해야할 것이며 부분 업데이트되는 쿼리가 여러개이면 훨씬 복잡해질 것임.
5.1.2 주의사항
- 트랜잭션은 DBMS의 컨넥션과 동일하게 최소의 코드에만 적용하는 것이 좋다. (트랜잭션의 범위 최소화)
- 조회 작업은 트랜잭션에서 제외.
- 조회도 한 트랜잭션에 묶어야 조회값을 얻을 수 있지않을까 ? 그렇지 않으면 격리 레벨에 따라 원하는 조회값이 바뀔 수 있지 않을까 ?
- CUD 작업은 같은 성격끼리 묶어서 최소화.
- DB컨넥션 또한 DB작업이 시작되는 시점에 연결하고 작업이 없으면 종료하는 식으로 범위 최소화.
- 즉, 전체 로직에 있어서 DB작업을 좀 더 세분화하여 범위를 줄이자.
- spring에서 사용하는 @transactional 어노테이션은 하나의 서비스 로직(메소드) 단위로 붙여준다. 이것도 좀더 세분화해서 메소드 내부의 코드단에 붙여주는 것이 좋을까 ?
- 해당 어노테이션이 어떻게 동작하고 어떤 기능을 하는지 좀 더 정확하게 파악해보자.
- @Transactional 어노테이션은 프록시 객체를 생성하여 해당 어노테이션이 붙은 클래스, 메소드가 호출 될때, 트랜잭션을 시작하고 정상 여부에 다라 Commit/Rollback 동작을 수행한다. 또한 해당 트랜잭션과 관련된 설정 및 해당 트랜잭션의 시작, 종료와 관련된 옵션 기능을 지원한다.
- 트랜잭션이 시작되고 종료되는 범위가 최소화 되면 좋을 텐데 가능할까?
- 일단 @Transactional은 인터페이스, 클래스, 메소드에 적용 가능하며, 범위를 더 작게 지정할 수는 없는듯.
- 이러한 이유는 ?? → 서비스 레이어의 메소드 단위가 충분히 작은 범위다 ?, 큰 성능적 이점이 없다 ?
- 혹은 범위 최소화를 위해 내부 로직을 더 세분화해서 쪼개고 세분화된 메소드들에 @Transactional 어노테이션을 붙이는 식으로 적용해볼 수 있기 때문에 굳이 만들지 않았다 ?
5-2 MySQL 엔진의 잠금
- MySQL에서 잠금은 스토리지 엔진 레벨과 MySQL엔진 레벨로 나눌 수 있음.
- MySQL레벨의 잠금은 모든 스토리지 엔진에 영향을 주지만, 스토리지 엔진 레벨은 특정 스토리지에만 영향.
- 글로벌 락
- FLUSH TABLES WITH READ LOCK으로 획득.
- 제공하는 잠금 기능 가운데 가장 범위가 큼. ( MySQL 서버 전체 )
- 트랜잭션을 지원하는 InnoDB 엔진이 보편화 되면서, 데이터의 일관된 상태를 위해 서버 전체에 잠금을 거는 글로벌 락의 기능의 필요성이 줄어들었다. MySQL8.0부터는 좀더 가벼운 글로벌 락인 백업 락이 도입.
- 테이블 락
- 개별 테이블 단위로 설정되는 잠금.
- LOCK TABLES table_name [READ | WRITE] 로 획득. / UNLOCK TABLES로 반납.
- 해당 락도 특별한 상황이 아니면 사용할 일이 거의 없다. (글로벌 락과 동일하게 온라인 작업에 상당한 영향을 미치기 때문)
- 묵시적으로 테이블 락이 걸리는 경우(자동)는 MyISAM, Memory엔진에서 데이터 변경 쿼리를 실행하는 경우 발생하며, InnoDB엔진에서도 스키마를 변경하는 쿼리의 경우 동작함.(InnoDB에서 단순 데이터 변경 쿼리는 설정되지 않음. 레코드 기반의 잠금을 제공하기 때문.)
- 네임드 락
- GET_LOCK()을 이용해 문자열에 대해 잠금 설정.
- 특정 상황에서 유용하게 사용 가능.
- 메타데이터 락
- 데이터베이스 객체(테이블이나 뷰)의 이름이나 구조를 변경하는 경우에 획득하는 잠금.
- 테이블의 이름을 변경하는 경우와 같은 상황에 자동으로 획득하는 잠금.
5-3 InnoDB 스토리지 엔진 잠금
- MySQL에서 제공하는 잠금과는 별개로 스토리지 엔진 내부에서 레코드 기반의 잠금 방식 탑재.
- 레코드 기반의 장금 방식으로 InnoDB는 MyISAM보다 뛰어난 동시성 처리를 제공.
- InnoDB 스토리지 엔진에 사용되는 잠금에 대한 정보는 이원화되어있어서 MySQL 명령을 이용해 접근하기가 어려웠는데 최근 버전에서는 여러 관련 정보를 조회하고 모니터링할 수 있는 방법이 도입됨.
InnoDB 스토리지 엔진의 잠금
- 레코드 락
- 레코드 자체만을 잠그는 것.
- 다른 dbms에서 지원하는 레코드 락과의 차이점은 레코드 자체를 잠금하는 것이 아인 레코드의 인덱스를 잠금 함.
- 갭 락
- 레코드 자체가 아닌 레코드와 바로 인접한 레코드 사이의 간격을 잠그는 것.
- 즉 인접한 레코드 사이에 새로운 레코드가 생성되는 것을 제어함.
- 넥스트 키 락의 일부로 자주 사용됨.
- 넥스트 키 락
- 레코드 락과 갭 락을 합쳐 놓은 형태의 잠금.
- 자동 증가(auto increment) 락
- 인서트되는 레코드들의 순서가 중복되지 않고 증가하며 저장되도록 보장하기위해 사용되는 락.
- 테이블 수준의 잠금.
- 인서트와 리플레이스 쿼리 문장에서 Auto Increment 속성을 가져오는 순간만 락이 걸림.
- 하나만 존재하기 때문에 하나의 쿼리가 자동 증가 락을 걸면 나머지 쿼리는 해당 락이 풀릴 때까지 기다려야한다.
인덱스와 잠금
- InnoDB의 잠금과 인덱스는 중요한 연관 관계가 있다. 왜냐하면 InnoDB에서 잠금은 레코드 자체가 아닌 인덱스를 잠금하는 것이기 떄문.
- 예를 들어 인덱스로 사용되는 컬럼에 대해 특정 값을 업데이트하는 경우, 해당 특정 값이 여러개이면 해당되는 모든 레코드가 잠금된다. 또한 테이블에 인덱스가 하나도 없다면, 테이블을 풀 스캔하기 때문에 테이블의 모든 레코드가 잠금된다.(즉, 테이블 수준의 잠금)
- 인덱스는 컬럼을 정해서 설정하는 것이며 위와같은 이유를 포함하여 중복값이 적은 칼럼을 고르는 것이 좋다.
레코드 수준의 잠금 확인 및 해제
- InnoDB 스토리지 엔진에서 기본적으로 적용되는 잠금 수준인 레코드 수준의 잠금은 테이블 수준의 잠금보다는 보다 복잡하다.
- ver 5.1 이 전에는 레코드 잠금에 대한 메타 정보를 제공하지 않았기 때문에 더욱 복잡했으며 이 후로는 레코드 잠금과 잠금 대기 정보들에 대한 조회가 가능하여서 정보를 모니터링할 수 있게 되었다.
5-4 MySQL의 격리 수준
- 트랜잭션의 격리 수준이란 여러 트랜잭션이 동시에 처리될 때 특정 트랜잭션이 다른 트랜잭션에서 변경하거나 조회하는 데이터를 볼 수 있게 허용할지 말지를 결정하는 것.
- Serializable 격리 수준이 아니면 나머지 3가지 격리 수준끼리는 크게 성능의 개선이자 저하는 발생하지 않는다고 함.
- 세가지 부정합 : Dirty Read, Non-repeatable read, Phantom read
- 일반적으로 read commited 혹은 repeatable read중 하나를 사용.
Read Uncommited
- 트랜잭션의 변경 내용이 commit이나 rollback 여부에 상관없이 다른 트랜잭션에서 보인다.
- Dirty Read 발생
Read Commited
- 트랜잭션의 변경 내용이 커밋이 완료되었을 때만 조회할 수 있으며, 커밋이 완료되지 않았을 때는 기존의 데이터를(언두로그 사용) 보여준다.
- Non-repeatable read 발생
Repeatable Read
- 트랜잭션의 변경 내용이 커밋이 완료되었더라도 다른 트랜잭션에서 두번에 걸쳐 조회할 때, 해당 조회 쿼리가 커밋 이전과 이후에 실행되면 두 조회 결과가 달라지게 될 수 있으며 Repeatable Read는 한 트랜잭션에서 똑같은 쿼리에 대해 같은 결과를 가져오는 것을 보장한다.
- 즉, Read commited와다르게 non-repeatble read발생 x
- Phantom read 발생
Serializable
- 가장엄격한 격리 수준.
- InnoDB 스토리지 엔진에서는 갭 락과 넥스트 키 락이 사용되어 PHATOM READ가 발생하지 않기 때문에 굳이 사용할 필요 x.
스터디
- 분산락
- 낙관적 락, 비관적 락