[MySQL] MVCC 및 읽기 보기를 기반으로 트랜잭션의 4가지 격리 수준이 읽기 및 쓰기 시나리오에서 격리를 어떻게 반영하는지 분석합니다.


Linux를 배우기 위해 클라우드 서버 및 기타 클라우드 제품이 필요한 학생은 / --> Tencent Cloud <-- / --> Alibaba Cloud <-- / --> Huawei Cloud <-- / 공식 웹사이트, 경량 클라우드 서버로 이동할 수 있습니다. 연간 112위안까지 저렴한 비용으로 신규 사용자는 첫 주문 시 매우 저렴한 할인 혜택을 누릴 수 있습니다.


 목차

1. 데이터베이스 동시성의 세 가지 시나리오

2. 시나리오 읽기 및 쓰기를 위한 MVCC 

1. 3(4)개의 기록 숨김 컬럼 필드

2. 실행 취소 로그(실행 취소 로그)

3. MVCC 시나리오 시뮬레이션

3.1업데이트 시나리오

3.2삭제 시나리오

3.3삽입

3.4장면 선택

4、읽기 보기

5. RR과 RC의 차이점

5.1 RR 수준에서 현재 읽기와 스냅샷 읽기의 차이점

예 1: jly 수정 전 루트 스냅샷 읽기

예 2: jly 수정 후 루트의 스냅샷 읽기

5.2MySQL의 4가지 격리 수준에 대한 다양한 처리 방법 

3. 장면 쓰기


1. 데이터베이스 동시성의 세 가지 시나리오

읽기-읽기: 문제가 없으며 동시성 제어가 필요하지 않습니다.

읽기-쓰기: 트랜잭션 격리 문제를 일으킬 수 있고 더티 읽기, 팬텀 읽기 및 반복 불가능한 읽기가 발생할 수 있는 스레드 안전 문제가 있습니다.

쓰기-쓰기: 스레드 안전성 문제가 있으며 첫 번째 유형의 업데이트 손실, 두 번째 유형의 업데이트 손실과 같은 업데이트 손실 문제가 있을 수 있습니다.

2. 시나리오 읽기 및 쓰기를 위한 MVCC 

MVCC(다중 버전 동시성 제어)는 읽기-쓰기 충돌을 해결하는 데 사용되는 잠금 없는 동시성 제어 입니다.

트랜잭션에는 한 방향으로 증가하는 트랜잭션 ID가 할당되고 수정될 때마다 버전이 저장됩니다. 버전은 트랜잭션 ID와 연결됩니다. 읽기 작업은 트랜잭션이 시작되기 전에 데이터베이스의 스냅샷만 읽습니다. 따라서 MVCC는 데이터베이스에 대한 다음 문제를 해결할 수 있습니다.

데이터베이스를 동시에 읽고 쓰는 경우 읽기 작업 중에 쓰기 작업을 차단하지 않고 수행할 수 있으며, 쓰기 작업에서 읽기 작업을 차단할 필요가 없으므로 동시 읽기 및 쓰기 성능이 향상됨과 동시에 더티 읽기(dirty read), 팬텀 읽기(phantom read), 반복 불가능 읽기 등의 트랜잭션 격리 문제를 해결하지만 업데이트 손실 문제는 해결하지 못합니다.

1. 3(4)개의 기록 숨김 컬럼 필드

테이블을 생성할 때 MySQL은 사용자가 요구하는 열 외에 3개의 레코드 숨겨진 열 필드를 생성합니다.

DB_TRX_ID: 6바이트, 각 행의 마지막 수정에 대한 트랜잭션 ID(수정/삽입)를 기록하는 컬럼입니다.

DB_ROLL_PTR: 7바이트, 롤백 포인터, 이 레코드의 이전 버전을 가리킴(간단히 기록 버전을 가리키는 것으로 이해되며 이러한 데이터는 일반적으로 실행 취소 로그에 있음)

DB_ROW_ID: 6바이트, 암시적 자동 증가 ID(숨겨진 기본 키), 데이터 테이블에 기본 키가 없으면 InnoDB는 자동으로 DB_ROW_ID를 사용하여 클러스터형 인덱스를 생성합니다.

