0부터 시작하여 MySQL 트랜잭션 필기

미리 말해봐 : 0부터 시작, 필기의 학습가치 MySQL

한때 Nien의 멘토였던 7년 경력의 남자는 Mysql에 능숙한 덕분에 월급 40,000을 받았습니다.

0부터 시작하여 MySQL 손글씨의 학습 가치는 다음과 같습니다.

  • MySQL의 내부 메커니즘과 원리를 깊이 이해할 수 있으며 인터뷰의 절대적인 초점과 난이도는 MySQL이라고 할 수 있습니다.
  • MySQL의 사용 및 최적화를 더 잘 파악하기 위해.
  • 프로그래밍 기술과 문제 해결 기술을 향상시키는 데 도움이 됩니다.
  • 고품질 이력서 휠 프로젝트로서 이것은 고품질 이력서 휠 프로젝트라는 점에 유의하십시오.

많은 소규모 파트너의 프로젝트는 매우 낮고 휠 프로젝트가 극도로 부족하므로 여기에 휠 프로젝트가 있습니다.

기사 디렉토리

필기 DB 아키텍처 설계:

Nien의 스타일: 코드 작성을 시작하기 전에 아키텍처를 먼저 수행하십시오.

기능적으로 필기 DB 시스템 아키텍처는 다음 모듈로 나뉩니다.

  • 데이터 관리자 DM
  • 트랜잭션 매니저TM
  • 버전 관리자(VM)
  • 테이블 관리자(TBM)
  • 인덱스 매니저(IM),

다음과 같이 필기 DB 아키텍처 디자인 디자인 다이어그램 :

0부터 시작하여 필기 Mysql 트랜잭션

트랜잭션은 응용 프로그램에서 일관된 작업 시퀀스이며 모두 성공적으로 완료되어야 합니다. 그렇지 않으면 각 작업 중에 수행된 모든 변경 사항이 실행 취소됩니다.

즉, 트랜잭션은 원자적이며 트랜잭션의 일련의 작업이 성공하거나 수행되지 않습니다.

트랜잭션을 종료하는 방법에는 두 가지가 있는데 트랜잭션의 모든 단계가 성공적으로 실행되면 트랜잭션이 커밋됩니다.

이러한 단계 중 하나가 실패하면 롤백 작업이 발생하여 트랜잭션 시작 시 모든 작업이 실행 취소됩니다.

트랜잭션 정의의 설명은 다음과 같습니다.

(1) BEGIN TRANSACTION : 거래가 시작된다.

(2) END TRANSACTION : 거래가 종료된다.

(3) COMMIT : 트랜잭션 커밋. 이 작업은 트랜잭션의 성공적인 종료를 나타내며 트랜잭션의 모든 업데이트 작업을 이제 커밋하거나 영구적으로 유지할 수 있음을 트랜잭션 관리자에게 알립니다.

(4) ROLLBACK : 트랜잭션 롤백. 이 작업은 트랜잭션이 성공적으로 종료되지 않았음을 나타냅니다. 트랜잭션 관리자에게 오류가 발생했고 데이터베이스가 일관성 없는 상태일 수 있으며 트랜잭션의 모든 업데이트 작업을 롤백하거나 실행 취소해야 함을 알립니다.

5가지 상태

트랜잭션은 데이터베이스의 기본 실행 단위이며 트랜잭션이 성공적으로 실행되면 데이터베이스는 하나의 일관된 상태에서 또 다른 일관된 상태로 들어갑니다.

트랜잭션 상태의 상태는 다음과 같은 5가지 유형이 있습니다.

  • 활성 상태 : 트랜잭션이 실행될 때 이 상태인 트랜잭션의 초기 상태.
  • 부분 커밋 상태 : 연산 시퀀스의 마지막 문이 실행될 때 트랜잭션은 부분 커밋 상태가 됩니다. 이때 트랜잭션이 완전히 실행되더라도 실제 출력은 메모리에 일시적으로 상주할 수 있으므로 트랜잭션이 성공적으로 완료되기 전에 하드웨어 장애가 발생할 수 있으므로 부분 커밋 상태가 트랜잭션이 성공적으로 실행되었음을 의미하지는 않습니다. .
  • 실패 상태 : 하드웨어 또는 논리 오류로 인해 트랜잭션이 정상적으로 계속 실행될 수 없으며 트랜잭션이 실패 상태에 들어가고 실패 상태의 트랜잭션을 롤백해야 합니다. 이러한 방식으로 트랜잭션은 일시 중지 상태에 들어갑니다.
  • 중단 상태 : 트랜잭션이 롤백되고 데이터베이스가 트랜잭션이 시작되기 전의 상태로 복원됩니다.
  • 커밋 상태 : 트랜잭션이 성공적으로 완료되면 트랜잭션이 커밋 상태에 있다고 합니다. 트랜잭션이 커밋된 상태가 된 후에만 트랜잭션이 커밋되었다고 말할 수 있습니다.

어떤 이유로 트랜잭션이 성공적으로 실행되지는 않았지만 이미 데이터베이스를 수정한 경우 현재 데이터베이스가 일관성 없는 상태가 될 수 있으므로 트랜잭션으로 인한 변경 사항을 실행 취소(롤백)해야 합니다. .

트랜잭션의 5가지 상태 간 전환

  • BEGIN TRANSACTION : 트랜잭션 실행 시작, 트랜잭션 활성화
  • END TRANSACTION : 트랜잭션의 모든 읽기 및 쓰기 작업이 완료되고 트랜잭션이 부분 커밋 상태에 들어가며 트랜잭션의 모든 작업이 데이터베이스에 미치는 영향이 데이터베이스에 저장됨을 나타냅니다.
  • COMMIT : 트랜잭션이 성공적으로 완료되었음을 표시하고, 트랜잭션의 모든 작업이 데이터베이스에 미치는 영향이 데이터베이스에 안전하게 저장되었으며, 트랜잭션이 커밋 상태로 들어가고 트랜잭션의 작업을 종료합니다.
  • ABORT : 트랜잭션을 실패 상태로 전환하도록 표시하고 시스템은 트랜잭션의 모든 작업이 데이터베이스 및 기타 트랜잭션에 미치는 영향을 취소하고 트랜잭션 작업을 종료합니다.

트랜잭션 상태를 관리하는 방법은 무엇입니까?

필기 데이터베이스 MYDB에서 각 트랜잭션에는 트랜잭션을 고유하게 식별하는 XID가 있습니다.

트랜잭션의 XID는 1부터 시작하여 자체적으로 증가하며 반복될 수 없습니다.

트랜잭션 관리자 TM은 XID 파일을 유지함으로써 트랜잭션의 상태를 유지하고, 특정 트랜잭션의 상태를 조회할 수 있는 다른 모듈에 대한 인터페이스를 제공합니다.

XID 0은 슈퍼 트랜잭션(Super Transaction)이라고 규정되어 있습니다.

트랜잭션을 신청하지 않고 일부 작업을 수행하려는 경우 해당 작업의 XID를 0으로 설정할 수 있습니다. XID가 0인 트랜잭션의 상태는 항상 커밋됩니다.

각 트랜잭션에는 세 가지 상태가 있습니다.

  • 활성 , 진행 중, 아직 끝나지 않은 활성 상태
  • 커밋됨 , 제출됨 상태
  • 중단됨 , 롤백 상태

다음과 같이 정의됩니다.

 // 事务的三种状态
//活动状态
private static final byte FIELD_TRAN_ACTIVE   = 0;
//已提交状态
private static final byte FIELD_TRAN_COMMITTED = 1;
//回滚状态
private static final byte FIELD_TRAN_ABORTED  = 2;

XID 파일은 상태를 저장하기 위해 각 트랜잭션에 1바이트의 공간을 할당합니다.

동시에 8바이트의 숫자가 XID 파일의 헤드에 저장되어 XID 파일이 관리하는 트랜잭션의 수를 기록합니다.

따라서 파일에서 트랜잭션 xid의 상태는 (xid-1)+8바이트에 저장되며, xid-1은 xid 0(Super XID)의 상태를 기록할 필요가 없기 때문이다.

일부 인터페이스는 다른 모듈이 트랜잭션을 생성하고 트랜잭션 상태를 쿼리하기 위해 호출할 수 있도록 TransactionManager에 제공됩니다.인터페이스 메서드는 다음과 같습니다.

//开启事务
long begin();
//提交事务
void commit(long xid);
//撤销事务
void abort(long xid);
// 判断事务状态-活动状态
boolean isActive(long xid);
// 是否提交状态
boolean isCommitted(long xid);
// 是否失败状态
boolean isAborted(long xid);
// 关闭事务管理TM
void close();

xid 파일 생성

xid 파일을 생성하고 TM 개체를 생성해야 합니다. 구체적인 구현은 다음과 같습니다.

public static TransactionManagerImpl create(String path) {
    
    
	File f = new File(path+TransactionManagerImpl.XID_SUFFIX);
    try {
    
    
        if(!f.createNewFile()) {
    
    
            Panic.panic(Error.FileExistsException);
        }
    } catch (Exception e) {
    
    
        Panic.panic(e);
    }
    if(!f.canRead() || !f.canWrite()) {
    
    
        Panic.panic(Error.FileCannotRWException);
    }

    FileChannel fc = null;
    RandomAccessFile raf = null;
    try {
    
    
        raf = new RandomAccessFile(f, "rw");
        fc = raf.getChannel();
    } catch (FileNotFoundException e) {
    
    
       Panic.panic(e);
    }

    // 写空XID文件头
    ByteBuffer buf = ByteBuffer.wrap(new byte[TransactionManagerImpl.LEN_XID_HEADER_LENGTH]);
    try {
    
    
        //从零创建 XID 文件时需要写一个空的 XID 文件头,即设置 xidCounter 为 0,
        // 否则后续在校验时会不合法:
        fc.position(0);
        fc.write(buf);
    } catch (IOException e) {
    
    
        Panic.panic(e);
    }

    return new TransactionManagerImpl(raf, fc);
}

//从一个已有的 xid 文件来创建 TM
public static TransactionManagerImpl open(String path) {
    
    
    File f = new File(path+TransactionManagerImpl.XID_SUFFIX);
    if(!f.exists()) {
    
    
        Panic.panic(Error.FileNotExistsException);
    }
    if(!f.canRead() || !f.canWrite()) {
    
    
        Panic.panic(Error.FileCannotRWException);
    }

    FileChannel fc = null;
    RandomAccessFile raf = null;
    try {
    
    
        //用来访问那些保存数据记录的文件
        raf = new RandomAccessFile(f, "rw");
        //返回与这个文件有关的唯一FileChannel对象
        fc = raf.getChannel();
    } catch (FileNotFoundException e) {
    
    
       Panic.panic(e);
    }

    return new TransactionManagerImpl(raf, fc);
}

