PostgreSQL에서 동시 DELETE / INSERT 관련 잠금 문제


35

이것은 매우 간단하지만 PG의 기능 (v9.0)에 당황합니다. 우리는 간단한 테이블로 시작합니다 :

CREATE TABLE test (id INT PRIMARY KEY);

그리고 몇 줄 :

INSERT INTO TEST VALUES (1);
INSERT INTO TEST VALUES (2);

내가 좋아하는 JDBC 쿼리 도구 (ExecuteQuery)를 사용하여이 테이블이있는 DB에 두 개의 세션 창을 연결합니다. 둘 다 트랜잭션 방식입니다 (예 : auto-commit = false). 그것들을 S1과 S2라고합시다.

각각 동일한 코드 비트 :

1:DELETE FROM test WHERE id=1;
2:INSERT INTO test VALUES (1);
3:COMMIT;

이제 이것을 윈도우에서 한 번에 하나씩 실행하면서 슬로우 모션으로 실행하십시오.

S1-1 runs (1 row deleted)
S2-1 runs (but is blocked since S1 has a write lock)
S1-2 runs (1 row inserted)
S1-3 runs, releasing the write lock
S2-1 runs, now that it can get the lock. But reports 0 rows deleted. HUH???
S2-2 runs, reports a unique key constraint violation

이제 이것은 SQLServer에서 잘 작동합니다. S2가 삭제를 수행하면 삭제 된 1 개의 행을보고합니다. 그런 다음 S2의 삽입이 정상적으로 작동합니다.

PostgreSQL이 해당 행이있는 테이블의 인덱스를 잠그고있는 반면 SQLServer는 실제 키 값을 잠급니다.

내가 맞아? 이것이 작동하도록 할 수 있습니까?

답변:


39

Mat와 Erwin은 둘 다 맞으며 의견에 맞지 않는 방식으로 그들이 말한 것을 더 확장하기 위해 또 다른 대답을 추가하고 있습니다. 그들의 답변이 모든 사람을 만족시키는 것은 아니며, PostgreSQL 개발자와 상담해야한다는 제안이 있었으며, 저는 하나입니다.

여기서 중요한 점은 SQL 표준에 따라 READ COMMITTED트랜잭션 격리 수준 에서 실행되는 트랜잭션 내에서 커밋되지 않은 트랜잭션 작업을 볼 수 없다는 제한이 있습니다. 커밋 된 트랜잭션의 작업이 표시됩니다 구현에 의존합니다. 당신이 지적하는 것은 두 제품이 그것을 구현하기 위해 선택한 방법의 차이입니다. 두 가지 구현 모두 표준 요구 사항을 위반하지 않습니다.

PostgreSQL에서 발생하는 세부 사항은 다음과 같습니다.

S1-1이 실행 됨 (1 행 삭제)

S1이 여전히 롤백 될 수 있기 때문에 이전 행은 그대로 남아 있지만 S1은 이제 행에 대한 잠금을 유지하므로 행을 수정하려는 다른 세션이 S1이 커밋 또는 롤백되는지를 기다립니다. 모든이 읽고 그들이 그것을 잠금을 시도하지 않는 한 여전히 이전 행을 볼 수있는 테이블의 SELECT FOR UPDATESELECT FOR SHARE.

S2-1이 실행되지만 S1에 쓰기 잠금이 있으므로 차단됩니다.

S2는 이제 S1의 결과를보기 위해 기다려야합니다. S1이 커밋이 아닌 롤백 인 경우 S2는 행을 삭제합니다. 롤백하기 전에 S1이 새 버전을 삽입 한 경우 새 버전이 다른 트랜잭션의 관점에서 존재하지 않았거나 이전 버전이 다른 트랜잭션의 관점에서 삭제되지 않았을 것입니다.

S1-2 런 (1 열 삽입)

이 행은 이전 행과 독립적입니다. id = 1 인 행이 업데이트 된 경우 이전 버전과 새 버전이 관련되며 S2는 행이 차단되지 않은 경우 업데이트 된 버전의 행을 삭제할 수 있습니다. 새 행은 과거에 존재했던 일부 행과 동일한 값을 가지므로 해당 행의 업데이트 버전과 동일하지 않습니다.

S1-3이 실행되고 쓰기 잠금이 해제됩니다

따라서 S1의 변경 사항이 유지됩니다. 한 행이 사라졌습니다. 한 행이 추가되었습니다.

S2-1이 작동하여 잠금을 얻을 수 있습니다. 그러나 보고서 0 행이 삭제되었습니다. 허 ???