네 번째 숨겨진 열 필드: 실제로 데이터 행이 삭제되었는지 여부를 식별하는 숨겨진 필드 플래그가 있습니다.

예를 들어, 데이터 조각을 생성하고 삽입할 때 실제 테이블 구조는 다음과 같아야 합니다.

이름

나이

DB_TRX_ID

DB_ROLL_PTR

DB_ROW_ID

장산

20

거래 ID 생성

없는

1(암시적 기본 키)

2. 실행 취소 로그(실행 취소 로그)

MySQL은 메모리에서 데몬 프로세스로 실행됩니다. 실행 취소 로그는 로그 데이터를 저장하기 위한 MySQL의 메모리 버퍼입니다.

3. MVCC 시나리오 시뮬레이션

3.1업데이트 시나리오

기존 거래 ID는 10입니다. 위의 정보 테이블을 업데이트하고 이름을 Zhang San에서 Li Si로 변경합니다.

1. 수정을 원하기 때문에 먼저 레코드에 행 잠금을 추가해야 합니다.

2. 수정 전, 리디렉션된 데이터를 Undo 로그에 복사합니다. (쓰기 중 복사, 원본 데이터는 테이블에, 복사된 데이터는 Undo 로그에, 복사된 데이터 주소는 0XAA라고 가정)

3. 원본 데이터를 수정하는 동안 숨겨진 필드 DB_TRX_ID를 10으로 수정하고 DB_ROLL_PTR 롤백 포인터를 0XAAAAAAAAAA로 수정합니다.

4. 트랜잭션 10commit이 제출되고 행 잠금이 해제됩니다.

이름

나이

DB_TRX_ID

DB_ROLL_PTR

DB_ROW_ID

존 도우

20

10

0XAAAAAAAA

1(암시적 기본 키)

이때 정보 테이블의 레코드를 업데이트하고 Li Si 행의 연령을 30으로 변경해야 하는 또 다른 트랜잭션 11이 있습니다.

1. 수정을 원하기 때문에 먼저 레코드에 행 잠금을 추가해야 합니다.

2. 마찬가지로 주소가 0XBBBBBBBB라고 가정하고 현재 테이블의 해당 행을 실행 취소 로그에 복사합니다.

3. 원본 데이터를 수정하는 동안 숨겨진 필드 DB_TRX_ID를 10으로 수정하고 DB_ROLL_PTR 롤백 포인터를 0XAAAAAAAAAA로 수정합니다.

이름

나이

DB_TRX_ID

DB_ROLL_PTR

DB_ROW_ID

존 도우

30

11

0XBBBBBBBB

1(암시적 기본 키)

실행 취소 로그의 각 버전을 스냅샷이라고 합니다. 버전 체인 외에도 역방향 SQL을 기록하여 데이터 롤백(예: 데이터 삭제 및 로그에 삽입 데이터를 저장할 수 있음)에 대비할 수도 있습니다.

3.2삭제 시나리오

데이터를 삭제한다는 것은 데이터를 지우는 것을 의미하지 않으며, 숨겨진 플래그 플래그를 삭제하도록 설정하면 됩니다. 버전도 형성될 수 있습니다.

3.3삽입

Insert는 삽입이므로 삽입 시 실행 취소 로그에 해당 삭제 문만 기록하면 되며, 롤백할 때는 이러한 삭제 문만 실행하면 됩니다. 현재 트랜잭션이 커밋되면 실행 취소 로그가 백업 데이터를 삭제합니다. (업데이트 및 삭제에 여전히 접근하고 있는 다른 트랜잭션이 있을 수 있으며, 커밋 후 실행 취소 로그 롤백 데이터는 즉시 삭제되지 않습니다.)

3.4장면 선택

MySQL의 RR 수준에서는 한 트랜잭션의 쓰기 작업이 다른 트랜잭션의 읽기 작업에 영향을 주지 않습니다. 추가, 삭제 및 수정은 모두 최신 데이터에 대한 수정이지만, 읽으려면 기록 버전을 읽어야 할 수도 있습니다.