상수 정의

TransactionManager 인터페이스의 구현 클래스 TransactionManagerImpl을 살펴보겠습니다.먼저 필요한 상수를 정의합니다.

// XID文件头长度
static final int LEN_XID_HEADER_LENGTH = 8;
// 每个事务的占用长度
private static final int XID_FIELD_SIZE = 1;

// 事务的三种状态
//活动状态
private static final byte FIELD_TRAN_ACTIVE   = 0;
//已提交状态
private static final byte FIELD_TRAN_COMMITTED = 1;
//回滚状态
private static final byte FIELD_TRAN_ABORTED  = 2;

// 超级事务,永远为commited状态
public static final long SUPER_XID =1;

// XID 文件后缀
static final String XID_SUFFIX = ".xid";

private RandomAccessFile file;
private FileChannel fc;
private long xidCounter;
//显示锁
private Lock counterLock;

FileChannel은 NIO 모드에서 파일을 읽고 쓰는 데 사용됩니다. FileChannel은 채널을 통해 파일에 액세스하는 방법을 제공합니다. 매개 변수와 함께 position(int) 메서드를 사용하여 파일의 모든 위치에서 작동을 시작할 수 있으며 파일을 다이렉트 메모리에 저장하여 대용량 파일의 액세스 효율성을 높입니다. Java NIO에 대해서는 <<java High Concurrency Core Programming (Volume 1)>>을 참조하십시오.

XID 파일이 합법적인지 확인

생성자가 TransactionManager를 만든 후 먼저 XID 파일을 확인하여 합법적인 XID 파일인지 확인해야 합니다.

확인 방법도 매우 간단하여 파일 헤더의 8바이트 숫자에서 파일의 이론적 길이를 유추하여 실제 파일 길이와 비교합니다. 다르면 XID 파일이 유효하지 않은 것으로 간주됩니다.

TransactionManagerImpl(RandomAccessFile raf, FileChannel fc) {
    
    
    this.file = raf;
    this.fc = fc;
    //显式锁
    counterLock = new ReentrantLock();
    checkXIDCounter();
}

/**
 * 检查XID文件是否合法
 * 读取XID_FILE_HEADER中的xidcounter,根据它计算文件的理论长度,对比实际长度
 */
private void checkXIDCounter() {
    
    
    long fileLen = 0;
    try {
    
    
        fileLen = file.length();
    } catch (IOException e1) {
    
    
        Panic.panic(Error.BadXIDFileException);
    }
    if(fileLen < LEN_XID_HEADER_LENGTH) {
    
    
        //对于校验没有通过的,会直接通过 panic 方法,强制停机。
        // 在一些基础模块中出现错误都会如此处理,
        // 无法恢复的错误只能直接停机。
        Panic.panic(Error.BadXIDFileException);
    }

    // java NIO中的Buffer的array()方法在能够读和写之前,必须有一个缓冲区,
    // 用静态方法 allocate() 来分配缓冲区
    ByteBuffer buf = ByteBuffer.allocate(LEN_XID_HEADER_LENGTH);
    try {
    
    
        fc.position(0);
        fc.read(buf);
    } catch (IOException e) {
    
    
        Panic.panic(e);
    }
    //从文件开头8个字节得到事务的个数
    this.xidCounter = Parser.parseLong(buf.array());
    // 根据事务xid取得其在xid文件中对应的位置
    long end = getXidPosition(this.xidCounter + 1);
    if(end != fileLen) {
    
    
        //对于校验没有通过的,会直接通过 panic 方法,强制停机
        Panic.panic(Error.BadXIDFileException);
    }
}

잠금은 재진입 잠금 ReentrantLock을 사용합니다.ReentrantLock은 JUC 패키지에서 제공하는 명시적 잠금의 기본 구현 클래스입니다.ReentrantLock 클래스는 Lock 인터페이스를 구현합니다.동기화와 동일한 동시성 및 메모리 의미를 갖지만 시간 제한 선점을 가집니다. , 중단 가능한 선점과 같은 일부 고급 잠금 기능. ReenttrantLock의 내용은 <<Java High Concurrency Core Programming (Volume 2)>>을 참조하십시오.

파일의 이론적 길이: 처음 8바이트 + 트랜잭션 상태가 차지하는 바이트 * 트랜잭션 수

xid 파일의 시작 부분에 있는 8바이트(트랜잭션 수 기록)를 사용하여 파일의 이론적 길이를 추론하고 파일의 실제 길이와 비교합니다. 그렇지 않은 경우 XID 파일은 유효하지 않은 것으로 간주됩니다. xid 개체가 생성될 때마다 확인됩니다.

확인을 통과하지 못한 경우에는 패닉 방법을 사용하여 직접 강제 종료합니다. 일부 기본 모듈의 오류는 이러한 방식으로 처리되며 복구할 수 없는 오류는 직접 중지할 수만 있습니다.

파일에서 xid 상태의 오프셋을 얻기 위해 getXidPosition() 메서드는 다음과 같이 구현됩니다.

// 根据事务xid取得其在xid文件中对应的位置
private long getXidPosition(long xid) {
    
    
    return LEN_XID_HEADER_LENGTH + (xid-1)*XID_FIELD_SIZE;
}

열린 거래

begin()트랜잭션을 열고 트랜잭션 구조를 초기화한 다음 검사 및 스냅샷 사용을 위해 activeTransaction에 저장합니다.

 /**
 *begin() 每开启一个事务,并计算当前活跃的事务的结构,将其存放在 activeTransaction 中,
 * 用于检查和快照使用:
 * @param level
 * @return
 */
@Override
public long begin(int level) {
    
    
    lock.lock();
    try {
    
    
        long xid = tm.begin();
        //activeTransaction 当前事务创建时活跃的事务,,如果level!=0,放入t的快照中
        Transaction t = Transaction.newTransaction(xid, level, activeTransaction);
        activeTransaction.put(xid, t);
        return xid;
    } finally {
    
    
        lock.unlock();
    }
}

트랜잭션 상태 변경

트랜잭션 xid의 오프셋 = 처음 8바이트 + 트랜잭션 상태가 차지하는 바이트 * 트랜잭션의 xid;

트랜잭션 xid를 통해 트랜잭션의 상태를 기록할 오프셋을 계산한 다음 상태를 변경합니다.

구체적인 구현은 다음과 같습니다.

// 更新xid事务的状态为status
private void updateXID(long xid, byte status) {
    
    
    long offset = getXidPosition(xid);
    //每个事务占用长度
    byte[] tmp = new byte[XID_FIELD_SIZE];
    tmp[0] = status;
    ByteBuffer buf = ByteBuffer.wrap(tmp);
    try {
    
    
        fc.position(offset);
        fc.write(buf);
    } catch (IOException e) {
    
    
        Panic.panic(e);
    }
    try {
    
    
        //将数据刷出到磁盘,但不包括元数据
        fc.force(false);
    } catch (IOException e) {
    
    
        Panic.panic(e);
    }
}

그 중 abort()와 commit()이 이 메서드를 호출하는데,

// 提交XID事务
public void commit(long xid) {
    
    
    updateXID(xid, FIELD_TRAN_COMMITTED);
}

// 回滚XID事务
public void abort(long xid) {
    
    
    updateXID(xid, FIELD_TRAN_ABORTED);
}

xid 헤더 업데이트

트랜잭션이 생성될 때마다 파일 헤더에 기록되는 트랜잭션 수는 +1이어야 합니다.

// 将XID加一,并更新XID Header
private void incrXIDCounter() {
    
    
    xidCounter ++;
    ByteBuffer buf = ByteBuffer.wrap(Parser.long2Byte(xidCounter));
    //游标pos, 限制为lim, 容量为cap
    try {
    
    
        fc.position(0);
        fc.write(buf);
    } catch (IOException e) {
    
    
        Panic.panic(e);
    }
    try {
    
    
        fc.force(false);
    } catch (IOException e) {
    
    
        Panic.panic(e);
    }
}

거래 상태 판단

xid에 따르면-"레코드 트랜잭션 xid의 상태 오프셋 얻기-"트랜잭션 xid의 상태 읽기-"상태와 같은지 여부.

// 检测XID事务是否处于status状态
private boolean checkXID(long xid, byte status) {
    
    
    long offset = getXidPosition(xid);
    ByteBuffer buf = ByteBuffer.wrap(new byte[XID_FIELD_SIZE]);
    try {
    
    
        fc.position(offset);
        fc.read(buf);
    } catch (IOException e) {
    
    
        Panic.panic(e);
    }
    return buf.array()[0] == status;
}

// 活动状态判断
public boolean isActive(long xid) {
    
    
    if(xid == SUPER_XID) return false;
    return checkXID(xid, FIELD_TRAN_ACTIVE);
}

// 已提交状态判断
public boolean isCommitted(long xid) {
    
    
    if(xid == SUPER_XID) return true;
    return checkXID(xid, FIELD_TRAN_COMMITTED);
}

//回滚状态判断
public boolean isAborted(long xid) {
    
    
    if(xid == SUPER_XID) return false;
    return checkXID(xid, FIELD_TRAN_ABORTED);
}

닫기 TM

 //TM关闭
public void close() {
    
    
    try {
    
    
        fc.close();
        file.close();
    } catch (IOException e) {
    
    
        Panic.panic(e);
    }
}

2단계 잠금은 트랜잭션 작업을 구현합니다.

2상 잠금(2PL) 기능 소개

트랜잭션 스케줄링에는 일반적으로 직렬 스케줄링과 병렬 스케줄링이 포함되며 먼저 다음 개념을 이해합시다.

  • 동시성 제어 : 여러 사용자가 공유하는 시스템에서 여러 사용자가 동시에 동일한 데이터에 대해 작업할 수 있습니다.
  • Scheduling : 트랜잭션의 실행 순서
  • 직렬 스케줄링 : 여러 트랜잭션이 순차적으로 순차적으로 실행되고 다른 트랜잭션의 모든 작업은 한 트랜잭션의 모든 작업이 실행된 후에야 실행됩니다. 직렬 스케줄링인 한 실행 결과는 정확합니다.

  • 병렬 스케줄링: 시분할 방식을 사용하여 동시에 여러 트랜잭션을 처리합니다. 그러나 병렬 스케줄링의 스케줄링 결과는 잘못될 수 있으며 수정 손실, 반복 불가능한 읽기 및 더티 데이터 읽기를 포함하여 일관되지 않은 상태를 생성할 수 있습니다.

트랜잭션의 병렬 스케줄링

