READ COMMITTED에서도 MySQL InnoDB가 삭제시 기본 키를 잠급니다


11

머리말

우리의 응용 프로그램은 DELETE병렬로 쿼리를 실행하는 여러 스레드를 실행 합니다. 쿼리는 격리 된 데이터에 영향을줍니다. 즉, DELETE별도의 스레드에서 동일한 행에 동시 가 발생할 가능성이 없어야 합니다. 그러나 문서 당 MySQL은 DELETE명령문에 대해 소위 '다음 키'잠금을 사용 하여 일치하는 키와 일부 간격을 모두 잠급니다. 이로 인해 교착 상태가 발생하고 우리가 찾은 유일한 솔루션은 READ COMMITTED격리 수준 을 사용하는 것입니다.

문제

s의 거대한 테이블로 복잡한 DELETE명령문을 실행할 때 문제가 발생합니다 JOIN. 특별한 경우에는 행이 두 개 뿐인 경고가있는 테이블이 있지만 쿼리는 두 개의 개별 INNER JOINed 테이블 에서 특정 엔티티에 속하는 모든 경고를 삭제해야합니다 . 쿼리는 다음과 같습니다.

DELETE pw 
FROM proc_warnings pw 
INNER JOIN day_position dp 
   ON dp.transaction_id = pw.transaction_id 
INNER JOIN ivehicle_days vd 
   ON vd.id = dp.ivehicle_day_id 
WHERE vd.ivehicle_id=? AND dp.dirty_data=1

day_position 테이블이 충분히 클 때 (내 테스트의 경우 1448 행이 있음) READ COMMITTED격리 모드 를 사용하는 모든 트랜잭션 은 전체 proc_warnings 테이블을 차단 합니다.

이 문제는 MySQL 5.1 (5.1.59에서 확인)과 MySQL 5.5 (MySQL 5.5.24에서 확인) 모두 에서이 샘플 데이터 ( http://yadi.sk/d/QDuwBtpW1BxB9) 에서 항상 재현됩니다 .

편집 : 링크 된 샘플 데이터에는 쿼리 테이블에 대한 스키마 및 인덱스가 포함되어 있으며 편의를 위해 여기에 재현되었습니다.

CREATE TABLE  `proc_warnings` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `transaction_id` int(10) unsigned NOT NULL,
    `warning` varchar(2048) NOT NULL,
    PRIMARY KEY (`id`),
    KEY `proc_warnings__transaction` (`transaction_id`)
);

CREATE TABLE  `day_position` (
    `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
    `transaction_id` int(10) unsigned DEFAULT NULL,
    `sort_index` int(11) DEFAULT NULL,
    `ivehicle_day_id` int(10) unsigned DEFAULT NULL,
    `dirty_data` tinyint(4) DEFAULT NULL,
    PRIMARY KEY (`id`),
    KEY `day_position__trans` (`transaction_id`),
    KEY `day_position__is` (`ivehicle_day_id`,`sort_index`),
    KEY `day_position__id` (`ivehicle_day_id`,`dirty_data`)
) ;

CREATE TABLE  `ivehicle_days` (
    `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
    `d` date DEFAULT NULL,
    `sort_index` int(11) DEFAULT NULL,
    `ivehicle_id` int(10) unsigned DEFAULT NULL,
    PRIMARY KEY (`id`),
    KEY `ivehicle_days__is` (`ivehicle_id`,`sort_index`),
    KEY `ivehicle_days__d` (`d`)
);

트랜잭션 당 쿼리는 다음과 같습니다.

  • 거래 1

    set transaction isolation level read committed;
    set autocommit=0;
    begin;
    DELETE pw 
    FROM proc_warnings pw 
    INNER JOIN day_position dp 
        ON dp.transaction_id = pw.transaction_id 
    INNER JOIN ivehicle_days vd 
        ON vd.id = dp.ivehicle_day_id 
    WHERE vd.ivehicle_id=2 AND dp.dirty_data=1;
    
  • 거래 2

    set transaction isolation level read committed;
    set autocommit=0;
    begin;
    DELETE pw 
    FROM proc_warnings pw 
    INNER JOIN day_position dp 
        ON dp.transaction_id = pw.transaction_id 
    INNER JOIN ivehicle_days vd 
        ON vd.id = dp.ivehicle_day_id 
    WHERE vd.ivehicle_id=13 AND dp.dirty_data=1;
    