현재 읽기: 현재 읽은 최신 레코드를 읽습니다. 추가, 삭제 및 수정을 모두 현재 읽기라고 하며 선택은 공유 모드의 선택 잠금(공유 잠금), 업데이트용 선택과 같이 현재 읽기일 수도 있습니다.

스냅샷 읽기: 기록 버전을 읽습니다. 스냅샷 읽기는 잠겨 있지 않습니다.

여러 트랜잭션을 동시에 추가, 삭제, 수정하는 경우 현재 읽기이므로 잠금이 필요하며, 선택 항목도 잠긴 경우 격리 수준은 직렬화입니다. 선택 항목이 스냅샷 읽기인 경우 추가, 삭제 및 수정에 대한 현재 읽기에 영향을 미치지 않으므로 잠금이 필요하지 않으며 병렬 실행이 매우 효율적입니다. 트랜잭션의 격리 수준에 따라 선택한 읽기 기록 데이터가 현재 읽기인지 스냅샷 읽기인지가 결정됩니다. (읽기보기 업데이트 여부)

그렇다면 서로 다른 트랜잭션이 서로 다른 내용을 볼 수 있도록 하려면 어떻게 해야 할까요?먼저 발생한 트랜잭션이 후속 트랜잭션의 수정 사항을 확인해야 할까요? Read View는 가시성 판단을 수행합니다.

4、읽기 보기

Read View는 트랜잭션이 처음으로 스냅샷 읽기를 수행할 때 MySQL에 의해 생성되며, 시스템에서 현재 활성화된 트랜잭션의 ID를 기록하고 유지한다. Read View는 MySQL 소스 코드의 클래스로, 볼 수 있는 스냅샷과 볼 수 없는 스냅샷을 결정하기 위해 MVCC와 함께 기본적으로 사용됩니다.

트랜잭션이 스냅샷 읽기 선택을 수행하면 MySQL은 이에 대한 새 객체를 생성하고 내부 조건을 사용하여 현재 트랜잭션이 볼 수 있는 데이터 버전을 결정합니다. 표시되는 데이터는 최신 데이터 또는 최신 데이터일 수 있습니다. 이 행은 기록합니다. 격리 수준에 따라 결정되는 실행 취소 로그의 특정 버전의 데이터입니다.

다음은 ReadView의 단순화된 구조입니다.

class ReadView {
    // 省略...
    private:
    /** 高水位,大于等于这个ID的事务均不可见*/
    trx_id_t m_low_limit_id
    /** 低水位:小于这个ID的事务均可见 */
    trx_id_t m_up_limit_id;
    /** 创建该 Read View 的事务ID*/
    trx_id_t m_creator_trx_id;
    /** 创建视图时的活跃事务id列表*/
    ids_t m_ids;//ids_t集合类型 
    /** 配合purge,标识该视图不需要小于m_low_limit_no的UNDO LOG,
    * 如果其他视图也不需要,则可以删除小于m_low_limit_no的UNDO LOG*/
    trx_id_t m_low_limit_no;
    /** 标记视图是否被关闭*/
    bool m_closed;
    // 省略...
};
m_ids; //一张列表,用来维护Read View生成时刻,系统正活跃的事务ID
up_limit_id; //记录m_ids列表中事务ID最小的ID
low_limit_id; //ReadView生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的最大值+1
creator_trx_id //创建该ReadView的事务ID

그렇다면 트랜잭션에서 읽을 수 있는 데이터는 무엇이고, 트랜잭션에서 볼 수 없는 데이터는 무엇일까요? 아래를 참조하세요:

정리하자면, 예를 들어 저는 후배인데, 나보다 먼저 입학한 선배의 구직 데이터는 볼 수 있지만, 나중에 입학한 선배의 구직 데이터는 선배들이 볼 수 없습니다. 마찬가지로 스냅샷을 찍으면 다음을 볼 수 있습니다.

제출된 거래:

1. creator_trx_id (스냅샷을 생성한 트랜잭션 ID) == DB_TRX_ID (실행 취소 로그의 행을 마지막으로 수정한 트랜잭션 ID)