트랜잭션에서는 다음 그림과 같이 잠금(lock) 단계와 잠금 해제(unlock) 단계로 나뉩니다. 즉, 모든 잠금 작업은 잠금 해제 작업 이전입니다.

트랜잭션 잠금 및 잠금 해제의 두 단계

실제 상황에서 SQL은 시시각각 변하고 항목 수는 불확실하므로 데이터베이스에서 트랜잭션의 잠금 단계와 잠금 해제 단계를 결정하기 어렵습니다. 그래서 S2PL(Strict-2PL)이 도입되었는데, 즉 트랜잭션에서 커밋하거나 롤백할 때만 잠금 해제 단계이고 나머지 시간은 잠금 단계입니다. 2PL의 도입은 트랜잭션의 격리를 보장하는 것입니다. 즉, 여러 트랜잭션이 동시성의 경우 직렬 실행과 동일합니다.

2상 잠금 잠금 위상

첫 번째 단계는 잠금을 획득하는 단계로 확장 단계라고 하며 실제로 이 단계에서 잠금 작업에 들어갈 수 있습니다. X 잠금을 신청하고 획득해야 합니다. , 잠금에 실패하고 트랜잭션이 대기 상태에 들어가고 잠금이 성공할 때까지 계속되지 않습니다. 잠긴 후에는 잠금을 해제할 수 없습니다.

두 번째 단계는 잠금을 해제하는 단계로 수축 단계라고 합니다. 트랜잭션이 블록을 해제하면 잠금 해제만 수행할 수 있고 잠금 작업은 수행할 수 없는 블록 단계로 트랜잭션이 진입합니다.

2PL(2단계 잠금) 트랜잭션 구현

mysql의 트랜잭션은 기본적으로 묵시적 트랜잭션으로 삽입, 업데이트, 삭제 작업을 수행할 때 데이터베이스가 자동으로 트랜잭션을 시작, 커밋 또는 롤백합니다.

암시적 트랜잭션을 활성화할지 여부는 변수 autocommit에 의해 제어됩니다.

따라서 트랜잭션은 암시적 트랜잭션명시적 트랜잭션 으로 나뉩니다 .

암시적 트랜잭션은 삽입, 업데이트, 삭제 문과 같은 트랜잭션에 의해 자동으로 열리거나 커밋되거나 롤백되며 트랜잭션의 열기, 제출 또는 롤백은 mysql에 의해 자동으로 제어됩니다.

명시적 트랜잭션은 수동으로 열거나 커밋하거나 롤백해야 하며 개발자가 직접 제어합니다.

수기 2PL(two-phase lock) 트랜잭션

트랜잭션 격리 수준을 구현하기 전에 버전 관리자(VM)에 대해 논의해야 합니다 .

VM은 2단계 잠금 프로토콜을 기반으로 스케줄링 시퀀스의 직렬화를 실현하고 MVCC를 구현하여 읽기 및 쓰기 차단을 제거합니다.

두 가지 격리 수준이 동시에 구현됩니다.VM은 필기 데이터베이스 MYDB의 트랜잭션 및 데이터 버전 관리 핵심입니다.

거래 저장

레코드의 경우 MYDB는 구조를 유지하기 위해 Entry 클래스를 사용합니다.

이론적으로는 MVCC가 여러 버전을 구현하지만 구현상 VM은 Update 연산을 제공하지 않으며 필드에 대한 업데이트 연산은 다음과 같은 테이블 및 필드 관리(TBM)에 의해 구현된다.

따라서 VM 구현에는 하나의 레코드 버전만 있습니다.

레코드는 데이터 항목에 저장되므로 항목에 DataItem 참조를 저장하기만 하면 됩니다.

public class Entry {
    
    

    private static final int OF_XMIN = 0;
    private static final int OF_XMAX = OF_XMIN+8;
    private static final int OF_DATA = OF_XMAX+8;

    private long uid;
    private DataItem dataItem;
    private VersionManager vm;

    public static Entry newEntry(VersionManager vm, DataItem dataItem, long uid) {
    
    
        Entry entry = new Entry();
        entry.uid = uid;
        entry.dataItem = dataItem;
        entry.vm = vm;
        return entry;
    }

    public static Entry loadEntry(VersionManager vm, long uid) throws Exception {
    
    
        DataItem di = ((VersionManagerImpl)vm).dm.read(uid);
        return newEntry(vm, di, uid);
    }

    public void release() {
    
    
        ((VersionManagerImpl)vm).releaseEntry(this);
    }

    public void remove() {
    
    
        dataItem.release();
    }
}

Entry에 저장되는 데이터 형식은 다음과 같이 규정됩니다.

[XMIN]  [XMAX]  [DATA]
8个字节  8个字节

XMIN은 레코드(버전)를 생성한 트랜잭션 번호이고 XMAX는 레코드(버전)를 삭제한 트랜잭션 번호입니다. DATA는 이 레코드가 보유한 데이터입니다.

이 구조에 따르면 레코드 생성 시 호출되는 wrapEntryRaw() 메서드는 다음과 같습니다.

public static byte[] wrapEntryRaw(long xid, byte[] data) {
    
    
    byte[] xmin = Parser.long2Byte(xid);
    byte[] xmax = new byte[8];
    return Bytes.concat(xmin, xmax, data);
}

마찬가지로 레코드에 있는 데이터를 가져오려면 다음 구조에 따라 데이터를 구문 분석해야 합니다.

// 以拷贝的形式返回内容
public byte[] data() {
    
    
    dataItem.rLock();
    try {
    
    
        SubArray sa = dataItem.data();
        byte[] data = new byte[sa.end - sa.start - OF_DATA];
        System.arraycopy(sa.raw, sa.start+OF_DATA, data, 0, data.length);
        return data;
    } finally {
    
    
        dataItem.rUnLock();
    }
}

여기서 데이터는 복사본 형태로 반환되며, 수정이 필요한 경우 수정 전에 before()메서드를 unBefore()하고 수정 완료 후 after()방법 .

전체 프로세스는 주로 이전 단계 데이터를 저장하고 시간에 로그인하는 것입니다. DM은 DataItem의 수정이 원자적임을 보장합니다.

@Override
public void before() {
    
    
    wLock.lock();
    pg.setDirty(true);
    System.arraycopy(raw.raw, raw.start, oldRaw, 0, oldRaw.length);
}

@Override
public void unBefore() {
    
    
    System.arraycopy(oldRaw, 0, raw.raw, raw.start, oldRaw.length);
    wLock.unlock();
}

@Override
public void after(long xid) {
    
    
    dm.logDataItem(xid, this);
    wLock.unlock();
}

XMAX의 값을 설정하는 것은 수정이 필요한 경우 일정한 규칙을 따라야 한다는 것을 반영하며, 이 버전은 XMAX 이후의 각 트랜잭션에 보이지 않는 삭제와 동일합니다.setXmax의 코드는 다음과 같습니다.

public void setXmax(long xid) {
    
    
    dataItem.before();
    try {
    
    
        SubArray sa = dataItem.data();
        System.arraycopy(Parser.long2Byte(xid), 0, sa.raw, sa.start+OF_XMAX, 8);
    } finally {
    
    
        dataItem.after(xid);
    }
}

열린 거래

begin()은 트랜잭션을 시작할 때마다 현재 활성 트랜잭션의 구조를 계산하고 이를 activeTransaction에 저장하고,

@Override
public long begin(int level) {
    
    
    lock.lock();
    try {
    
    
        long xid = tm.begin();
        Transaction t = Transaction.newTransaction(xid, level, activeTransaction);
        activeTransaction.put(xid, t);
        return xid;
    } finally {
    
    
        lock.unlock();
    }
}

트랜잭션 커밋

commit() 메서드는 주로 관련 구조를 해제하고, 보류된 잠금을 해제하고, TM 상태를 수정하고, activeTransaction에서 트랜잭션을 제거하기 위해 트랜잭션을 커밋합니다.

@Override
public void commit(long xid) throws Exception {
    
    
    lock.lock();
    Transaction t = activeTransaction.get(xid);
    lock.unlock();

    try {
    
    
        if(t.err != null) {
    
    
            throw t.err;
        }
    } catch(NullPointerException n) {
    
    
        System.out.println(xid);
        System.out.println(activeTransaction.keySet());
        Panic.panic(n);
    }

    lock.lock();
    activeTransaction.remove(xid);
    lock.unlock();

    lt.remove(xid);
    tm.commit(xid);
}

롤백 트랜잭션

트랜잭션을 중단하는 방법에는 수동과 자동의 두 가지가 있습니다.

수동은 abort() 메서드를 호출하는 것을 의미하고, 자동은 트랜잭션에서 교착 상태가 감지되면 롤백 트랜잭션이 자동으로 실행 취소되거나 버전 점프가 발생하면 자동으로 롤백됨을 의미합니다.

/**
 * 回滚事务
 * @param xid
 */
@Override
public void abort(long xid) {
    
    
    internAbort(xid, false);
}

private void internAbort(long xid, boolean autoAborted) {
    
    
    lock.lock();
    Transaction t = activeTransaction.get(xid);
   //手动回滚
    if(!autoAborted) {
    
    
        activeTransaction.remove(xid);
    }
    lock.unlock();

    //自动回滚
    if(t.autoAborted) return;
    lt.remove(xid);
    tm.abort(xid);
}

트랜잭션 삭제

트랜잭션이 커밋되거나 중단되면 보유하고 있는 모든 잠금을 해제하고 대기 그래프에서 자신을 제거합니다.

그리고 uid를 점유할 대기 큐에서 xid를 선택합니다.

잠금을 해제할 때 다른 비즈니스 스레드가 잠금을 획득하고 계속 실행할 수 있도록 잠금 개체의 잠금을 해제하십시오.

public void remove(long xid) {
    
    
    lock.lock();
    try {
    
    
        List<Long> l = x2u.get(xid);
        if(l != null) {
    
    
            while(l.size() > 0) {
    
    
                Long uid = l.remove(0);
                selectNewXID(uid);
            }
        }
        waitU.remove(xid);
        x2u.remove(xid);
        waitLock.remove(xid);

    } finally {
    
    
        lock.unlock();
    }
}

// 从等待队列中选择一个xid来占用uid
private void selectNewXID(long uid) {
    
    
    u2x.remove(uid);
    List<Long> l = wait.get(uid);
    if(l == null) return;
    assert l.size() > 0;

    while(l.size() > 0) {
    
    
        long xid = l.remove(0);
        if(!waitLock.containsKey(xid)) {
    
    
            continue;
        } else {
    
    
            u2x.put(uid, xid);
            Lock lo = waitLock.remove(xid);
            waitU.remove(xid);
            lo.unlock();
            break;
        }
    }

    if(l.size() == 0) wait.remove(uid);
}