그중 하나는 항상 '잠금 대기 시간 초과 초과 ...'오류와 함께 실패합니다. information_schema.innodb_trx다음 행 이 포함됩니다.

| trx_id     | trx_state   | trx_started           | trx_requested_lock_id  | trx_wait_started      | trx_wait | trx_mysql_thread_id | trx_query |
| '1A2973A4' | 'LOCK WAIT' | '2012-12-12 20:03:25' | '1A2973A4:0:3172298:2' | '2012-12-12 20:03:25' | '2'      | '3089'              | 'DELETE pw FROM proc_warnings pw INNER JOIN day_position dp ON dp.transaction_id = pw.transaction_id INNER JOIN ivehicle_days vd ON vd.id = dp.ivehicle_day_id WHERE vd.ivehicle_id=13 AND dp.dirty_data=1' |
| '1A296F67' | 'RUNNING'   | '2012-12-12 19:58:02' | NULL                   | NULL | '7' | '3087' | NULL |

information_schema.innodb_locks

| lock_id                | lock_trx_id | lock_mode | lock_type | lock_table | lock_index | lock_space | lock_page | lock_rec | lock_data |
| '1A2973A4:0:3172298:2' | '1A2973A4'  | 'X'       | 'RECORD'  | '`deadlock_test`.`proc_warnings`' | '`PRIMARY`' | '0' | '3172298' | '2' | '53' |
| '1A296F67:0:3172298:2' | '1A296F67'  | 'X'       | 'RECORD'  | '`deadlock_test`.`proc_warnings`' | '`PRIMARY`' | '0' | '3172298' | '2' | '53' |

보시다시피 두 쿼리 모두 X기본 키 = 53 인 행에 독점 잠금을 원합니다 . 그러나 두 쿼리 모두 proc_warnings테이블 에서 행을 삭제하지 않아야 합니다. 색인이 잠겨있는 이유를 이해하지 못합니다. 또한, proc_warnings테이블이 비어 있거나 day_position테이블에 적은 수의 행 (예 : 100 개의 행)이 포함되어 있으면 인덱스가 잠기지 않습니다 .

추가 조사는 EXPLAIN유사한 SELECT쿼리 를 실행 하는 것이 었습니다 . 쿼리 최적화 프로그램이 인덱스를 사용하여 proc_warnings테이블 을 쿼리하지 않으며 이것이 전체 기본 키 인덱스를 차단하는 이유를 상상할 수있는 유일한 이유입니다.

단순화 된 사례

두 개의 레코드가있는 테이블이 두 개만 있지만 자식 테이블의 부모 테이블 참조 열에 인덱스가없는 경우 문제를 더 간단한 경우에 재현 할 수도 있습니다.

parent테이블 만들기