내부적으로 일어나는 것은 행의 한 버전에서 동일한 행의 다음 버전으로 포인터가 업데이트된다는 것입니다. 행이 삭제되면 다음 버전이 없습니다. READ COMMITTED쓰기 충돌시 블록에서 트랜잭션이 깨어날 때 해당 업데이트 체인을 따라 끝까지 이어집니다. 행이 삭제되지 않았고 여전히 쿼리의 선택 기준을 충족하는 경우 처리됩니다. 이 행이 삭제되었으므로 S2의 쿼리가 계속 진행됩니다.

S2는 테이블 스캔 중에 새 행에 도달 할 수도 있고 그렇지 않을 수도 있습니다. 그렇다면 S2의 DELETE명령문이 시작된 후 새 행이 작성 되어 행 세트의 일부가 아닌 것을 볼 수 있습니다.

PostgreSQL이 S2의 전체 DELETE 문을 처음부터 새 스냅 샷으로 다시 시작하면 SQL Server와 동일하게 작동합니다. PostgreSQL 커뮤니티는 성능상의 이유로 그렇게하지 않았습니다. 이 간단한 경우 성능의 차이를 결코 눈치 채지 못할 것입니다. 그러나 DELETE막혔을 때 천만 개의 행이 있다면 확실히 그럴 것입니다. 더 빠른 버전은 여전히 ​​표준의 요구 사항을 준수하기 때문에 PostgreSQL이 성능을 선택한 위치에 상충 관계가 있습니다.

S2-2 실행, 고유 키 제약 조건 위반보고

물론 행이 이미 존재합니다. 이것은 그림에서 가장 놀라운 부분입니다.

여기에는 놀라운 동작이 있지만 모든 것은 SQL 표준을 따르며 표준에 따라 "구현 별"의 범위 내에 있습니다. 다른 구현의 동작이 모든 구현에 존재한다고 가정하면 PostgreSQL은 READ COMMITTED격리 수준 에서 직렬화 실패를 피하기 위해 매우 열심히 노력 하고 있으며이를 달성하기 위해 다른 제품과 다른 일부 동작을 허용합니다.

개인적으로 저는 모든 제품 구현 READ COMMITTED에서 트랜잭션 격리 수준을 좋아하지 않습니다 . 그들은 모두 경쟁 조건이 거래 관점에서 놀라운 행동을하도록 허용합니다. 한 제품이 허용하는 이상한 동작에 익숙해지면 "정상"과 다른 제품이 선택한 절충을 고려하는 경향이 있습니다. 그러나 모든 제품은 실제로로 구현되지 않은 모드에 대해 일종의 절충점을 만들어야 합니다. PostgreSQL 개발자가 줄을 서기로 선택한 곳에서는 차단을 최소화하고 (읽기가 쓰기를 차단하지 않고 쓰기가 읽기를 차단하지 않음) 직렬화 실패 가능성을 최소화합니다.SERIALIZABLEREAD COMMITTED

표준은 SERIALIZABLE트랜잭션을 기본값으로 요구 하지만, 대부분의 제품은 더 느슨한 트랜잭션 격리 수준보다 성능이 저하되기 때문에 그렇게하지 않습니다. 일부 제품 SERIALIZABLE은 선택 시 실제로 직렬화 가능한 트랜잭션을 제공하지 않으며, 특히 Oracle 및 9.1 이전의 PostgreSQL 버전입니다. 그러나 SERIALIZABLE실제 거래를 사용하는 것은 경쟁 조건으로 인한 놀라운 영향을 피할 수있는 유일한 방법이며, 경쟁 조건을 피하기 위해 SERIALIZABLE트랜잭션을 항상 차단하거나 경쟁 조건이 발생하지 않도록 일부 트랜잭션을 롤백해야합니다. SERIALIZABLE트랜잭션 의 가장 일반적인 구현은 S2PL (Strict Two-Phase Locking)이며 차단 및 직렬화 실패 (교착 상태 형태)를 모두 가지고 있습니다.

전체 공개 : 나는 MIT의 Dan Ports와 함께 Serializable Snapshot Isolation이라는 새로운 기술을 사용하여 PostgreSQL 버전 9.1에 진정으로 직렬화 가능한 트랜잭션을 추가했습니다.


이 작업을 수행하는 데 가장 싼 (건방진?) 방법이 두 개의 DELETES와 INSERT를 발행하는 것이 궁금합니다. 내 제한된 (2 스레드) 테스트에서는 정상적으로 작동했지만 많은 스레드에 적용되는지 확인하려면 더 테스트해야합니다.
DaveyBob

READ COMMITTED트랜잭션 을 사용 하는 한 경쟁 조건이 있습니다. 첫 번째 DELETE시작 후 두 번째 DELETE시작 전에 다른 트랜잭션이 새 행을 삽입하면 어떻게됩니까? 보다 엄격한 거래로 SERIALIZABLE가까운 경쟁 조건의 두 가지 방법을 통해있는 홍보 (행이 삭제 될 때 그러나 그것은 도움이되지 않습니다) 충돌 및 구체화 충돌. 삭제 된 모든 행에 대해 업데이트 된 "id"테이블을 갖거나 테이블을 명시 적으로 잠그면 충돌을 구체화 할 수 있습니다. 또는 오류시 재 시도를 사용하십시오.
kgrittn