데이터 삽입

insert()는 데이터를 항목으로 래핑하고 삽입을 위해 DM에 전달하는 것입니다.

@Override
public long insert(long xid, byte[] data) throws Exception {
    
    
    lock.lock();
    Transaction t = activeTransaction.get(xid);
    lock.unlock();

    if(t.err != null) {
    
    
        throw t.err;
    }

    byte[] raw = Entry.wrapEntryRaw(xid, data);
    return dm.insert(xid, raw);
}

트랜잭션 읽기

read() 메소드는 항목을 읽고 격리 수준에 따라 가시성을 판단할 수 있습니다.

@Override
public byte[] read(long xid, long uid) throws Exception {
    
    
    lock.lock();
    //当前事务xid读取时的快照数据
    Transaction t = activeTransaction.get(xid);
    lock.unlock();

    if(t.err != null) {
    
    
        throw t.err;
    }

    Entry entry = null;
    try {
    
    
        //通过uid找要读取的事务dataItem
        entry = super.get(uid);
    } catch(Exception e) {
    
    
        if(e == Error.NullEntryException) {
    
    
            return null;
        } else {
    
    
            throw e;
        }
    }
    try {
    
    
        if(Visibility.isVisible(tm, t, entry)) {
    
    
            return entry.data();
        } else {
    
    
            return null;
        }
    } finally {
    
    
        entry.release();
    }
}

트랜잭션의 ACID 속성

트랜잭션은 데이터베이스의 다양한 데이터 항목에 액세스하고 업데이트하는 프로그램 실행 단위입니다. 트랜잭션의 목적은 모두 수정하거나 수정하지 않는 것입니다.

대부분의 시나리오에서 애플리케이션은 단일 데이터베이스만 운영하면 되며, 이 경우의 트랜잭션을 로컬 트랜잭션(Local Transaction)이라고 합니다.

로컬 트랜잭션의 ACID 속성은 데이터베이스에서 직접 지원됩니다.

로컬 트랜잭션을 달성하기 위해 Mysql은 롤백 로그, 리두 로그, MVCC, 읽기-쓰기 잠금 등과 같은 많은 작업을 수행했습니다.

InnoDB 스토리지 엔진의 경우 기본 트랜잭션 격리 수준은 트랜잭션의 ACID 특성을 완전히 따르고 충족하는 반복 읽기입니다.

ACID는 원자성(Atomicity), 일관성(Consistency), 격리(Isolation), 내구성(Durability)이라는 네 단어의 약어입니다.

InnoDB는 로그 및 잠금을 통해 트랜잭션의 ACID 특성을 보장합니다.

  • 데이터베이스 잠금 메커니즘을 통해 트랜잭션의 격리가 보장됩니다.
  • Redo Log(리두 로그)를 통해 트랜잭션의 격리를 보장합니다.
  • 트랜잭션의 원자성 및 일관성은 Undo Log(실행 취소 로그)를 통해 보장됩니다.

원자성

트랜잭션은 원자적 작업 시퀀스 단위여야 합니다. 트랜잭션에 포함된 모든 작업은 모두 성공하거나 한 번에 실행되지 않습니다. 하나라도 실패하면 전체 트랜잭션이 롤백됩니다. 모든 작업이 성공적으로 실행된 경우에만 전체 트랜잭션이 성공으로 간주됩니다.

데이터를 실행하기 전에 먼저 데이터를 Undo Log에 백업한 후 데이터를 수정하십시오. 오류가 발생하거나 사용자가 Rollback 문을 실행하면 시스템은 Undo Log의 백업을 사용하여 데이터의 원자성을 보장하기 위해 트랜잭션이 시작되기 전 상태로 데이터를 복원할 수 있습니다.

일관성

트랜잭션 실행은 데이터베이스 데이터의 무결성과 일관성을 파괴할 수 없으며 데이터베이스는 트랜잭션 실행 전후에 일관된 상태여야 합니다.

일관성에는 두 가지 측면, 즉 제약 조건 일관성과 데이터 일관성이 포함됩니다.

  • Constraint Consistency: 외래키, 체크(mysql에서 지원하지 않음), 고유 인덱스 등 테이블 구조 생성 시 명시되는 제약 조건.
  • 데이터 일관성: 특정 기술에만 의존하는 것이 아니라 원자성, 지속성 및 격리의 결과이기 때문에 포괄적인 규정입니다.

격리

동시 상황에서 동시 트랜잭션은 서로 격리되며 한 트랜잭션의 실행이 다른 트랜잭션에 의해 방해받을 수 없습니다.

즉, 서로 다른 트랜잭션이 동일한 데이터에서 동시에 작동할 때 각 트랜잭션은 자체의 완전한 데이터 공간을 갖습니다. 즉, 트랜잭션 내에서 사용되는 작업 및 데이터는 다른 동시 트랜잭션과 격리되며 동시에 실행되는 트랜잭션은 서로 간섭할 수 없습니다. .

내구성

영속성이라고도 하는 지속성은 트랜잭션이 커밋되면 데이터베이스의 해당 데이터에 대한 상태 변경이 영구적이어야 함을 의미합니다. 시스템이 다운되거나 시스템이 다운되더라도 데이터베이스를 재시작할 수만 있다면 트랜잭션이 성공적으로 종료되었을 때의 상태로 복원할 수 있습니다.

Redo Log는 새로운 데이터의 백업을 기록하는 것으로 트랜잭션이 커밋되기 전에는 Redo Log만 유지하면 되고 데이터를 유지할 필요는 없다. 이미 지속되었습니다. 시스템은 Redo Log의 내용에 따라 모든 데이터를 crash 이전 상태로 복구할 수 있으며, Redo Log를 이용하여 데이터의 지속성을 확보하는 과정입니다.

요약하다

데이터의 무결성은 주로 일관성 특성을 반영하며, 데이터의 무결성은 원자성, 격리성, 지속성에 의해 보장되며 이 세 가지 특성은 redo Log 및 Undo Log에 의해 보장됩니다. ACID 속성 간의 관계는 아래 그림에 나와 있습니다.

이중 자물쇠

mysql에서 2PL(two-phase locking protocol)은 보통 확장과 수축의 두 단계를 포함한다. 확장 단계에서 트랜잭션은 잠금을 획득하지만 잠금을 해제할 수는 없습니다. 수축 단계에서는 기존 잠금을 ​​해제할 수 있지만 새로운 잠금을 획득할 수 없으므로 이 조항은 교착 상태의 위험이 있습니다.

예를 들어, T1'이 확장 단계에 있을 때 Y의 읽기 잠금을 획득하고 Y를 읽습니다. 이때 X의 쓰기 잠금을 획득하려고 하지만 T2'의 읽기 잠금이 X를 잠근 것을 발견하고, T2'도 Y에 대한 쓰기 잠금을 획득하려고 합니다. 요컨대 T1'은 X를 얻지 않으면 Y를 해제하지 않고 T2'는 Y를 얻지 않으면 X를 해제하지 않으므로 루프에 걸리고 교착 상태가 형성됩니다.

2PL은 잠금을 보유한 스레드가 잠금을 해제할 때까지 트랜잭션을 차단합니다. 이 대기 관계는 방향성 에지로 추상화할 수 있는데, 예를 들어 Tj가 Ti를 기다리고 있다면 Tj --> Ti로 표현할 수 있다. 이러한 방식으로 무한한 수의 방향성 모서리가 그래프를 형성할 수 있습니다. 교착 상태를 감지하려면 이 그래프에 주기가 있는지 확인하기만 하면 됩니다.

교착 상태 감지

LockTable 객체를 생성하고 이 그래프를 메모리에 유지 관리합니다. 유지 관리 구조는 다음과 같습니다.

public class LockTable {
    
    
    
    private Map<Long, List<Long>> x2u;  // 某个XID已经获得的资源的UID列表
    private Map<Long, Long> u2x;        // UID被某个XID持有
    private Map<Long, List<Long>> wait; // 正在等待UID的XID列表
    private Map<Long, Lock> waitLock;   // 正在等待资源的XID的锁
    private Map<Long, Long> waitU;      // XID正在等待的UID
    ......
}

대기 상황이 있을 때마다 그래프에 간선을 추가하고 교착 상태 감지를 수행하십시오. 교착 상태가 감지되면 Edge를 철회하고 추가를 허용하지 않으며 트랜잭션을 철회합니다.

// 不需要等待则返回null,否则返回锁对象
// 会造成死锁则抛出异常
public Lock add(long xid, long uid) throws Exception {
    
    
    lock.lock();
    try {
    
    
        //某个xid已经获得的资源的UID列表,如果在这个列表里面,则不造成死锁,也不需要等待
        if(isInList(x2u, xid, uid)) {
    
    
            return null;
        }
        //表示有了一个新的uid,则把uid加入到u2x和x2u里面,不死锁,不等待
        // u2x  uid被某个xid占有
        if(!u2x.containsKey(uid)) {
    
    
            u2x.put(uid, xid);
            putIntoList(x2u, xid, uid);
            return null;
        }
        //以下就是需要等待的情况
        //多个事务等待一个uid的释放
        waitU.put(xid, uid);
        //putIntoList(wait, xid, uid);
        putIntoList(wait, uid, xid);
        //造成死锁
        if(hasDeadLock()) {
    
    
            //从等待列表里面删除
            waitU.remove(xid);
            removeFromList(wait, uid, xid);
            throw Error.DeadlockException;
        }
        //从等待列表里面删除
        Lock l = new ReentrantLock();
        l.lock();
        waitLock.put(xid, l);
        return l;

    } finally {
    
    
        lock.unlock();
    }
}

add를 호출합니다. 기다릴 필요가 없으면 다음 단계로 이동합니다. 기다릴 필요가 있으면 잠긴 Lock 개체를 반환합니다. 호출자가 개체를 획득하면 스레드 차단 목적을 달성하기 위해 개체 잠금을 획득해야 합니다.

 try {
    
    
    l = lt.add(xid, uid);
} catch(Exception e) {
    
    
    t.err = Error.ConcurrentUpdateException;
    internAbort(xid, true);
    t.autoAborted = true;
    throw t.err;
}
if(l != null) {
    
    
    l.lock();
    l.unlock();
}

판단 교착 상태