CREATE TABLE `parent` (
  `id` int(10) unsigned NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB

child테이블 만들기

CREATE TABLE `child` (
  `id` int(10) unsigned NOT NULL,
  `parent_id` int(10) unsigned DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB

테이블 채우기

INSERT INTO `parent` (id) VALUES (1), (2);
INSERT INTO `child` (id, parent_id) VALUES (1, NULL), (2, NULL);

두 개의 병렬 트랜잭션에서 테스트하십시오.

  • 거래 1

    SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
    SET AUTOCOMMIT=0;
    BEGIN;
    DELETE c FROM child c 
      INNER JOIN parent p ON p.id = c.parent_id 
    WHERE p.id = 1;
    
  • 거래 2

    SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
    SET AUTOCOMMIT=0;
    BEGIN;
    DELETE c FROM child c 
      INNER JOIN parent p ON p.id = c.parent_id 
    WHERE p.id = 2;
    

두 경우 모두 공통적 인 부분은 MySQL이 인덱스를 사용하지 않는다는 것입니다. 나는 그것이 전체 테이블을 잠그는 이유라고 생각합니다.

우리의 솔루션

현재 볼 수있는 유일한 해결책은 기본 잠금 대기 시간 제한을 50 초에서 500 초로 늘려서 스레드 정리를 완료하는 것입니다. 그런 다음 손가락을 교차시킵니다.

도움을 주셔서 감사합니다.


질문이 있습니다. 거래에서 COMMIT를 실행 했습니까?
RolandoMySQLDBA

물론이야. 문제는 다른 모든 트랜잭션 중 하나가 변경을 커밋 할 때까지 기다려야한다는 것입니다. 간단한 테스트 사례에는 문제를 재현하는 방법을 보여주는 commit 문이 포함되어 있지 않습니다. 대기 중이 아닌 트랜잭션에서 커밋 또는 롤백을 실행하면 동시에 잠금이 해제되고 대기중인 트랜잭션이 완료됩니다.
vitalidze

MySQL이 두 경우 모두 인덱스를 사용하지 않는다고 말할 때 실제 시나리오에 인덱스가 없기 때문입니까? 색인이있는 경우 색인을 제공 할 수 있습니까? 아래에 게시 된 색인 제안 중 하나를 시도 할 수 있습니까? 인덱스가없고 추가를 시도 할 수없는 경우 MySQL은 각 스레드에서 처리하는 데이터 세트를 제한 할 수 없습니다. 이 경우 N 스레드는 단순히 서버 워크로드에 N 배를 곱한 것이므로 한 스레드가 {WHERE vd.ivehicle_id IN (2, 13) AND dp.dirty_data = 1;}.
JM Hicks

좋아, 연결된 샘플 데이터 파일에서 인덱스를 찾아 냈다.
JM Hicks

몇 가지 추가 질문 : 1) day_position테이블이 너무 느리게 실행될 때 테이블에 일반적으로 포함 되는 행 수는 500 초로 제한 시간을 초과해야합니까? 2) 샘플 데이터 만 있으면 얼마나 오래 걸립니까?
JM Hicks

답변:


3

새로운 답변 (MySQL 스타일의 동적 SQL) : 좋아,이 포스터는 다른 포스터 중 하나가 설명하는 방식으로 문제를 해결합니다. 상호 호환되지 않는 독점 잠금이 획득되는 순서를 반대로하여 발생 횟수에 관계없이 발생합니다. 트랜잭션 실행 종료시 최소 시간

이는 명령문의 읽기 부분을 자체의 select 명령문으로 분리하고 명령문 모양의 순서로 인해 마지막으로 강제 실행되고 proc_warnings 테이블에만 영향을주는 delete 명령문을 동적으로 생성하여 수행됩니다.

데모는 SQL Fiddle에서 사용할 수 있습니다.

링크 는 샘플 데이터가있는 스키마와에 일치하는 행에 대한 간단한 쿼리를 보여줍니다 ivehicle_id=2. 그 중 어느 것도 삭제되지 않았으므로 2 개의 행이 생성됩니다.

링크 는 동일한 스키마, 샘플 데이터를 표시하지만 값 2를 DeleteEntries 스토어드 프로그램에 전달하여 SP에게에 대한 proc_warnings항목 을 삭제하도록 지시 합니다 ivehicle_id=2. 행에 대한 간단한 쿼리는 모두 성공적으로 삭제되었으므로 결과를 반환하지 않습니다. 데모 링크는 코드가 삭제하려는 의도대로 작동한다는 것을 강조합니다. 적절한 테스트 환경을 가진 사용자는 이것이 차단 된 스레드의 문제를 해결하는지 여부에 대해 의견을 줄 수 있습니다.

편의를위한 코드는 다음과 같습니다.