다시 시도하십시오. 소중한 통찰력에 감사드립니다!
DaveyBob

21

PostgreSQL 9.2 의 읽기 커밋 격리 수준에 대한 설명에 따르면 이것은 의도적으로 설계된 것입니다 .

UPDATE, DELETE, SELECT FOR UPDATE 및 SELECT FOR SHARE 명령은 대상 행 검색 측면에서 SELECT와 동일하게 동작합니다 . 명령 시작 시간 1 으로 커밋 된 대상 행만 찾습니다 . 그러나 이러한 목표 행은 발견 될 때까지 다른 동시 트랜잭션에 의해 이미 갱신 (또는 삭제 또는 잠김)되었을 수 있습니다. 이 경우, 유망 업데 이터는 첫 번째 업데이트 트랜잭션이 커밋 또는 롤백 될 때까지 기다립니다 (아직 진행중인 경우). 첫 번째 업데이터가 롤백되면 그 효과는 무시되고 두 번째 업데이터는 원래 찾은 행의 업데이트를 진행할 수 있습니다. 첫 번째 업데이터가 커밋하면 두 번째 업데이터는 첫 번째 업데이터가 삭제 한 행을 무시합니다. 2그렇지 않으면 업데이트 된 버전의 행에 작업을 적용하려고 시도합니다.

당신이 삽입 행 S1때 아직 존재하지 않았다 S2'는 s의 DELETE시작. 따라서 위의 S2( 1 ) 에 따라 삭제로 표시되지 않습니다 . 한 S1삭제는 무시 S2DELETE(에 따라 2 ).

따라서에서 S2삭제는 아무 것도 수행하지 않습니다. 삽입 그래도 함께 올 때, 한 것을 않는S1의 삽입 :

커밋 된 읽기 모드는 해당 순간까지 커밋 된 모든 트랜잭션을 포함 하는 새 스냅 샷으로 각 명령을 시작하기 때문에 동일한 트랜잭션의 후속 명령은 커밋 된 동시 트랜잭션의 영향을 나타 냅니다. 위의 문제는 단일 명령이 데이터베이스에 대해 일관된 뷰를 보는지 여부입니다.

따라서 삽입 시도 S2가 제약 조건 위반 으로 실패합니다.

반복 가능한 읽기 또는 직렬화 가능을 사용하여 해당 문서를 계속 읽 더라도 문제를 완전히 해결할 수는 없습니다. 두 번째 세션은 삭제시 직렬화 오류로 실패합니다.

그래야 트랜잭션을 다시 시도 할 수 있습니다.


고마워 매트. 그것이 일어나고있는 것처럼 보이지만 그 논리에는 결함이있는 것 같습니다. READ_COMMITTED iso 레벨 에서이 두 명령문 tx 내에서 성공 해야합니다 .DELETE FROM test WHERE ID = 1 테스트 값에 삽입 (1) 행을 삭제하고 행을 삽입하면 삽입이 성공해야합니다. SQLServer가이 권한을 얻습니다. 현재 두 데이터베이스 모두에서 작동해야하는 제품에서이 상황을 처리하는 데 어려움을 겪고 있습니다.
DaveyBob

11

@Mat의 탁월한 답변에 전적으로 동의합니다 . 의견에 맞지 않기 때문에 다른 답변 만 작성합니다.

귀하의 의견에 대한 답변 : DELETES2 의 in은 이미 특정 행 버전에 연결되어 있습니다. 그 동안 S1이이를 종료했기 때문에 S2는 성공한 것으로 간주합니다. 한 눈에 분명하지는 않지만 일련의 이벤트는 사실상 다음과 같습니다.

   S1 삭제 성공  
S2 DELETE (프록시에 의해 성공-S1의 DELETE)  그 동안 
   S1은 삭제 된 값을 가상으로 다시 삽입 합니다.  
고유 키 제약 조건 위반으로 S2 INSERT 실패

디자인에 의한 것입니다. 실제로 SERIALIZABLE요구 사항에 대해 트랜잭션 을 사용해야 하고 직렬화 실패를 다시 시도해야합니다.


1

DEFERRABLE 기본 키를 사용하고 다시 시도하십시오.


팁 주셔서 감사하지만 DEFERRABLE을 사용하면 아무런 차이가 없습니다. 문서 는 꼭 읽어야 하지만 읽지 않아도됩니다.
DaveyBob

-2

우리는 또한이 문제에 직면했습니다. 우리의 솔루션은 select ... for update전에 추가 delete from ... where됩니다. 격리 수준은 읽기 커밋해야합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.