그래프에 주기가 있는지를 찾는 알고리즘은 사실 딥서치인데, 이 그래프가 반드시 연결된 그래프는 아니라는 점에 유의해야 합니다. 구체적인 아이디어는 1로 초기화되는 각 노드에 대한 액세스 스탬프를 설정한 다음 모든 노드를 순회하고 심층 검색의 루트로 1이 아닌 각 노드를 사용하여 연결된 그래프에서 만나는 모든 노드를 검색하도록 설정합니다. 같은 숫자, 서로 다른 연결된 그래프는 서로 다른 숫자를 가집니다. 이와 같이 특정 그래프를 순회할 때 이전에 순회한 노드를 만난다면 사이클이 나타났음을 의미합니다.

교착 상태 판단의 구체적인 구현은 다음과 같습니다.

private boolean hasDeadLock() {
    
    
    xidStamp = new HashMap<>();
    stamp = 1;
    System.out.println("xid已经持有哪些uid x2u="+x2u);//xid已经持有哪些uid
    System.out.println("uid正在被哪个xid占用 u2x="+u2x);//uid正在被哪个xid占用

    //已经拿到锁的xid
    for(long xid : x2u.keySet()) {
    
    
        Integer s = xidStamp.get(xid);
        if(s != null && s > 0) {
    
    
            continue;
        }
        stamp ++;
        System.out.println("xid"+xid+"的stamp是"+s);
        System.out.println("进入深搜");
        if(dfs(xid)) {
    
    
            return true;
        }
    }
    return false;
}

private boolean dfs(long xid) {
    
    
    Integer stp = xidStamp.get(xid);
    //遍历某个图时,遇到了之前遍历过的节点,说明出现了环
    if(stp != null && stp == stamp) {
    
    
        return true;
    }
    if(stp != null && stp < stamp) {
    
    
        return false;
    }
    //每个已获得资源的事务一个独特的stamp
    xidStamp.put(xid, stamp);
    System.out.println("xidStamp找不到该xid,加入后xidStamp变为"+xidStamp);
    //已获得资源的事务xid正在等待的uid
    Long uid = waitU.get(xid);
    System.out.println("xid"+xid+"正在等待的uid是"+uid);
   if(uid == null){
    
    
       System.out.println("未成环,退出深搜");
       //xid没有需要等待的uid,无死锁
       return false;
   }
    //xid需要等待的uid被哪个xid占用了
    Long x = u2x.get(uid);
    System.out.println("xid"+xid+"需要的uid被"+"xid"+x+"占用了");
    System.out.println("=====再次进入深搜"+"xid"+x+"====");
    assert x != null;
    return dfs(x);
}

트랜잭션이 커밋되거나 중단되면 보유하고 있는 모든 잠금을 해제하고 대기 그래프에서 자신을 제거합니다.

public void remove(long xid) {
    
    
    lock.lock();
    try {
    
    
        List<Long> l = x2u.get(xid);
        if(l != null) {
    
    
            while(l.size() > 0) {
    
    
                Long uid = l.remove(0);
                selectNewXID(uid);
            }
        }
        waitU.remove(xid);
        x2u.remove(xid);
        waitLock.remove(xid);

    } finally {
    
    
        lock.unlock();
    }
}

while 루프는 이 스레드가 보유한 리소스에 대한 모든 잠금을 해제합니다. 이 잠금은 대기 중인 스레드에서 획득할 수 있습니다.

// 从等待队列中选择一个xid来占用uid
private void selectNewXID(long uid) {
    
    
    u2x.remove(uid);
    List<Long> l = wait.get(uid);
    if(l == null) return;
    assert l.size() > 0;

    while(l.size() > 0) {
    
    
        long xid = l.remove(0);
        if(!waitLock.containsKey(xid)) {
    
    
            continue;
        } else {
    
    
            u2x.put(uid, xid);
            Lock lo = waitLock.remove(xid);
            waitU.remove(xid);
            lo.unlock();
            break;
        }
    }

    if(l.size() == 0) wait.remove(uid);
}

목록의 처음부터 잠금 해제를 시도하는 것은 여전히 ​​공정한 잠금입니다. 잠금을 해제할 때 비즈니스 스레드가 잠금을 획득하고 계속 실행할 수 있도록 잠금 개체를 잠금 해제하십시오.

테스트 코드는 다음과 같습니다.

public static void main(String[] args) throws Exception {
    
    
    LockTable lock = new LockTable();
    lock.add(1L,3L);
    lock.add(2L,4L);
    lock.add(3L,5L);
    lock.add(1L,4L);

    System.out.println("+++++++++++++++++++++++");
    lock.add(2L,5L);
    System.out.println("++++++++++++++++");
    lock.add(3L,3L);
    System.out.println(lock.hasDeadLock());
}

실행 결과는 다음과 같습니다.

xid已经持有哪些uid x2u={
    
    1=[3], 2=[4], 3=[5]}
uid正在被哪个xid占用 u2x={
    
    3=1, 4=2, 5=3}
xid1的stamp是null
进入深搜
xidStamp找不到该xid,加入后xidStamp变为{
    
    1=2}
xid1正在等待的uid是4
xid1需要的uid被xid2占用了
=====再次进入深搜xid2====
xidStamp找不到该xid,加入后xidStamp变为{
    
    1=2, 2=2}
xid2正在等待的uid是null
未成环,退出深搜
xid3的stamp是null
进入深搜
xidStamp找不到该xid,加入后xidStamp变为{
    
    1=2, 2=2, 3=3}
xid3正在等待的uid是null
未成环,退出深搜
+++++++++++++++++++++++
xid已经持有哪些uid x2u={
    
    1=[3], 2=[4], 3=[5]}
uid正在被哪个xid占用 u2x={
    
    3=1, 4=2, 5=3}
xid1的stamp是null
进入深搜
xidStamp找不到该xid,加入后xidStamp变为{
    
    1=2}
xid1正在等待的uid是4
xid1需要的uid被xid2占用了
=====再次进入深搜xid2====
xidStamp找不到该xid,加入后xidStamp变为{
    
    1=2, 2=2}
xid2正在等待的uid是5
xid2需要的uid被xid3占用了
=====再次进入深搜xid3====
xidStamp找不到该xid,加入后xidStamp变为{
    
    1=2, 2=2, 3=2}
xid3正在等待的uid是null
未成环,退出深搜
++++++++++++++++
xid已经持有哪些uid x2u={
    
    1=[3], 2=[4], 3=[5]}
uid正在被哪个xid占用 u2x={
    
    3=1, 4=2, 5=3}
xid1的stamp是null
进入深搜
xidStamp找不到该xid,加入后xidStamp变为{
    
    1=2}
xid1正在等待的uid是4
xid1需要的uid被xid2占用了
=====再次进入深搜xid2====
xidStamp找不到该xid,加入后xidStamp变为{
    
    1=2, 2=2}
xid2正在等待的uid是5
xid2需要的uid被xid3占用了
=====再次进入深搜xid3====
xidStamp找不到该xid,加入后xidStamp变为{
    
    1=2, 2=2, 3=2}
xid3正在等待的uid是3
xid3需要的uid被xid1占用了
=====再次进入深搜xid1====

높은 동시성 시나리오에서 트랜잭션 격리를 수행하는 방법

트랜잭션 격리 수준의 기능 분석

잠금 및 MVCC(다중 버전 제어) 기술을 사용하여 격리를 보장합니다. InnoDB에서 지원하는 격리 유형은 낮음에서 높음까지 4가지입니다.

  • 커밋되지 않은 읽기(READ UNCOMMITTED)
  • 커밋 읽기
  • 반복 읽기(REPEATABLE READ)
  • 직렬화 가능(SERIALIZABLE).

커밋되지 않은 읽기(READ UNCOMMITTED)

커밋되지 않은 읽기 격리 수준에서는 더티 읽기가 허용됩니다. 커밋되지 않음은 이러한 데이터가 롤백될 수 있음을 의미하며 반드시 읽히지 않아도 되는 데이터를 읽습니다. 커밋되지 않은 읽기 격리 수준은 더티 읽기 문제가 발생하기 쉽습니다.

더티 읽기는 다른 트랜잭션에서 현재 트랜잭션이 다른 트랜잭션의 커밋되지 않은 데이터를 읽을 수 있음을 의미하며 이는 단순히 더티 데이터 읽기를 의미합니다. 이른바 더티 데이터는 트랜잭션에 의한 버퍼 풀의 행 레코드 수정을 말하며 아직 제출되지 않았습니다.

더티 데이터를 읽는 경우, 즉 한 트랜잭션이 다른 트랜잭션의 커밋되지 않은 데이터를 읽을 수 있으며 이는 분명히 데이터베이스 격리를 위반합니다.

더티 읽기가 발생하기 위한 조건은 트랜잭션의 격리 수준이 커밋되지 않은 읽기여야 한다는 것입니다. 프로덕션 환경에서 대부분의 데이터베이스는 최소한 커밋된 읽기로 설정되므로 프로덕션 환경에서 보낼 확률은 매우 낮습니다.

커밋 읽기

제출된 데이터만 읽을 수 있습니다. 즉 트랜잭션 A가 0에서 10까지 n을 누적하는 과정에서 B는 중국 값 n을 볼 수 없고 10만 볼 수 있습니다.

커밋된 격리 수준에서 더티 읽기는 금지되지만 반복 불가능한 읽기는 실행됩니다.

커밋된 읽기는 격리의 간단한 정의를 충족합니다. 트랜잭션은 커밋된 트랜잭션에 의해 변경된 내용만 볼 수 있습니다.

oracle 및 SQL Server의 기본 격리 수준은 커밋된 읽기입니다.

커밋된 읽기 격리 수준은 반복 불가능한 읽기 문제가 발생하기 쉽습니다. 반복 불가능한 읽기는 트랜잭션 내에서 동일한 데이터 세트의 데이터를 여러 번 읽는 상황을 말합니다.


트랜잭션 A는 동일한 데이터를 여러 번 읽지만 트랜잭션 B는 트랜잭션 A를 여러 번 읽는 동안 데이터를 업데이트하고 비교하므로 트랜잭션 A가 동일한 데이터를 여러 번 읽을 때 일관성 없는 결과가 발생합니다.

더티 읽기와 반복 불가능한 읽기의 차이점은 더티 읽기는 커밋되지 않은 데이터를 읽는 반면 반복 불가능한 읽기는 커밋된 데이터를 읽지만 데이터베이스 트랜잭션 일관성 원칙을 위반한다는 것입니다.

정상적인 상황에서 반복 불가능 읽기의 문제는 제출된 데이터를 읽기 때문에 허용되며 자체적으로 큰 문제를 일으키지 않습니다. Oracle 및 SQL 서버 데이터베이스 트랜잭션의 기본 격리 수준은 Read Committed로 설정되며 RC 격리 수준은 반복 불가능한 읽기를 허용하는 현상입니다. Mysql의 기본 격리 수준은 RR이며, Next-Key Lock 알고리즘을 사용하여 반복 불가능한 읽기 현상을 방지합니다.