CREATE PROCEDURE DeleteEntries (input_vid INT)
BEGIN

    SELECT @idstring:= '';
    SELECT @idnum:= 0;
    SELECT @del_stmt:= '';

    SELECT @idnum:= @idnum+1 idnum_col, @idstring:= CONCAT(@idstring, CASE WHEN CHARACTER_LENGTH(@idstring) > 0 THEN ',' ELSE '' END, CAST(id AS CHAR(10))) idstring_col
    FROM proc_warnings
    WHERE EXISTS (
        SELECT 0
        FROM day_position
        WHERE day_position.transaction_id = proc_warnings.transaction_id
        AND day_position.dirty_data = 1
        AND EXISTS (
            SELECT 0
            FROM ivehicle_days
            WHERE ivehicle_days.id = day_position.ivehicle_day_id
            AND ivehicle_days.ivehicle_id = input_vid
        )
    )
    ORDER BY idnum_col DESC
    LIMIT 1;

    IF (@idnum > 0) THEN
        SELECT @del_stmt:= CONCAT('DELETE FROM proc_warnings WHERE id IN (', @idstring, ');');

        PREPARE del_stmt_hndl FROM @del_stmt;
        EXECUTE del_stmt_hndl;
        DEALLOCATE PREPARE del_stmt_hndl;
    END IF;
END;

다음은 트랜잭션 내에서 프로그램을 호출하는 구문입니다.

CALL DeleteEntries(2);

ORIGINAL ANSWER (여전히 너무 허름하지 않다고 생각합니다) 2 문제처럼 보입니다. 1) 쿼리 속도가 느림 2) 예기치 않은 잠금 동작

이슈 # 1과 관련하여 느린 쿼리는 종종 탠덤 쿼리 문 단순화 및 인덱스에 대한 유용한 추가 또는 수정에서 동일한 두 가지 기술로 해결됩니다. 이미 색인에 연결했습니다. 색인이 없으면 옵티마이 저가 처리 할 제한된 행 세트를 검색 할 수 없으며 각 테이블의 각 행에서 추가 행마다 곱하는 것이 수행해야하는 추가 작업량을 스캔했습니다.

스키마 및 색인 게시 후 수정 : 그러나 인덱스 구성이 양호해야 쿼리 성능이 가장 뛰어납니다. 이렇게하려면 더 큰 인덱스를 사용하고 추가 인덱스 구조가 추가 된 동일한 테이블에서 삽입 성능이 현저하게 저하되어 삭제 성능이 향상되고 삭제 성능이 향상 될 수 있습니다.

더 나은 것 :

CREATE TABLE  `day_position` (
    ...,
    KEY `day_position__id_rvrsd` (`dirty_data`, `ivehicle_day_id`)

) ;


CREATE TABLE  `ivehicle_days` (
    ...,
    KEY `ivehicle_days__vid_no_sort_index` (`ivehicle_id`)
);

너무 많이 개정 : 실행하는 데 시간이 오래 걸리기 때문에 dirty_data를 색인에 남겨두고 ivehicle_day_id 다음에 색인 순서로 배치하면 확실하지 않습니다. 먼저 있어야합니다.

그러나 내가 손에 든다면,이 시점에서 오래 걸리는 데 필요한 양의 데이터가 있어야하기 때문에 인덱스를 다루는 데 가장 적합한 인덱싱을 얻기 위해 모든 인덱스를 다루려고합니다. 문제의 해당 부분을 배제 할 다른 방법이 없다면 문제 해결 시간을 구입할 수 있습니다.

최고 / 표지 색인 :

CREATE TABLE  `day_position` (
    ...,
    KEY `day_position__id_rvrsd_trnsid_cvrng` (`dirty_data`, `ivehicle_day_id`, `transaction_id`)
) ;

CREATE TABLE  `ivehicle_days` (
    ...,
    UNIQUE KEY `ivehicle_days__vid_id_cvrng` (ivehicle_id, id)
);

CREATE TABLE  `proc_warnings` (

    .., /*rename primary key*/
    CONSTRAINT pk_proc_warnings PRIMARY KEY (id),
    UNIQUE KEY `proc_warnings__transaction_id_id_cvrng` (`transaction_id`, `id`)
);

마지막 두 가지 변경 제안에서 추구하는 두 가지 성능 최적화 목표가 있습니다.
1) 연속적으로 액세스 된 테이블의 검색 키가 현재 액세스 된 테이블에 대해 리턴 된 클러스터 된 키 결과와 동일하지 않은 경우, 우리는 필요한 것을 제거합니다. 클러스터 된 인덱스에 대한 두 번째 인덱스 탐색 스캔 작업 세트
2) 후자가 그렇지 않은 경우, 인덱스가 인덱스를 유지하기 때문에 옵티마이 저가 더 효율적인 조인 알고리즘을 선택할 수있는 가능성은 여전히 ​​남아 있습니다. 정렬 된 순서로 필요한 조인 키.