2. DB_TRX_ID (Undo 로그에서 마지막으로 수정된 행의 트랜잭션 ID) <up_limit_id (스냅샷을 구성하는 m_ids 목록에서 가장 작은 트랜잭션 ID를 가진 ID)

스냅샷이 생성되었을 때 m_ids(활성 트랜잭션 ID)의 트랜잭션:

1. 스냅샷의 트랜잭션 ID는 반드시 연속적일 필요는 없으며, 스냅샷의 트랜잭션 ID 범위는 up_limit_id<=ID<low_limit_id입니다. DB_TRX_ID(Undo 로그의 행에 대한 가장 최근 수정의 트랜잭션 ID)가 이 범위에 있지만 스냅샷 테이블의 m_ids 목록에 해당 ID가 없으면 해당 트랜잭션이 커밋되어 수행될 수 있음을 의미합니다. m_ids 목록에 이 ID가 있는 경우 현재 스냅샷에서 이 ID를 가진 트랜잭션이 여전히 활성 상태이므로 볼 수 없음을 나타냅니다.

볼 수 없습니다:

스냅샷이 생성된 후 새로운 사항:

1. DB_TRX_ID(Undo 로그에서 마지막으로 수정된 행의 트랜잭션 ID) >= low_limit_id(스냅샷 생성 시 시스템에서 할당되지 않은 다음 ID)

현재 버전을 볼 수 없는 것으로 확인되면 다음 단계는 조건이 충족될 때까지 다음 버전을 순회하는 것입니다.

이름

나이

DB_TRX_ID

DB_ROLL_PTR

DB_ROW_ID

장산

28

거래 ID 생성

없는

1(암시적 기본 키)

현재 실행 취소 로그의 버전 체인은 다음과 같습니다.

트랜잭션 2는 변경된 행에 대해 스냅샷 읽기를 수행할 때 이번에 행의 스냅샷을 읽을 때 읽어야 할 스냅샷의 버전을 결정하기 위해 실행 취소 로그의 스냅샷을 순회합니다.

5. RR과 RC의 차이점

5.1 RR 수준에서 현재 읽기와 스냅샷 읽기의 차이점

준비:

--将全局隔离级别设置为可重复读(需重启)
mysql> set global transaction isolation level REPEATABLE READ;
Query OK, 0 rows affected (0.00 sec)
--创建一张表
mysql> create table if not exists account(
    -> id int primary key,
    -> age int not null,
    -> name varchar(20) not null
    -> )ENGINE=InnoDB DEFAULT CHARSET=UTF8;
Query OK, 0 rows affected (0.26 sec)
--插入一条数据
mysql> insert into account values (1,18,'张三');
Query OK, 1 row affected (0.04 sec)
예 1: jly 수정 전 루트 스냅샷 읽기

사용자:jly

--1、启动事务
mysql> begin;
Query OK, 0 rows affected (0.01 sec)
--2、进行快照读
mysql> select* from account;
+----+-----+--------+
| id | age | name   |
+----+-----+--------+
|  1 |  18 | 张三   |
+----+-----+--------+
1 row in set (0.00 sec)
--3、更新数据,修改id为1的字段的年龄为20
mysql> update account set age=20 where id=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
--4、对事务进行提交
mysql> commit;
Query OK, 0 rows affected (0.04 sec)

사용자: 루트

--当上方用户执行完第一步时,root同时启动事务
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
--当上方用户执行完第二步时,root同时进行快照读,读取的结果一样
mysql> select* from account;
+----+-----+--------+
| id | age | name   |
+----+-----+--------+
|  1 |  18 | 张三   |
+----+-----+--------+
1 row in set (0.01 sec)
--当上方用户执行完第三步时,root进行快照读,发现年龄的修改并没有被读到
mysql> select* from account;
+----+-----+--------+
| id | age | name   |
+----+-----+--------+
|  1 |  18 | 张三   |
+----+-----+--------+
1 row in set (0.00 sec)
--当上方用户执行完第四步提交事务时,root再次进行快照读,发现年龄的修改还是没有被读到
mysql> select* from account;
+----+-----+--------+
| id | age | name   |
+----+-----+--------+
|  1 |  18 | 张三   |
+----+-----+--------+
1 row in set (0.00 sec)
--但是此时root使用当前读,使能够读到年龄的修改的
mysql> select* from account lock in share mode;
+----+-----+--------+
| id | age | name   |
+----+-----+--------+
|  1 |  20 | 张三   |
+----+-----+--------+
1 row in set (0.01 sec)
예 2: jly 수정 후 루트의 스냅샷 읽기