반복 불가능한 읽기와 팬텀 읽기의 차이점은 다음과 같습니다.

  • 반복 불가능 읽기의 핵심은 수정입니다. 동일한 조건에서 처음 읽은 데이터를 다시 읽어 값이 다릅니다.
  • 팬텀 읽기의 초점은 추가 또는 삭제입니다. 동일한 조건에서 처음으로 읽은 레코드 수와 두 번째로 읽은 레코드 수가 다릅니다.

반복 읽기(REPEATABLE READ)

트랜잭션 중에 동일한 데이터를 여러 번 읽을 때 해당 값이 트랜잭션 시작 시점의 값과 일치함을 보장합니다.
반복 가능한 읽기 격리 수준에서 더티 읽기 및 반복 불가능한 읽기는 금지되지만 팬텀 읽기는 존재합니다.

팬텀 읽기는 트랜잭션의 두 쿼리에서 데이터 수가 일치하지 않음을 의미합니다. 예를 들어 한 트랜잭션은 데이터의 여러 열(Row)을 쿼리하고 다른 트랜잭션은 이때 새로운 데이터 열을 삽입합니다. 다음 쿼리에서 이전에는 없었던 여러 데이터 열이 있음을 알게 됩니다.

mysql의 기본 격리 수준은 반복 읽기이며 현재 mysql 데이터베이스의 트랜잭션 격리 수준을 보려면 다음 명령을 사용하십시오.

show variables like 'tx_isolation';select @@tx_isolation;

트랜잭션 격리 수준을 설정합니다.

set tx_isolation='READ-UNCOMMITTED';
set tx_isolation='READ-COMMITTED';
set tx_isolation='REPEATABLE-READ';
set tx_isolation='SERIALIZABLE';

이론적으로 반복 가능한 읽기는 또 다른 까다로운 문제로 이어질 것입니다: 현재 사용자가 데이터 행 범위를 읽을 때까지 팬텀 읽기, 다른 트랜잭션이 범위에 새 행을 삽입하고 사용자가 읽을 때 이 범위에 데이터 행이 있을 때 , 새로운 "팬텀" 행이 발견될 것입니다. InnoDB 스토리지 엔진은 다중 버전 동시성 제어(MVCC, Multiversion Concurrency Control) 메커니즘을 통해 이 문제를 해결합니다.

직렬화 가능(SERIALIZABLE)

가장 엄격한 트랜잭션은 모든 트랜잭션이 직렬로 실행되어야 하며 동시에 실행될 수 없습니다.

트랜잭션이 서로 충돌하지 않도록 순서를 지정하여 팬텀 읽기 문제를 해결합니다.

즉, 각 읽기 데이터 행에 공유 잠금이 추가됩니다.

Serializable은 엄청난 시간 초과와 잠금 경합을 유발할 수 있습니다.

요약 :

격리 수준 더티 읽기 반복 불가능한 읽기(NoRepeatable Read) 팬텀 읽기
커밋되지 않은 읽기(커밋되지 않은 읽기) 가능한 가능한 가능한
커밋 읽기 불가능한 가능한 가능한
반복 읽기(Repeatable read) 불가능한 불가능한 가능한
직렬화 가능 불가능한 불가능한 불가능한

MVCC

MVCC의 정식 명칭은 Multi-Version Concurrent Control, 즉 다중 버전 동시성 제어이며 그 원리는 copyonwrite와 유사합니다.

동시 트랜잭션으로 인한 문제

읽고 또 읽어라

즉, 동시 트랜잭션은 동일한 레코드를 차례로 읽습니다.

레코드를 읽는 것은 레코드에 영향을 주지 않기 때문에 같은 트랜잭션에서 같은 레코드를 동시에 읽는 데 보안 문제가 없으므로 이 작업이 허용됩니다. 문제가 없으며 동시성 제어가 필요하지 않습니다.

쓰기-쓰기

즉, 동시 트랜잭션은 동일한 레코드를 연속적으로 수정합니다.

동시 트랜잭션이 동일한 레코드를 읽고 이전 추정을 기반으로 이 레코드를 수정하도록 허용하면 이전 트랜잭션의 수정이 후속 트랜잭션의 수정으로 덮어쓰이게 됩니다. 즉, 커밋 범위 문제가 발생합니다. .

또 다른 경우 동시성 트랜잭션은 동일한 레코드에 대해 차례차례 수정을 하게 되고, 하나의 트랜잭션이 커밋된 후 다른 트랜잭션이 롤백하게 되므로 롤백으로 인해 커밋된 수정 내용이 손실되는 문제, 즉 롤백 커버리지가 발생하게 된다. 문제.

따라서 스레드 안전 문제가 있으며 두 경우 모두 업데이트 손실 문제가 있을 수 있습니다.

쓰기-읽기 또는 읽기-쓰기

즉, 두 개의 동시 트랜잭션이 각각 동일한 레코드에 대해 읽기 및 쓰기 작업을 수행합니다.

트랜잭션이 다른 트랜잭션에 의해 커밋되지 않은 개정 레코드를 읽으면 더티 읽기 문제가 있는 것입니다.

트랜잭션이 커밋된 다른 트랜잭션의 수정된 데이터만 읽을 수 있도록 제어하면 다른 트랜잭션이 수정을 커밋하기 전과 후에 이 트랜잭션이 읽은 데이터가 다르므로 반복 불가능한 읽기가 발생 했습니다 .

트랜잭션이 어떤 조건에 따라 일부 레코드를 찾은 다음 다른 것이 일부 레코드를 테이블에 삽입하면 원래 트랜잭션은 동일한 조건을 다시 쿼리했을 때 얻은 결과가 첫 번째 쿼리에서 얻은 결과와 일치하지 않는다는 것을 알게 됩니다. 그런 다음 팬텀 읽기가 발생했습니다 .

쓰기-읽기 또는 읽기-쓰기 문제에 대해 MySQL의 InnoDB는 읽기-쓰기 충돌을 더 잘 처리하기 위해 MVCC를 구현합니다.동시 읽기 및 쓰기가 있더라도 "비 차단 동시 읽기"를 잠그고 실현할 필요가 없습니다.

요약하자면, MVCC가 필요한 이유는 일반적으로 데이터베이스가 격리를 달성하기 위해 잠금을 사용하기 때문입니다.가장 원시적인 잠금은 리소스를 잠근 후 다른 스레드가 동일한 리소스에 액세스하는 것을 금지합니다. 그러나 많은 응용 프로그램의 특징은 더 많은 읽기와 적은 쓰기의 시나리오입니다.많은 데이터의 읽기 횟수가 수정 횟수보다 훨씬 많으며 읽기 데이터 간의 상호 배제가 필요하지 않습니다. 읽기 잠금과 읽기 잠금은 상호 배타적이지 않지만 쓰기 잠금, 쓰기 잠금, 읽기 잠금은 상호 배타적입니다. 이는 시스템의 동시성 기능을 크게 향상시킵니다.

나중에 사람들은 동시 읽기로는 충분하지 않다는 것을 알게 되었고 읽기와 쓰기 사이의 충돌을 방지하는 방법, 즉 데이터를 읽을 때 스냅샷과 유사한 방식으로 데이터를 저장하여 읽기 잠금과 쓰기가 충돌하지 않도록 하는 방법을 제안했습니다. 예, 서로 다른 트랜잭션 세션은 고유한 특정 버전의 데이터를 볼 수 있습니다. 물론 스냅샷은 개념적 모델이며 다른 데이터베이스는 다른 방식으로 이 기능을 구현할 수 있습니다.

MVCC 프로토콜에서 각 읽기 작업은 일관된 스냅샷 스냅샷을 볼 수 있으며 비차단 읽기를 달성할 수 있습니다. 잠그지 않고 이 스냅샷 스냅샷을 읽습니다. 스냅샷 스냅샷 버전 외에도 MVCC는 데이터가 여러 버전을 가질 수 있도록 허용합니다. 버전 번호는 타임스탬프 또는 전역적으로 증가된
트랜잭션 ID일 수 있습니다. 동일한 시점에서 다른 트랜잭션은 다른 데이터를 봅니다.

먼저 MVCC를 이해하기 전에 두 가지 정의를 명확히 해야 합니다.

  • 현재 읽기 : 읽은 데이터는 최신 버전입니다.데이터를 읽을 때 다른 동시 트랜잭션이 현재 데이터를 수정하지 않도록 해야 합니다.현재 읽기는 읽기 레코드를 잠급니다. 예: 선택 ... 공유 모드에서 잠금(공유 잠금), 선택 ... 업데이트 | 업데이트 | 삽입 | 삭제(독점 잠금)
  • 스냅샷 읽기 : 데이터가 수정될 때마다 언두 로그에 스냅샷 기록이 저장되는데, 여기서 스냅샷은 언두 로그에서 특정 버전의 스냅샷을 읽는 것이다. 이 방법의 장점은 잠금 없이 데이터를 읽을 수 있다는 점이지만, 읽은 데이터가 최신 버전이 아닐 수 있다는 단점이 있습니다. 일반 쿼리는 스냅샷 읽기입니다(예: select * from t_user where id=1, MVCC의 쿼리는 모두 스냅샷입니다).

mvcc의 구현 원리

MySQL의 MVCC는 주로 행 레코드(숨겨진 기본 키 row_id, 트랜잭션 ID trx_id, 롤백 포인터 roll_pointer), 실행 취소 로그(버전 체인) 및 ReadView(일관된 읽기 보기)의 숨겨진 필드를 통해 구현됩니다.

숨겨진 필드

MySQL에는 사용자 정의 필드 외에도 레코드의 각 행에 몇 가지 숨겨진 필드가 있습니다.

  • row_id : 데이터베이스 테이블이 기본 키를 정의하지 않으면 InnoDB는 기본 키로 row_id를 사용하여 클러스터형 인덱스를 생성합니다.
  • trx_id : 트랜잭션 ID는 새로 추가/최근 수정된 레코드의 트랜잭션 ID를 기록하며 트랜잭션 ID는 자체 증가합니다.
  • roll_pointer : 롤백 포인터는 현재 레코드의 이전 버전(실행 취소 로그에 있음)을 가리킵니다.

읽기보기