귀하의 쿼리는 가능한 한 단순화 된 것처럼 보입니다 (나중에 편집 할 경우 여기에 복사).

DELETE pw 
FROM proc_warnings pw 
INNER JOIN day_position dp 
    ON dp.transaction_id = pw.transaction_id 
INNER JOIN ivehicle_days vd 
    ON vd.id = dp.ivehicle_day_id 
WHERE vd.ivehicle_id=2 AND dp.dirty_data=1;

물론 쿼리 최적화 프로그램이 진행되는 방식에 영향을 미치는 조인 순서 작성에 대한 내용이없는 경우 인덱스 힌트를 포함하여이 힌트를 포함하여 다른 사람들이 제공 한 다시 쓰기 제안 중 일부를 시도해 볼 수 있습니다 (선택 사항).

DELETE FROM proc_warnings
FORCE INDEX (`proc_warnings__transaction_id_id_cvrng`, `pk_proc_warnings`)
WHERE EXISTS (
    SELECT 0
    FROM day_position
    FORCE INDEX (`day_position__id_rvrsd_trnsid_cvrng`)  
    WHERE day_position.transaction_id = proc_warnings.transaction_id
    AND day_position.dirty_data = 1
    AND EXISTS (
        SELECT 0
        FROM ivehicle_days
        FORCE INDEX (`ivehicle_days__vid_id_cvrng`)  
        WHERE ivehicle_days.id = day_position.ivehicle_day_id
        AND ivehicle_days.ivehicle_id = ?
    )
);

# 2와 관련하여 예기치 않은 잠금 동작.

보시다시피 두 쿼리 모두 기본 키 = 53 인 행에서 독점 X 잠금을 원합니다. 그러나 둘 다 proc_warnings 테이블에서 행을 삭제하지 않아야합니다. 색인이 잠겨있는 이유를 이해하지 못합니다.

잠길 데이터 행이 클러스터 된 인덱스에 있기 때문에 잠겨있는 인덱스 일 것입니다. 즉, 단일 데이터 행 자체가 인덱스에 있습니다.


1) http://dev.mysql.com/doc/refman/5.1/en/innodb-locks-set.html 에 따라 잠겨 있습니다.

... DELETE는 일반적으로 SQL 문 처리시 스캔되는 모든 인덱스 레코드에 레코드 잠금을 설정합니다. 명령문에 행을 제외시킬 WHERE 조건이 있는지 여부는 중요하지 않습니다. InnoDB는 정확한 WHERE 조건을 기억하지 않지만 스캔 된 인덱스 범위 만 알고 있습니다.

또한 위에서 언급했습니다.

READ COMMITTED의 주요 기능은 잠금을 처리하는 방법입니다. 일치하지 않는 행의 인덱스 잠금을 해제해야하지만 일치하지는 않습니다.

http://dev.mysql.com/doc/refman/5.1/en/set-transaction.html#isolevel_read-committed에 대한 다음 참조를 제공했습니다.

동일한 참조에 따라 잠금이 해제되는 조건이 있다는 점을 제외하고, 귀하와 동일한 상태 :

또한 MySQL이 WHERE 조건을 평가 한 후 일치하지 않는 행에 대한 레코드 잠금이 해제됩니다.

이 매뉴얼 페이지 http://dev.mysql.com/doc/refman/5.1/en/innodb-record-level-locks.html 에서도 반복됩니다.

READ COMMITTED 격리 수준을 사용하거나 innodb_locks_unsafe_for_binlog를 활성화하면 다른 효과도 있습니다. MySQL이 WHERE 조건을 평가 한 후 일치하지 않는 행에 대한 레코드 잠금이 해제됩니다.