사용자:jly

--1、启动事务
mysql> begin;
Query OK, 0 rows affected (0.01 sec)
--2、进行快照读
mysql> select* from account;
+----+-----+--------+
| id | age | name   |
+----+-----+--------+
|  1 |  20 | 张三   |
+----+-----+--------+
1 row in set (0.00 sec)
--3、更新数据,修改id为1的字段的年龄为30
mysql> update account set age=30 where id=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
--4、提交事务
mysql> commit;
Query OK, 0 rows affected (0.03 sec)

사용자: 루트

--1、同时启动事务
mysql> begin;
Query OK, 0 rows affected (0.01 sec)
--当上方用户执行完第四步提交事务时,root进行快照读,发现读到的数据是被修改过的
mysql> select* from account;
+----+-----+--------+
| id | age | name   |
+----+-----+--------+
|  1 |  30 | 张三   |
+----+-----+--------+
1 row in set (0.00 sec)

예제 1을 통해 다음을 확인할 수 있습니다. jly가 제출되기 전에 루트 선택은 수정 전에 데이터를 읽습니다.

예제 2를 통해 jly가 루트 선택을 제출한 후 수정된 데이터를 읽는다는 것을 알 수 있습니다.

트랜잭션을 읽으면 MySQL이 읽기 뷰 객체를 생성하기 때문입니다.위의 읽기 뷰 소개 장에서 언급했듯이 읽기 뷰는 본질적으로 어떤 스냅샷을 볼 수 있고 어떤 스냅샷을 볼 수 없는지 결정하는 데 사용되는 클래스입니다. .

읽기 보기 생성 시점이 다르므로 트랜잭션 가시성에 영향을 미칩니다.

5.2MySQL의 4가지 격리 수준에 대한 다양한 처리 방법 

읽기 보기 생성 타이밍의 차이로 인해 RC 및 RR의 서로 다른 격리 수준에서 스냅샷 읽기 결과가 달라집니다.

반복 읽기: RR 수준의 트랜잭션에 의한 레코드의 첫 번째 스냅샷 읽기는 현재 시스템의 다른 활성 트랜잭션을 기록하기 위해 스냅샷 및 읽기 보기 개체를 생성합니다. 나중에 스냅샷 읽기가 다시 호출되면 동일한 읽기 보기가 여전히 유지됩니다. 다른 트랜잭션이 업데이트를 커밋하기 전에 현재 트랜잭션이 스냅샷 읽기를 사용하는 한 후속 스냅샷 읽기는 동일한 읽기 보기를 사용하므로 후속 수정 사항은 표시되지 않습니다. 즉, RR 수준에서는 스냅샷 읽기가 읽기를 생성할 때 보기, 읽기 보기는 현재 다른 모든 활성 트랜잭션의 스냅샷을 기록하며 이러한 트랜잭션의 수정 사항은 현재 트랜잭션에 표시되지 않습니다. 읽기 보기 이전에 생성된 트랜잭션의 수정 사항만 볼 수 있습니다.

읽기 커밋: RC 수준 트랜잭션에서 각 스냅샷 읽기는 새로운 스냅샷과 읽기 보기를 생성하므로 RC 수준에서 다른 트랜잭션의 업데이트를 볼 수 있습니다. RC가 스냅샷을 읽을 때 읽기 보기가 형성되므로 RC에는 반복 불가능한 읽기 문제가 발생합니다.

커밋되지 않은 읽기: 현재 읽기입니다. 고립은 없습니다.

직렬화: 현재 읽기, 추가, 삭제, 수정이 잠겨 있는 동안 선택도 잠겨 있습니다.

3. 장면 쓰기

현재의 독서로 직접 이해하십시오.

추천

출처blog.csdn.net/gfdxx/article/details/131524580