ReadView 일관성 보기는 주로 커밋되지 않은 모든 트랜잭션의 ID 배열과 생성된 가장 큰 트랜잭션 ID의 두 부분으로 구성됩니다. 예: [100,200],300. 트랜잭션 100과 200은 현재 커밋되지 않은 트랜잭션이며 트랜잭션 300은 현재 생성된(이미 커밋된) 가장 큰 트랜잭션입니다. ReadView는 SELECT 문이 실행될 때 생성되지만 ReadView를 생성하는 전략은 두 가지 트랜잭션 수준인 읽기 커밋 및 반복 읽기에서 다릅니다. 읽기 커밋 수준은 SELECT 문이 실행될 때마다 재생성됩니다. ReadView 및 반복 읽기 level은 첫 번째 SELECT 문이 실행될 때만 생성되며 이후 SELECT 문은 이전에 생성된 ReadView를 계속 사용합니다(나중에 업데이트 문이 있어도 계속 사용함).

ReadView는 MVCC가 데이터를 스냅샷할 때 생성하는 "읽기 보기"입니다. ReadView에는 4가지 더 중요한 변수가 있습니다.

  • m_ids : 활성 트랜잭션 ID 목록, 현재 시스템의 모든 활성(즉, 커밋되지 않은) 트랜잭션의 트랜잭션 ID 목록입니다.
  • min_trx_id : m_ids에서 가장 작은 트랜잭션 ID입니다.
  • max_trx_id : ReadView를 생성할 때 시스템은 id를 다음 트랜잭션에 할당해야 합니다(m_ids에서 가장 큰 트랜잭션 id가 아님), 즉 m_ids에서 가장 큰 트랜잭션 id + 1.
  • creator_trx_id : 이 ReadView를 생성한 트랜잭션의 트랜잭션 ID입니다.

버전 체인

데이터를 수정할 때 수정된 페이지 내용은 리두 로그에 기록되고(데이터베이스 재시작 후 데이터베이스 운영을 복원하기 위해) 데이터의 원본 스냅샷은 실행 취소 로그에 기록됩니다(롤백을 위해). 거래). 언두 로그는 두 가지 기능을 가지고 있는데, 트랜잭션 롤백에 사용되는 것 외에도 MVCC를 구현하는 데에도 사용됩니다.

간단한 예를 사용하여 MVCC에서 사용되는 실행 취소 로그 버전 체인의 논리 다이어그램을 그립니다.

트랜잭션 100(trx_id=100)이 insert into t_user values(1,'bend',30)를 실행할 때; 이후:

mysql 버전 체인

트랜잭션 102(trx_id=102)가 업데이트를 실행할 때 t_user set name='Li Si' where id=1; 이후:

mysql 버전 체인

트랜잭션 102(trx_id=102)가 update t_user set name='Wang Wu'(id=1인 경우)를 실행할 때; 이후:

mysql 버전 체인

특정 버전 체인의 비교 규칙은 다음과 같습니다.먼저 버전 체인의 맨 위에 있는 첫 번째 버전의 트랜잭션 ID를 꺼내고 하나씩 비교를 시작합니다.

특정 버전 체인

(여기서 min_id는 ReadView의 커밋되지 않은 트랜잭션 배열에서 가장 작은 트랜잭션 ID를 가리키고 max_id는 ReadView에서 생성된 가장 큰 트랜잭션 ID를 가리킴)

트랜잭션이 스냅샷 읽기를 수행할 때 어떤 버전의 데이터를 읽을 수 있는지, ReadView의 규칙은 다음과 같습니다.

(1) 버전 체인에 기록된 trx_id가 현재 트랜잭션 id(trx_id = creator_trx_id)와 같으면 버전 체인의 이 버전이 현재 트랜잭션에 의해 수정되어 스냅샷 레코드가 현재 트랜잭션에 표시됨을 의미합니다. .

(2) 버전 체인에 기록된 trx_id가 활성 트랜잭션의 최소 id보다 작으면(trx_id < min_trx_id) 버전 체인의 레코드가 제출되었음을 의미하므로 스냅샷 레코드가 현재 트랜잭션에 표시됩니다. .

(3) 버전 체인에 기록된 trx_id가 할당할 다음 트랜잭션 id보다 큰 경우 (trx_id > max_trx_id) , 스냅샷 레코드는 현재 트랜잭션에 표시되지 않습니다.

(4) 버전 체인에 기록된 trx_id가 최소 활성 트랜잭션 id보다 크거나 같고 버전 체인에 기록된 trx_id가 다음 할당할 트랜잭션 id보다 작을 때 (min_trx_id<= trx_id < max_trx_id), if 버전 체인에 기록된 trx_id가 활성화됨 트랜잭션 id 목록 m_ids에서 ReadView가 생성될 때 레코드를 수정하는 트랜잭션이 제출되지 않았으므로 스냅샷 레코드가 현재 트랜잭션에 표시되지 않음을 나타냅니다. 스냅샷 레코드는 현재 트랜잭션에 표시됩니다.

트랜잭션이 id=1인 레코드의 스냅샷을 읽을 때 id=1인 t_user에서 *를 선택하고 버전 체인의 스냅샷에서 최신 레코드부터 시작하여 이 4가지 조건을 차례로 판단하여 특정 버전의 스냅샷이 현재에 적합합니다. 트랜잭션이 표시됩니다. 그렇지 않으면 레코드의 이전 버전을 계속 비교합니다.

MVCC는 주로 RU 격리 수준에서 더티 읽기 문제를 해결하고 RC 격리 수준에서 반복 불가능한 읽기 문제를 해결하는 데 사용되므로 MVCC는 RC(더티 읽기 해결) 및 RR(반복 불가능 읽기 해결) 격리에서만 적용됩니다. 즉, MySQL은 RC 및 RR 격리 수준에서 스냅샷 읽기에 대해서만 ReadView를 생성합니다.

차이점은 RC 격리 수준에서 각 스냅샷 읽기는 최신 ReadView를 생성하고 RR 격리 수준에서는 트랜잭션의 첫 번째 스냅샷 읽기만 ReadView를 생성하고 후속 스냅샷 읽기는 첫 번째로 생성된 ReadView를 사용한다는 것입니다. 시간. .

MySQL은 MVCC를 통해 트랜잭션의 차단 가능성을 줄입니다.

예: T1은 레코드 X의 값을 업데이트하려고 하므로 T1은 먼저 X의 잠금을 획득한 다음 업데이트해야 합니다. 즉, x3이라고 가정하고 X의 새 버전을 만들어야 합니다.

T1이 X의 잠금을 해제하지 않았다고 가정하면 T2는 X의 값을 읽으려고 하고 이때 차단되지 않으며 MYDB는 x2와 같은 이전 버전의 X를 반환합니다. 이러한 방식으로 최종 실행 결과는 T2가 먼저 실행되고 T1이 나중에 실행되며 스케줄링 시퀀스는 여전히 직렬화 가능하다는 것과 같습니다. X에 이전 버전이 없으면 T1이 잠금을 해제할 때까지만 기다릴 수 있습니다.

그래서 확률이 줄어듭니다.

필기 트랜잭션 격리 수준

레코드의 최신 버전이 잠긴 경우 다른 트랜잭션이 이 레코드를 수정하거나 읽으려고 할 때 MYDB는 이전 버전의 데이터를 반환합니다.

이때 가장 최근에 잠긴 버전은 다른 트랜잭션에 보이지 않는다고 볼 수 있습니다.

읽기에 전념

즉, 트랜잭션이 데이터를 읽을 때 커밋된 트랜잭션이 생성한 데이터만 읽을 수 있습니다.

버전 가시성은 트랜잭션 격리와 관련이 있습니다.

MYDB가 지원하는 가장 낮은 수준의 트랜잭션 격리는 "읽기 커밋됨"입니다. 즉, 트랜잭션이 데이터를 읽을 때 커밋된 트랜잭션에서 생성된 데이터만 읽을 수 있습니다. 가장 낮은 읽기 커밋을 보장하는 이점은 4장에서 설명했습니다(계단식 롤백이 커밋 의미 체계와 충돌하지 않도록 방지).

MYDB는 읽기 제출을 구현하고 각 버전에 대해 앞서 언급한 XMIN 및 XMAX라는 두 가지 변수를 유지합니다.

  • XMIN : 이 버전을 생성한 트랜잭션 번호
  • XMAX : 이 버전의 트랜잭션 번호 삭제

XMIN은 버전이 생성될 때 채워져야 하고, XMAX는 버전이 삭제되거나 새로운 버전이 나올 때 채워져야 합니다.

XMAX 변수는 DM 레이어가 삭제 작업을 제공하지 않는 이유도 설명합니다. 버전을 삭제하려면 XMAX만 설정하면 됩니다. 이런 식으로 이 버전은 XMAX 이후의 각 트랜잭션에 표시되지 않습니다. 삭제되었습니다.

읽기 커밋에서 트랜잭션에 대한 버전의 가시성 논리는 다음과 같습니다.

(XMIN == Ti and                             // 由Ti创建且
    XMAX == NULL                            // 还未被删除
)
or                                          // 或
(XMIN is commited and                       // 由一个已提交的事务创建且
    (XMAX == NULL or                        // 尚未删除或
    (XMAX != Ti and XMAX is not commited)   // 由一个未提交的事务删除
))

조건이 참이면 버전이 Ti에 표시됩니다. 그런 다음 Ti에 적합한 버전을 얻으려면 최신 버전에서 시작하여 가시성을 하나씩 확인하면 됩니다. 사실이면 직접 반환할 수 있습니다.

다음 메서드는 레코드가 트랜잭션 t에 표시되는지 여부를 결정합니다.

private static boolean readCommitted(TransactionManager tm, Transaction t, Entry e) {
    
    
    long xid = t.xid;
    long xmin = e.getXmin();
    long xmax = e.getXmax();
    if(xmin == xid && xmax == 0) return true;

    if(tm.isCommitted(xmin)) {
    
    
        if(xmax == 0) return true;
        if(xmax != xid) {
    
    
            if(!tm.isCommitted(xmax)) {
    
    
                return true;
            }
        }
    }
    return false;
}

반복 읽기

비반복성으로 인해 트랜잭션이 실행 중에 동일한 데이터 항목을 읽을 때 다른 결과를 얻습니다. 다음 결과와 같이 X를 더한 초기 값은 0입니다.


T1 begin
R1(X) // T1 读得 0
T2 begin
U2(X) // 将 X 修改为 1
T2 commit
R1(X) // T1 读的 

T1이 X를 두 번 읽고 판독 결과가 다른 것을 볼 수 있습니다. 이러한 상황을 피하려면 더 엄격한 격리 수준, 즉 반복 가능한 읽기를 도입해야 합니다.

T1이 두 번째로 읽을 때 제출된 T2가 수정한 값을 읽게 되므로(이 수정 트랜잭션은 XMAX의 원래 버전과 XMIN의 새 버전임) 이 문제가 발생합니다. 따라서 트랜잭션이 시작될 때 종료된 트랜잭션에서 생성된 데이터 버전만 읽을 수 있다고 규정할 수 있습니다.