따라서 잠금을 해제하기 전에 WHERE 조건을 평가해야합니다. 불행히도 WHERE 조건이 언제 평가되는지는 알려지지 않았으며 아마도 옵티마이 저가 작성한 계획에서 다른 계획으로 변경 될 수 있습니다. 그러나 잠금 해제는 쿼리 실행 성능에 달려 있으며 위에서 언급 한 최적화는 명령문을 신중하게 작성하고 인덱스를 신중하게 사용하는 데 달려 있습니다. 더 나은 테이블 디자인으로 향상시킬 수도 있지만 별도의 질문에 가장 적합 할 것입니다.

또한 proc_warnings 테이블이 비어있는 경우에도 인덱스가 잠기지 않습니다.

데이터베이스는 인덱스 내에 레코드가 없으면이를 잠글 수 없습니다.

또한 day_position 테이블에 적은 수의 행 (예 : 100 개의 행)이 포함되어 있으면 인덱스가 잠기지 않습니다.

이는 통계 변경으로 인한 다른 실행 계획, 훨씬 더 작은 데이터 세트로 인한 실행 속도가 너무 빠르기 때문에 관찰하기에 너무 간단한 잠금과 같은 수많은 것을 의미 할 수 있습니다. 조인 작업.


WHERE쿼리가 완료 될 때 조건이 평가됩니다. 그렇지 않습니까? 동시 쿼리 중 일부가 실행 된 직후에 잠금이 해제된다고 생각했습니다. 그것은 자연스러운 행동입니다. 그러나 이것은 일어나지 않습니다. 이 스레드에서 제안 된 쿼리 중 어느 것도 proc_warnings테이블 에서 클러스터 된 인덱스 잠금을 피하는 데 도움이되지 않습니다 . MySQL에 버그를 제기 할 것입니다. 당신의 도움을 주셔서 감사합니다.
vitalidze

나는 그들이 잠금 동작을 피하기를 기대하지 않을 것입니다. 나는 그것이 문서가 그것이 우리가 쿼리를 처리하기를 원하는 방식인지 아닌지에 따라 문서에 나와 있다고 생각하기 때문에 잠길 것으로 기대합니다. 성능 문제를 제거하면 동시 쿼리가 분명히 (500+ 초 시간 초과) 오랫동안 차단되지 않을 것으로 기대합니다.
JM Hicks

{WHERE}는 조인 처리 중에 조인 계산에 포함되는 행을 제한하는 데 사용할 수있는 것처럼 보이지만 전체 조인 집합이 될 때까지 잠긴 행마다 {WHERE} 절을 평가할 수있는 방법을 알 수 없습니다 뿐만 아니라 계산. 즉, 분석을 위해 "쿼리가 완료되면 WHERE 조건이 평가됩니다"라고 의심해야합니다. 그러나 그것은 동일한 전반적인 결론으로 ​​이어지고, 성능이 해결되어야하며, 명백한 동시성 정도가 비례 적으로 증가 할 것입니다.
JM Hicks

적절한 인덱스는 proc_warnings 테이블에서 발생하는 전체 테이블 스캔을 잠재적으로 제거 할 수 있습니다. 이를 위해서는 쿼리 최적화 프로그램이 잘 작동하고, 인덱스, 쿼리 및 데이터가 잘 작동해야합니다. 매개 변수 값은 두 쿼리간에 겹치지 않는 목표 테이블의 행 끝에서 평가되어야합니다. 인덱스는 쿼리 최적화 프로그램에 해당 행을 효율적으로 검색 할 수있는 수단을 제공해야합니다. 잠재적 인 검색 효율성을 실현하고 그러한 계획을 선택하려면 최적화 프로그램이 필요합니다.
JM Hicks

매개 변수 값, 인덱스, 겹치지 않는 결과가 proc_warnings 테이블의 결과 및 옵티 마이저 계획 선택 사이에 모두 잘 맞는 경우 각 스레드에 대해 쿼리를 실행하는 데 필요한 시간 동안 잠금이 생성 될 수 있지만 해당 잠금은 그렇지 않은 경우 겹치면 다른 스레드의 잠금 요청과 충돌하지 않습니다.
JM Hicks

3

READ_COMMITTED 가 어떻게 이런 상황을 일으킬 수 있는지 알 수 있습니다.

READ_COMMITTED 는 다음 세 가지를 허용합니다.

  • READ_COMMITTED 격리 수준을 사용하여 다른 트랜잭션에서 커밋 된 변경 내용을 볼 수 있습니다.
  • 반복 불가능한 읽기 : 매번 다른 결과를 얻을 수있는 가능성으로 동일한 검색을 수행하는 트랜잭션.
  • Phantoms : 트랜잭션에 미리 표시되지 않은 행이 나타날 수 있습니다.

트랜잭션은 다음과의 연결을 유지해야하기 때문에 트랜잭션 자체에 대한 내부 패러다임을 만듭니다.

  • InnoDB 버퍼 풀 (커밋은 여전히 ​​진행되지 않은 상태)
  • 테이블의 기본 키
  • 혹시
    • 이중 쓰기 버퍼
    • 테이블 스페이스 실행 취소
  • 그림 표현

두 개의 별개의 READ_COMMITTED 트랜잭션이 동일한 방식으로 업데이트되는 동일한 테이블 / 행에 액세스하는 경우 테이블 잠금이 아니라 gen_clust_index (일명 Clustered Index) 내에서 독점 잠금을 기대할 수 있습니다 . 단순화 된 사례의 쿼리가 주어지면 :

  • 거래 1

    SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
    SET AUTOCOMMIT=0;
    BEGIN;
    DELETE c FROM child c 
      INNER JOIN parent p ON p.id = c.parent_id 
    WHERE p.id = 1;
  • 거래 2

    SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
    SET AUTOCOMMIT=0;
    BEGIN;
    DELETE c FROM child c 
      INNER JOIN parent p ON p.id = c.parent_id 
    WHERE p.id = 2;

gen_clust_index에서 동일한 위치를 잠그고 있습니다. "각 트랜잭션마다 다른 기본 키가 있습니다."라고 말할 수 있습니다. 불행히도, 이것은 InnoDB의 눈에는 그렇지 않습니다. id 1과 id 2가 같은 페이지에 상주합니다.

information_schema.innodb_locks질문에 제공된 내용을 다시 확인 하십시오.

| lock_id                | lock_trx_id | lock_mode | lock_type | lock_table | lock_index | lock_space | lock_page | lock_rec | lock_data |
| '1A2973A4:0:3172298:2' | '1A2973A4'  | 'X'       | 'RECORD'  | '`deadlock_test`.`proc_warnings`' | '`PRIMARY`' | '0' | '3172298' | '2' | '53' |
| '1A296F67:0:3172298:2' | '1A296F67'  | 'X'       | 'RECORD'  | '`deadlock_test`.`proc_warnings`' | '`PRIMARY`' | '0' | '3172298' | '2' | '53' |

를 제외하고 lock_id, lock_trx_id로크의 설명의 나머지는 동일하다. 거래는 동일한 수준의 거래 영역에 있기 때문에 (동일한 거래 격리) 실제로 발생 합니다.

나를 믿어, 나는 전에 이런 종류의 상황을 해결했다. 여기에 내 과거 게시물이 있습니다 :


MySQL 문서에서 설명하는 내용에 대해 읽었습니다. 그러나 READ COMMITTED의 주요 특징은 잠금을 처리하는 방법 입니다. 일치하지 않는 행의 인덱스 잠금을 해제해야하지만 일치하지는 않습니다.
vitalidze

오류로 인해 단일 SQL 문만 롤백하는 경우 명령문에 의해 설정된 일부 잠금이 유지 될 수 있습니다. 형식으로 InnoDB의 저장 행 잠금이 어느 문으로 설정 한 잠금 이후에 알 수 있도록하기 때문에 이런 일이 발생 : dev.mysql.com/doc/refman/5.5/en/innodb-deadlock-detection.html
RolandoMySQLDBA

내가 잠금에 대해 같은 페이지에 존재하는 두 행의 가능성을 언급 한 (참조 유의하시기 바랍니다 Look back at information_schema.innodb_locks you supplied in the Question)
RolandoMySQLDBA