이 규칙에 따르면 거래는 2가지 사항을 무시해야 합니다.

(1) 이 거래 이후에 시작된 거래 데이터;

(2) 트랜잭션이 시작될 때 여전히 활성 상태였던 트랜잭션의 데이터.

첫 번째 항목의 경우 거래 ID만 비교하면 됩니다. 두 번째 항목은 트랜잭션 Ti의 시작 부분에 현재 활성 트랜잭션 SP(Ti)를 모두 기록해야 하며, 특정 버전이 기록되어 있으면 SP(Ti)에서 XMIN이 Ti에 보이지 않아야 합니다.

따라서 반복 읽기의 판단 논리는 다음과 같습니다.

(XMIN == Ti and                 // 由Ti创建且
 (XMAX == NULL or               // 尚未被删除
))
or                              // 或
(XMIN is commited and           // 由一个已提交的事务创建且
 XMIN < XID and                 // 这个事务小于Ti且
 XMIN is not in SP(Ti) and      // 这个事务在Ti开始前提交且
 (XMAX == NULL or               // 尚未被删除或
  (XMAX != Ti and               // 由其他事务删除但是
   (XMAX is not commited or     // 这个事务尚未提交或
XMAX > Ti or                    // 这个事务在Ti开始之后才开始或
XMAX is in SP(Ti)               // 这个事务在Ti开始前还未提交
))))

따라서 스냅샷 데이터를 저장하기 위해 트랜잭션을 추상화하는 구조를 제공해야 합니다(트랜잭션이 생성될 때 트랜잭션은 여전히 ​​활성 상태였습니다).

// vm对一个事务的抽象
public class Transaction {
    
    
    public long xid;
    public int level;
    public Map<Long, Boolean> snapshot;
    public Exception err;
    public boolean autoAborted;

    //事务id  隔离级别  快照
    public static Transaction newTransaction(long xid, int level, Map<Long, Transaction> active) {
    
    
        Transaction t = new Transaction();
        t.xid = xid;
        t.level = level;
        if(level != 0) {
    
    
            //隔离级别为可重复读,读已提交不需要快照信息
            t.snapshot = new HashMap<>();
            for(Long x : active.keySet()) {
    
    
                t.snapshot.put(x, true);
            }
        }
        return t;
    }

    public boolean isInSnapshot(long xid) {
    
    
        if(xid == TransactionManagerImpl.SUPER_XID) {
    
    
            return false;
        }
        return snapshot.containsKey(xid);
    }
}

구성 방법의 활성은 현재 활성 트랜잭션을 모두 저장합니다.

따라서 반복 읽기의 격리 수준에서 버전이 트랜잭션에 표시되는지 여부에 대한 판단은 다음과 같습니다.

private static boolean repeatableRead(TransactionManager tm, Transaction t, Entry e) {
    
    
    long xid = t.xid;
    long xmin = e.getXmin();
    long xmax = e.getXmax();
    if(xmin == xid && xmax == 0) return true;
 
    if(tm.isCommitted(xmin) && xmin < xid && !t.isInSnapshot(xmin)) {
    
    
        if(xmax == 0) return true;
        if(xmax != xid) {
    
    
            if(!tm.isCommitted(xmax) || xmax > xid || t.isInSnapshot(xmax)) {
    
    
                return true;
            }
        }
    }
    return false;
}

버전 점프

버전 점프 문제의 경우 X가 초기에 버전 x0만 있고 T1과 T2가 모두 반복 가능한 읽기 격리 수준이라고 가정하여 다음 상황을 고려하십시오.

T1 begin
T2 begin
R1(X) // T1读取x0
R2(X) // T2读取x0
U1(X) // T1将X更新到x1
T1 commit
U2(X) // T2将X更新到x2
T2 commit

이 상황은 실제로는 괜찮지만 논리적으로 올바르지는 않습니다.

T1은 X0에서 x1로 X를 업데이트합니다. 이는 정확합니다. 그러나 T2는 x1 버전을 건너뛰고 x0에서 x2로 X를 업데이트합니다.

읽기 커밋은 버전 점프를 허용하지만 반복 읽기는 버전 점프를 허용하지 않습니다.

버전 점프를 해결하기 위한 아이디어: Ti가 X를 수정해야 하고 X가 Ti에 보이지 않는 트랜잭션 Tj에 의해 수정된 경우 Ti는 롤백해야 합니다.

MVCC를 구현하면 트랜잭션을 실행 취소하거나 롤백할 수 있습니다. 이 트랜잭션을 중단된 것으로 표시하기만 하면 됩니다.

이전 장에서 언급한 가시성에 따르면 각 트랜잭션은 커밋된 다른 트랜잭션에서 생성된 데이터만 볼 수 있으며 중단된 트랜잭션에서 생성된 데이터는 다른 트랜잭션에 영향을 미치지 않습니다.

if(Visibility.isVersionSkip(tm, t, entry)) {
    
    
    System.out.println("检查到版本跳跃,自动回滚");
    t.err = Error.ConcurrentUpdateException;
    internAbort(xid, true);
    t.autoAborted = true;
    throw t.err;
}

앞에서 Ti가 Tj에게 보이지 않는 두 가지 상황이 있음을 요약했습니다.

(1) XID(Tj) > XID(Ti) Ti 이후에 생성된 수정 버전

(2) SP(Ti) Ti의 Tj가 생성되면 수정된 버전이 생성되었지만 아직 제출되지 않았습니다.

버전 점프 확인은 먼저 수정될 데이터 X의 가장 최근에 제출된 버전을 꺼내고 최신 버전의 작성자가 현재 트랜잭션에 표시되는지 확인합니다.구체적인 구현은 다음과 같습니다.

public static boolean isVersionSkip(TransactionManager tm, Transaction t, Entry e) {
    
    
    long xmax = e.getXmax();
    if(t.level == 0) {
    
    
        return false;
    } else {
    
    
        return tm.isCommitted(xmax) && (xmax > t.xid || t.isInSnapshot(xmax));
    }
}

저자 소개:

첫 번째 작업: Mark , 수석 빅 데이터 아키텍트, Java 아키텍트, Java, 빅 데이터 아키텍처 및 개발 분야에서 거의 20년의 경력. 수석 아키텍처 멘토, 여러 중급 Java 및 수석 Java 변환 설계자 위치를 성공적으로 안내했습니다.

두 번째 작업: Nien , 수석 시스템 아키텍트, IT 분야 수석 작가, 유명 블로거. 지난 20년 동안 고성능 웹 플랫폼, 고성능 커뮤니케이션, 고성능 검색, 데이터 마이닝 분야에서 3단계 아키텍처 연구, 시스템 아키텍처, 시스템 분석, 핵심 코드 개발에 종사했다. 수석 아키텍처 멘토, 여러 중급 Java 및 수석 Java 변환 설계자 위치를 성공적으로 안내했습니다.

뒤에서 이렇게 말했습니다.

지속적인 반복과 지속적인 업그레이드 는 Nien 팀의 신조입니다.

지속적인 반복과 지속적인 업그레이드는 "0부터 시작, 손글씨 MySQL"의 영혼이기도 합니다.

추후에 더 많은 실제 면접 질문을 모을 예정이며, 동시에 면접 문제가 발생하면 니엔의 커뮤니티 "Technical Freedom Circle(구 Crazy Maker Circle)"에 오셔서 소통하고 도움을 요청할 수 있습니다.

우리의 목표는 세계 최고의 "필기 MySQL" 인터뷰 모음을 만드는 것입니다.

기술적 자유의 실현 경로 PDF:

아키텍처의 자유 실현:

" 8자형 1형 템플릿을 철저히 이해하면 누구나 아키텍처를 수행할 수 있습니다. "

" 10Wqps 검토 플랫폼, 어떻게 구성할 것인가? 이것이 스테이션 B가 하는 일입니다! ! ! "

" Alibaba Two Sides: 수천만, 수십억 데이터의 성능을 최적화하는 방법은 무엇입니까?" 교과서 수준의 답이 나온다 "

" Peak 21WQps, DAU 1억, 스몰게임 'Sheep a Sheep' 구성은? "

" 100억 수준의 주문을 예약하는 방법, 큰 공장의 뛰어난 솔루션에 도달 "

" 두 개의 큰 공장 100억 수준의 빨간 봉투 건축 계획 "

... 더 많은 아키텍처 기사, 추가 중

응답의 자유를 실현하십시오.

" 반응성 성경: 10W 단어, 스프링 반응성 프로그래밍 자유 실현 "

" 플럭스, 모노, 리액터 컴뱃(역사상 가장 완성도 높은) " 의 구버전입니다.

봄 구름의 자유를 실현하십시오.

" 봄 구름 알리바바 연구 성경 "

" Sharding-JDBC 기본 원칙 및 핵심 사례(역사상 가장 완전함) "

" 하나의 기사로 완료: SpringBoot, SLF4j, Log4j, Logback 및 Netty(역사상 가장 완전한) 사이의 혼란스러운 관계 "

Linux의 자유를 실현하십시오:

" Linux 명령 백과사전: 2W 더 많은 단어로 한 번에 Linux 자유 실현 "

온라인 자유 실현:

" TCP 프로토콜에 대한 자세한 설명(역사상 가장 완전함) "

" 세 개의 네트워크 테이블: ARP 테이블, MAC 테이블, 라우팅 테이블, 네트워크 자유를 실현하십시오!" ! "

분산 잠금의 자유를 실현하십시오.

" Redis Distributed Lock(그림 - 두 번째 이해 - 역사상 가장 완전한) "

" Zookeeper 분산 잠금 - 다이어그램 - 두 번째 이해 "

킹 컴포넌트의 자유를 실현하십시오.

" 대기열의 왕: 파괴자 원칙, 아키텍처 및 소스 코드 침투 "

" The King of Cache: Caffeine Source Code, Architecture, and Principles (역사상 가장 완전한, 10W 초긴 텍스트) "

" 캐시의 제왕: 카페인의 사용(역사상 가장 완전한) "

" Java 에이전트 프로브, 바이트코드 강화 ByteBuddy(역사상 가장 완전함) "

인터뷰 질문을 자유롭게 실현하십시오.

4000페이지의 "Nin's Java 인터뷰 모음" 40개 주제

Nien의 건축 노트 및 인터뷰 질문의 PDF 파일 업데이트를 받으려면 다음 "Technical Freedom Circle" 공식 계정으로 이동하십시오↓↓↓

추천

출처blog.csdn.net/crazymakercircle/article/details/131297906