단일 명령문 롤백 정보-단일 트랜잭션에서 단일 명령문이 실패하는 경우 여전히 잠금을 보유 할 수있는 것으로 이해합니다. 괜찮아. 내 큰 질문은 DELETE명령문 을 성공적으로 처리 한 후 일치하지 않는 행 잠금을 해제하지 않는 이유 입니다.
vitalidze

두 개의 잠금이 완료되면 하나를 롤백해야합니다. 자물쇠가 걸려있을 수 있습니다. 실무 이론 : 롤백 한 트랜잭션은 재 시도 할 수 있으며 트랜잭션을 보유한 이전 트랜잭션에서 오래된 잠금이 발생할 수 있습니다.
RolandoMySQLDBA

2

나는 질문과 설명을 보았다. 확실하지 않지만 문제가 다음과 같다는 느낌이 들었습니다. 쿼리를 보자.

DELETE pw 
FROM proc_warnings pw 
INNER JOIN day_position dp 
   ON dp.transaction_id = pw.transaction_id 
INNER JOIN ivehicle_days vd 
   ON vd.id = dp.ivehicle_day_id 
WHERE vd.ivehicle_id=? AND dp.dirty_data=1;

동등한 SELECT는 다음과 같습니다.

SELECT pw.id
FROM proc_warnings pw
INNER JOIN day_position dp
   ON dp.transaction_id = pw.transaction_id
INNER JOIN ivehicle_days vd
   ON vd.id = dp.ivehicle_day_id
WHERE vd.ivehicle_id=16 AND dp.dirty_data=1;

설명을 보면 실행 계획이 proc_warnings테이블로 시작한다는 것을 알 수 있습니다. 즉, MySQL은 테이블에서 기본 키를 스캔하고 각 행에 대해 조건이 true인지 확인하고 해당되는 경우 행이 삭제되는지 확인합니다. 즉, MySQL은 전체 기본 키를 잠 가야합니다.

필요한 것은 JOIN 주문을 뒤집는 것입니다. 즉, 모든 거래 ID를 찾아서 테이블에서 vd.ivehicle_id=16 AND dp.dirty_data=1조인합니다 proc_warnings.

즉, 인덱스 중 하나를 패치해야합니다.

ALTER TABLE `day_position`
 DROP INDEX `day_position__id`,
 ADD INDEX `day_position__id`
   USING BTREE (`ivehicle_day_id`, `dirty_data`, `transaction_id`);

삭제 쿼리를 다시 작성하십시오.

DELETE pw
FROM (
  SELECT DISTINCT dp.transaction_id
  FROM ivehicle_days vd
  JOIN day_position dp ON dp.ivehicle_day_id = vd.id
  WHERE vd.ivehicle_id=? AND dp.dirty_data=1
) as tr_id
JOIN proc_warnings pw ON pw.transaction_id = tr_id.transaction_id;

불행히도 이것은 도움이되지 않습니다. 즉 행이 proc_warnings여전히 잠겨 있습니다. 어쨌든 고마워
vitalidze

2

수행하지 않고 트랜잭션 레벨을 설정하면 다음 커밋에만 읽기 커밋을 적용하여 자동 커밋을 설정합니다. 이것은 autocommit = 0 이후에 더 이상 커밋되지 않은 상태임을 의미합니다. 나는 이런 식으로 쓸 것입니다 :

SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
DELETE c FROM child c
INNER JOIN parent p ON
    p.id = c.parent_id
WHERE p.id = 1;

쿼리하여 격리 수준을 확인할 수 있습니다

SELECT @@tx_isolation;

그건 사실이 아니야. SET AUTOCOMMIT=0다음 거래에서 격리 수준을 재설정해야하는 이유는 무엇 입니까? 이전에 아무것도 시작하지 않으면 새로운 거래가 시작된다고 생각합니다 (제 경우). 따라서 다음 START TRANSACTION이나 BEGIN진술을 보다 정확하게하는 것은 필요하지 않습니다. 자동 커밋을 비활성화하는 목적은 DELETE명령문 실행 후 트랜잭션을 열어 두는 것 입니다.
vitalidze

1
@SqlKiwi 이것이이 글을 편집하는 방법이었고, 이것은 ;-)에 댓글을 달았습니다.
jcolebrand
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.