사전 지식
- 내부는 B + 트리 구조를 사용하여 디스크에 저장되며, B + 트리 노드는 페이지의 가장 작은 단위 인 페이지, 기본적으로 16KB의 페이지, 테이블 데이터 레코드는 B + 트리의 리프 노드에 저장되며, 데이터를 수정, 삭제, 삽입해야 할 때 디스크에서 작동 할 페이지에 따라야합니다.
- 디스크의 순차적 쓰기는 일반적으로 기계식 하드 드라이브를 사용하는 임의 쓰기보다 훨씬 효율적이며, 기계식 하드 드라이브 쓰기 데이터에는 디스크 탐색, 디스크 회전 주소 지정, 데이터 쓰기 시간, 시간이 오래 걸리며, 순차적 쓰기 인 경우 탐색 및 디스크 회전 시간을 제거하면 효율성이 몇 배 더 높아집니다.
- 메모리에서의 데이터 읽기 및 쓰기는 디스크에서의 데이터 읽기 및 쓰기보다 훨씬 빠릅니다.
MYSQL 데이터 손실 없는 원칙 분석 보장
다음 구문을 실행하면 어떻게 될지 생각해 보세요:
start transaction;
update t_user set name = 'ABCJava' where user_id = 666;
commit;
일반적인 사고에 따르면 일반적인 프로세스는 다음과 같습니다:
- user_id=666 레코드가 있는 p1 페이지를 찾아 디스크에서 메모리로 p1을 로드합니다.
- p1의 user_id=666 레코드 정보에 대한 인메모리 수정
- mysql이 커밋 명령을 수신합니다.
- 디스크에 페이지 p1 쓰기
- 클라이언트에 성공적인 업데이트를 반환합니다.
위의 프로세스를 통해 데이터가 디스크에 보존됩니다.
요구 사항을 다음과 같이 변경합니다:
start transaction;
update t_user set name = 'Java' where user_id = 666;
update t_user set name = 'javacodeabcd' where user_id = 888;
commit;
처리 과정을 살펴보세요:
- user_id=666 레코드가 있는 p1 페이지를 찾아 디스크에서 메모리로 p1을 로드합니다.
- p1의 user_id=666 레코드 정보에 대한 인메모리 수정
- user_id=888 레코드가 있는 p2 페이지를 찾아서 디스크에서 메모리로 p2를 로드합니다.
- p2에서 user_id=888 레코드 정보에 대한 인메모리 수정
- mysql이 커밋 명령을 수신합니다.
- 디스크에 페이지 p1 쓰기
- 디스크에 2페이지 쓰기
- 클라이언트에 성공적인 업데이트를 반환합니다.
위 프로세스의 문제점을 확인하세요.
- 6의 성공 후 mysql이 다운되었다고 가정하면, 이때 p1 수정 사항은 디스크에 기록되었지만 p2 수정 사항은 디스크에 기록되지 않았기 때문에 궁극적으로 user_id=666 레코드가 성공적으로 수정되고 user_id=888 데이터 수정이 실패하면 데이터에 문제가 있습니다.
- 위의 p1과 p2는 디스크의 다른 위치에 위치할 수 있으며, 이는 무작위 디스크 쓰기를 포함하므로 전체 프로세스도 더 오래 걸립니다.
위의 문제는 데이터 신뢰성을 보장할 수 없다는 점과 무작위 쓰기로 인해 시간이 오래 걸린다는 점, 두 가지로 요약할 수 있습니다.
위의 질문과 관련하여 mysql이 어떻게 최적화되어 있는지 살펴보면, mysql은 내부적으로 위의 두 가지 업데이트 작업에 대해 파일 인 재실행 로그를 도입하고, mysql은 다음을 구현합니다:
mysql 내부 재실행 로그 버퍼는 메모리 영역의 일부이며 배열 구조로 이해되며 재실행 로그 파일에 데이터를 쓰고 먼저 재실행 로그 버퍼의 내용을 쓰고 이후이 버퍼의 내용이 디스크의 재실행 로그 파일에 쓰여지며이 재실행 로그 버퍼는 다음과 같습니다. 이 재실행 로그 버퍼는 mysql의 모든 연결이 공유하는 메모리 영역이며 재사용할 수 있습니다.
user_id=666인 레코드를 r1이라고 하고, user_id=888인 레코드를 r2라고 합니다.
R1 레코드가 있는 데이터 페이지 p1을 찾아 디스크에서 메모리로 로드합니다.
메모리에서 p1에서 r1의 위치를 찾은 다음 p1을 수정하면이 프로세스는 rb1 (내부적으로 트랜잭션 번호 trx_id를 포함), rb1로 기록되며, 이때 p1의 정보는 메모리에서 수정되었으며 디스크의 p1 데이터는 다릅니다.
R2 레코드가 있는 데이터 페이지 p2를 찾아 디스크에서 메모리로 로드합니다.
메모리에서 p2에서 r2의 위치를 찾은 다음 p2를 수정하면이 프로세스는 rb2 (내부적으로 트랜잭션 번호 trx_id 포함), rb2로 다시 실행 로그 버퍼 배열에 기록되며 이때 p2의 정보는 메모리에서 수정되고 디스크의 p2 데이터는 다릅니다.
이 시점에서 재실행 로그 버퍼 배열 [rb1,rb2]에는 2개의 레코드가 있습니다.
mysql이 커밋 명령을 수신합니다.
재실행 로그 버퍼 배열의 내용을 작성된 재실행 로그 파일에 씁니다:
1.start trx=10; 2.rb1 쓰기 3.rb2 쓰기 4.end trx=10;업데이트가 성공했음을 클라이언트에 반환합니다.
위의 프로세스가 실행된 후 데이터는 다음과 같이 표시됩니다:
- 메모리 p1, p2 페이지가 수정되었고 디스크와 동기화되지 않았으며, 현재 메모리의 데이터 페이지와 디스크 데이터 페이지가 일치하지 않으며, 현재 메모리의 데이터 페이지를 더티 페이지라고 합니다.
- 페이지 p1 및 p2의 변경 내용은 디스크의 redolog 파일에 유지되며 손실되지 않습니다.
위 9단계의 과정을 자세히 살펴보면, 재실행 로그의 성공적인 트랜잭션 기록은 시작과 끝, 재실행 로그 파일에 시작과 끝에 해당하는 trx_id가 쌍으로 나타나면 트랜잭션 실행이 성공한 것이고, 끝의 시작만 있으면 문제가 있는 것이 아닙니다.
그렇다면 페이지 p1과 p2의 변경 사항은 언제 디스크에 동기화되나요?
재실행 로그는 mysql의 모든 연결이 공유하는 파일로, mysql에서 삽입, 삭제 및 업데이트를 수행하는 과정은 위의 과정과 유사하며 먼저 메모리 페이지 데이터에서 수정 한 다음 수정 프로세스가 재실행 로그가있는 디스크 파일에 지속 된 다음 성공으로 돌아갑니다. 재실행 로그 파일에는 크기가 있으며 재사용해야합니다. 재실행 로그가 가득 차거나 시스템이 상대적으로 유휴 상태인 경우 재실행 로그 파일의 내용이 처리되며, 그 과정은 다음과 같습니다:
- 완전한 trx_id에 해당하는 재실행 로그 메시지를 읽고 처리합니다.
- 예를 들어, 시작 끝이 포함된 trx_id=10의 전체 내용을 읽으면 이 트랜잭션 작업이 성공했음을 나타내고, 계속해서
- p1이 메모리에 존재하는지 확인하고, 존재하면 p1 정보를 p1이 있는 디스크에 직접 쓰고, p1이 메모리에 존재하지 않으면 디스크에서 메모리로 p1을 로드하고 재실행 로그의 정보를 통해 메모리에서 p1을 수정한 다음 디스크에 씁니다.
위의 업데이트 후 p1이 메모리에 존재하고 p1이 이미 수정되어 디스크에 직접 플러시할 수 있습니다.
위의 업데이트 후 mysql이 다운되었다가 다시 시작되면 p1이 메모리에 존재하지 않으며, 시스템에서 복구 처리를 위해 재실행 로그 파일의 내용을 읽습니다.
- 재실행 로그 파일에서 trx_id=10이 차지하는 공간을 처리된 것으로 표시하면 이 공간은 재사용을 위해 해제됩니다.
- 2단계에서 끝이 없다는 내용, 즉 트랜잭션 실행이 절반으로 실패한 내용에 해당하는 trx_id를 읽으면 이때 이 레코드가 유효하지 않으므로 직접
위의 프로세스를 통해 데이터는 디스크의 페이지에 확실히 유지되고 손실되지 않으므로 안정성을 확보할 수 있습니다.
그리고 수정 작업의 첫 페이지를 메모리에 먼저 사용한 다음 재실행 로그 파일에 기록하면 재실행 로그가 순서대로 기록되고 io 순차 쓰기를 사용하면 효율성이 매우 높아지며 사용자 응답에 비해 속도가 빨라집니다.
데이터 페이지의 변경 사항을 디스크에 지속하기 위해 여기에서도 다시 재실행 로그의 내용을 읽는 비동기 방식이 사용되며, 페이지의 변경 사항을 디스크에 브러시하는 이 디자인도 매우 좋은 비동기 브러시 작업입니다!
그러나 트랜잭션 커밋이 재실행 로그가 충분하지 않다는 것을 방금 발견 한 상황이 있으며, 이때 먼저 재실행 로그의 내용을 처리하기 위해 중지 한 다음 후속 작업에서이 상황이 발생하면 모든 것이 약간 느린 응답이됩니다.
mysql에는 빈 로그가 있으며 트랜잭션 프로세스에서도 빈 로그, 우선 빈 로그의 역할, 데이터베이스의 세부 레코드에있는 빈 로그가 작업을 수행하는 데이터베이스 작업의 흐름이며이 흐름도 매우 중요하며 마스터-슬레이브 동기화는 빈 로그의 사용으로 달성하고 슬레이브 라이브러리는 빈 로그에서 마스터 라이브러리를 읽는 것입니다. 마스터-슬레이브 동기화는 빈로그를 사용하여 슬레이브가 마스터의 빈로그에 있는 정보를 읽은 다음 슬레이브에서 실행하고 마지막으로 슬레이브와 마스터가 서로 동기화되는 방식으로 이루어집니다. 데이터웨어 하우스로 추출 된 비즈니스 데이터 인 BI 시스템 등의 기능을 달성하기 위해 빈 로그와 같은 다른 시스템도 빈 로그의 기능을 사용할 수 있으며,이 프로젝트는 메인 라이브러리에서 라이브러리에서 시뮬레이션하여 빈 로그의 기능을 읽을 수 있습니다. 즉, 자바 프로그램을 통해 데이터베이스를 모니터링하여 물의 세부 사항을 변경할 수 있으며, 이것은 우리가 조금 브레인 스토밍 할 수 있고 많은 일을 할 수 있습니다. 많은 일을 할 수 있고, 관심있는 친구들은 연구에 갈 수 있습니다. 따라서 mysql 용 binlog는 재실행 로그와 빈 로그가 일관되게 작성되도록하는 시스템이 어떻게 성공적으로 작성되는지 확인하는 데에도 매우 중요합니다.
또는 업데이트를 예로 들어 보겠습니다:
start transaction;
update t_user set name = 'ABCJava' where user_id = 666;
update t_user set name = 'javacodeabcd' where user_id = 888;
commit;
트랜잭션에는 많은 작업이 있을 수 있으며, 이러한 작업은 많은 빈로그 로그를 쓰게 되며, 쓰기 속도를 높이기 위해 먼저 메모리에서 생성된 빈로그 로그의 전체 프로세스를 먼저 빈로그 캐시 캐시에 저장한 다음 나중에 빈로그 파일에 대한 일회성 지속성의 내용으로 빈로그 캐시에 저장합니다.
절차는 다음과 같습니다:
user_id=666인 레코드를 r1이라고 하고, user_id=888인 레코드를 r2라고 합니다.
R1 레코드가 있는 데이터 페이지 p1을 찾아 디스크에서 메모리로 로드합니다.
메모리에서 p1 변경하기
p1 수정 작업을 재실행 로그 버퍼에 기록합니다.
빈로그 캐시로 p1 수정 사항 스트리밍하기
R2 레코드가 있는 데이터 페이지 p2를 찾아 디스크에서 메모리로 로드합니다.
메모리에서 P2를 변경하기
P2 수정 작업을 재실행 로그 버퍼에 로깅하기
p2 수정 로그를 binlog 캐시로 스트리밍하기
mysql이 커밋 명령을 수신합니다.
trx_id=10을 갖는 재실행 로그 버퍼를 재실행 로그 파일에 기록하여 디스크에 유지하며, 이 단계를 재실행 로그 준비라고 하며 다음과 같습니다.
trx_id=10을 가진 binlog 캐시를 binlog 파일에 쓰고, 디스크에 유지합니다.
재실행 로그에 데이터 쓰기: end trx=10; 재실행 로그에 트랜잭션이 완료되었음을 나타내며, 이 단계를 재실행 로그 커밋이라고 합니다.
업데이트가 성공했음을 클라이언트에 반환합니다.
를 사용하여 위의 프로세스에서 가능한 몇 가지 시나리오를 분석할 수 있습니다:
10단계 작업이 완료되면 mysql이 다운됩니다.
다운타임, 모든 수정 사항은 메모리에 있으며, mysql 재시작 후 메모리 수정 사항은 디스크에 동기화되지 않고 디스크 데이터에 영향을 미치지 않으므로 영향을 미치지 않습니다.
12단계가 끝나면 mysql이 다운됩니다.
이때 redo 로그 준비 프로세스가 redo 로그 파일에 기록되지만 binlog 쓰기가 실패했습니다. 이때 mysql 재시작 복구 처리를 위해 redo 로그 읽기, trx_id = 10 레코드에 대한 쿼리는 준비 상태이며, binlog로 이동하여 binlog에서 trx_id = 10 작업이 있는지 여부를 찾습니다. 가 존재하는지 확인하고, 존재하지 않으면 binlog 쓰기가 실패했음을 의미하며, 이때 작업을 롤백할 수 있습니다!
13단계가 실행되면 mysql이 다운됩니다.
이때 redo 로그 준비 프로세스는 redo 로그 파일에 기록되지만 binlog 쓰기가 실패했습니다. 이때 mysql 재시작 복구 처리를 위해 redo 로그 읽기, trx_id=10 레코드에 대한 쿼리는 준비 상태이며 binlog로 이동하여 binlog에서 trx_id=10 작업이 있는지 확인합니다! 를 확인한 다음 위의 14단계와 15단계를 진행합니다.
요약하기
위의 프로세스는 두 가지 방식으로 더 잘 설계되었습니다.
로그 우선, io 순차적 쓰기, 비동기 작업, 효율적인 작업 수행
데이터 페이지의 경우 먼저 메모리에서 수정 한 다음 io 순차 쓰기를 사용하여 다시 실행 로그 파일에 지속 한 다음 비동기 적으로 다시 실행 로그를 처리하고 데이터 페이지의 수정 사항을 디스크에 지속하는 것이 매우 효율적이며 전체 프로세스는 실제로 MySQL에서 자주 이야기되는 WAL 기술이며 WAL의 전체 이름은 Write-Ahead Logging이며 그 [...]입니다. 핵심은 로그를 먼저 쓴 다음 디스크를 쓰는 것입니다.
2단계 커밋으로 재실행 로그와 빈로그의 일관성 보장
재실행 로그와 빈로그의 일관성을 보장하기 위해 여기서는 재실행 로그와 빈로그가 3단계로 작성되는 2단계 커밋 기법이 사용됩니다:
위의 3단계는 동일한 trx_id에 연결된 재실행 로그와 binlog의 안정성을 보장합니다.
위의 2 가지 우수한 디자인에 대해 일반적으로 개발 과정에서 다음 2 가지 일반적인 사례에서 배울 수도 있습니다.
사례: 이커머스에서 펀드 계좌의 잦은 이동을 위한 솔루션
이커머스에는 계정 테이블과 계정 흐름 테이블이 있으며, 두 테이블은 다음과 같이 구성되어 있습니다:
drop table IF EXISTS t_acct;
create table t_acct(
acct_id int primary key NOT NULL COMMENT '계정 ID',
balance decimal(12,2) NOT NULL COMMENT '계정 잔액',
version INT NOT NULL DEFAULT 0 COMMENT '버전 번호, 각 업데이트+1'
)COMMENT '계정 차트';
drop table IF EXISTS t_acct_data;
create table t_acct_data(
id int AUTO_INCREMENT PRIMARY KEY COMMENT ' ,
acct_id int primary key NOT NULL COMMENT '계정 ID',
price DECIMAL(12,2) NOT NULL COMMENT '거래 금액',
open_balance decimal(12,2) NOT NULL COMMENT '잔액 열기',
end_balance decimal(12,2) NOT NULL COMMENT '잔액 마감'
) COMMENT '계정 흐름 테이블 ';
INSERT INTO t_acct(acct_id, balance, version) VALUES (1,10000,0);
위는 계정 테이블 t_acct에 데이터를 삽입하고 잔액은 10000이고 주문이 성공하거나 충전되면 위의 두 테이블에서 작동하고 t_acct의 데이터를 수정하고 그런데 t_acct_data 테이블에 흐름을 작성하며이 t_acct_data 테이블에는 개장 및 마감 흐름이 있으며 관계는 다음과 같습니다:
end_balance = open_balance + price;
open_balance언제 비즈니스를 운영하려면,t_acct테이블의 잔액 값입니다.
계정 1에 100을 충전하는 경우, 그 과정은 다음과 같습니다:
t1: 트랜잭션 시작: 트랜잭션 시작;
t2R1= (select * from t_acct where acct_id = 1);
t3몇 가지 변수를 생성합니다.
v_balance = R1.balance;
t4: 업데이트 t_acct set balnce = v_balance+100,version = version + 1 where acct_id = 1;
t5:insert into t_acct_data(acct_id,price,open_balnace,end_balance)
values (1,100,#v_balance#,#v_balance+100#)
t6: 트랜잭션 커밋: 커밋;
위 프로세스의 문제점을 분석합니다:
2개의 스레드 "스레드" 1, "스레드" 2 시뮬레이션을 각각 열어 100을 충전하면 정상적인 상황에서는 데이터가 다음과 같아야 합니다:
t_acct테이블 레코드:
(1,10200,1);
t_acct_data이 테이블은 두 개의 데이터를 생성합니다:
(1,100,10000,10100);
(2,100,10100,10200);
하지만 2개의 스레드가 동시에 t2로 실행되어 R1 레코드 정보를 가져오는 경우, 변수 v_balance의 값도 동일하므로 실행이 완료된 후 데이터는 다음과 같이 됩니다:
t_acct ,10200
t_acct_data이 테이블은 두 개의 데이터를 생성합니다:
1,100,10000,10100;
2,100,10100,10100;
t_acct_data가 동일한 데이터 2개를 생성하는 상황은 동시성으로 인해 발생하는 문제입니다.
t1트랜잭션 시작 트랜잭션 열기
t2R1= (select * from t_acct where acct_id = 1);
t3몇 가지 변수를 생성합니다.
v_version = R1.version;
v_balance = R1.balance;
v_open_balance = v_balance;
v_balance = R1.balance + 100;
v_open_balance = v_balance;
t3: R1용 편집
t4업데이트 작업을 수행합니다.
int count = (update t_acct set balance = #v_balance#,version = version + 1 where acct_id = 1 and version = #v_version#);
t5: if(count==1){
// _acct_data테이블 쓰기 데이터
insert into t_acct_data(acct_id,price,open_balnace,end_balance) values (1,100,#v_open_balance#,#v_open_balance#)
//트랜잭션 제출
commit;
}else{
//트랜잭션 롤백
rollback;
}
위의 프로세스는 동일한 R1 데이터를보기 위해 2 개의 스레드가 동시에 t2로 이동하지만 마침내 데이터베이스가 잠길 때 t4로 이동하면 mysql에서 2 개의 업데이트 스레드가 실행 대기열에 대기하고 마지막으로 하나의 업데이트 만 1 행 수의 영향을 반환 한 다음 t5에 따라 롤백되고 다른 하나는 제출되어 동시성을 피하기 위해 제출됩니다. 동시성으로 인한 문제.
위의 프로세스를 분석하면 어떤 문제가 발생할 수 있을까요?
방금 위에서 언급했듯이 동시성이 큰 경우 10 개의 스레드가 동시에 t2로 실행되는 등 일부만 성공하면 그중 1 개만 성공하고 나머지 9 개는 실패하며 동시성이 큰 경우 실패 할 확률이 상대적으로 높으며 동시에 테스트 할 수 있으며 실패율이 매우 높으며 다음은 계속 최적화됩니다.
문제를 주로 위의 쓰기 t_acct_data에 나타나는 문제를 분석하면이 테이블의 작업이 없으면 작업 완료에 대한 업데이트로 직접 속도가 매우 빠르며 위의 첫 번째 로그에서 배운 mysql을 사용한 다음 비동기 방식으로 플레이트를 닦는 방법, 여기에서도 이러한 사고 방식으로 사용할 수 있으며, 먼저 트랜잭션 로그를 기록한 다음 트랜잭션 로그에 따라 비동기로 t에 기록되는 트랜잭션 흐름이됩니다. _acct_data 테이블에 기록됩니다.
이는 새로운 계정 작업 로그 테이블을 통해 계속 최적화되고 있습니다:
drop table IF EXISTS t_acct_log;
create table t_acct_log(
id INT AUTO_INCREMENT PRIMARY KEY COMMENT ' ,
acct_id int primary key NOT NULL COMMENT '계정 ID',
price DECIMAL(12,2) NOT NULL COMMENT '거래 금액',
status SMALLINT NOT NULL DEFAULT 0 COMMENT ' ,0:처리 보류 중, 1: 처리 성공'
) COMMENT '계정 작업 로그 테이블 ';
그런데 새 필드 old_balance를 추가하여 t_acct 태그를 수정하면 새 구조는 다음과 같습니다:
drop table IF EXISTS t_acct;
create table t_acct(
acct_id int primary key NOT NULL COMMENT '계정 ID',
balance decimal(12,2) NOT NULL COMMENT '계정 잔액',
old_balance decimal(12,2) NOT NULL COMMENT '계정 잔액',
version INT NOT NULL DEFAULT 0 COMMENT '버전 번호, 각 업데이트+1'
)COMMENT '계정 차트';
INSERT INTO t_acct(acct_id, balance,old_balance,version) VALUES (1,10000,10000,0);
새로운 old_balance 필드가 추가되었으며이 필드의 값은 처음의 잔액 값과 동일하며 나중에 작업에서 변경되며 먼저 아래를 볼 수 있으며 나중에 설명합니다.
계정 v_acct_id 거래 금액이 v_price라고 가정하면 프로세스는 다음과 같습니다:
t1.트랜잭션 열기: 트랜잭션 시작;
t2.insert into t_acct_log(acct_id,price,status) values (#v_acct_id#,#v_price#,0)
t3.int count = (update t_acct set balnce = v_balance+#v_price#,version = version+1 where acct_id = #v_acct_id# and v_balance+#v_price#>=0);
t6.if(count==1){
//트랜잭션 제출
commit;
}else{
//트랜잭션 롤백
rollback;
}
위의 물 기록이 없는 것을 확인할 수 있으며, t_acct_log 데이터에 따라 비동기식으로 삽입된 로그 t_acct_log에 t_acct_data 레코드를 생성합니다.
이 이상의 동시 작업을 지원하는 작업은 초당 500스트로크의 테스트 결과 여전히 비교적 높은 수준이며, 매우 효율적입니다.
새 작업을 추가하고 상태가 0인 레코드를 찾기 위해 t_acct_log를 쿼리한 다음 다음과 같이 반복하여 하나씩 처리합니다:
_acct_log현재 L1에 대한 레코드를 처리해야 하는 경우
t1트랜잭션 시작 트랜잭션 열기
t2변수 생성
v_price = L1.price;
v_acct_id = L1.acct_id;
t3R1= (select * from t_acct where acct_id = #v_acct_id#);
t4몇 가지 변수를 생성합니다.
v_old_balance = R1.old_balance;
v_open_balance = v_old_balance;
v_old_balance = R1.old_balance + v_price;
v_open_balance = v_old_balance;
t5:int count = (update t_acct set old_balance = #v_old_balance#,version = version + 1 where acct_id = #v_acct_id# and version = #v_version#);
t6: if(count==1){
// _acct_log작업의 상태가 1로 설정됩니다.
count = (update t_acct_log set status=1 where status=0 and id = #L1.id#);
}
if(count==1){
//트랜잭션 제출
commit;
}else{
//트랜잭션 롤백
rollback;
}
업데이트 조건 추가 버전의 t5 위, 업데이트 조건 추가 버전의 t6은 주로 문제를 수정하기 위한 동시 작업이 잘못되는 것을 방지하기 위해 상태 = 0 작업을 추가했습니다.
위의 t_acct_log에 있는 모든 status=0 레코드가 처리되면 t_acct 테이블의 잔액과 old_balance가 일치하게 됩니다.
위의 접근 방식은 첫 번째 쓰기 계정 작업 로그를 사용한 다음 로그에서 비동기 작업을 사용하여 실행중인 물 생성에서 MySQL의 디자인을 그리면 배우는 법을 배울 수도 있습니다.
사례 2: 기업 간 이전 문제
이 문제는 위에서 설명한 mysql의 2단계 커밋을 사용하여 해결할 수 있습니다.
예를 들어, 라이브러리 A의 T1 테이블에서 라이브러리 B의 T1 테이블로 100을 전송합니다.
C 라이브러리를 만들고 다음과 같이 C 라이브러리에 새 전송 주문 테이블을 추가합니다:
drop table IF EXISTS t_transfer_order;
create table t_transfer_order(
id int primary key NOT NULL COMMENT '계정 ID',
from_acct_id int NOT NULL COMMENT '발신자 계정 이전 ',
to_acct_id int NOT NULL COMMENT '파티 계정으로 전송됨',
price decimal(12,2) NOT NULL COMMENT '이체 금액 ',
addtime int COMMENT '들어오는 시간',
status SMALLINT NOT NULL DEFAULT 0 COMMENT '상태, 0: 보류 중, 1: 전송 성공, 2: 전송 실패',
version INT NOT NULL DEFAULT 0 COMMENT '버전 번호, 각 업데이트+1'
) COMMENT '전송 주문 양식 ';
라이브러리 A 및 B와 테이블 3개, 예를 들어:
drop table IF EXISTS t_acct;
create table t_acct(
acct_id int primary key NOT NULL COMMENT '계정 ID',
balance decimal(12,2) NOT NULL COMMENT '계정 잔액',
version INT NOT NULL DEFAULT 0 COMMENT '버전 번호, 각 업데이트+1'
)COMMENT '계정 차트';
drop table IF EXISTS t_order;
create table t_order(
transfer_order_id int primary key NOT NULL COMMENT '전송 주문 ID',
price decimal(12,2) NOT NULL COMMENT '이체 금액 ',
status SMALLINT NOT NULL DEFAULT 0 COMMENT '상태, 1: 전송 성공, 2: 전송 실패',
version INT NOT NULL DEFAULT 0 COMMENT '버전 번호, 각 업데이트+1'
) COMMENT '전송 주문 양식 ';
drop table IF EXISTS t_transfer_step_log;
create table t_transfer_step_log(
id int primary key NOT NULL COMMENT '계정 ID',
transfer_order_id int NOT NULL COMMENT '전송 주문 ID',
step SMALLINT NOT NULL COMMENT '전송 단계, 0: 정방향 작업, 1: 롤백 작업 ',
UNIQUE KEY (transfer_order_id,step)
) COMMENT '전송 단계 로그 테이블 ';
전송 로그 작업의 단계를 기록하는 데는 t_transfer_step_log 테이블이 사용되며, 각 단계가 한 번만 실행될 수 있음을 나타내는 고유 제약 조건이
transfer_order_id,step추가되어 단계의 무효성을 보장합니다.
여러 변수를 정의합니다:
v_From_ACCT_ID:계정에서 이체됨
V_TO_ACCT_ID:양도 당사자 계정
v_price:거래 금액
전체 이전 절차는 다음과 같습니다:
각 단계에는 반환 값이 있으며 반환 값은 배열 유형이며 의미는 0: 처리 중, 1: 성공, 2: 실패입니다.
step1:전송 주문을 생성하면 주문 상태가 0으로 표시되어 처리가 진행 중임을 나타냅니다.
C1: 트랜잭션 시작;
C2:insert into t_transfer_order(from_acct_id,to_acct_id,price,addtime,status,version)
values(#v_from_acct_id#,#v_to_acct_id#,#v_price#,0,unix_timestamp(now()));
C3방금 성공한 삽입의 주문 ID를 가져와서 변수 v에 넣습니다._transfer_order_id
C4: 커밋;
step2:A라이브러리 작업은 다음과 같습니다.
A1: AR1= (select * from t_order where transfer_order_id = #v_transfer_order_id#);
A2: if(AR1!=null){
return AR1.status==1?1:2;
}
A3: 트랜잭션 시작;
A4AR2= (select 1 from t_acct where acct_id = #v_from_acct_id#);
A5: if(AR2.balance<v_price){
//잔액이 부족하면 전송이 실패했음을 나타내며 전송 실패 주문을 삽입합니다.
insert into t_order (price,status) values (#v_price#,2);
commit;
//실패 상태 반환 2
return 2;
}else{
//通过乐观锁 & balance - #v_price# >= 0동시 작업을 방지하기 위해 계정 자금 업데이트
int count = (update t_acct set balance = balance - #v_price#, version = version + 1 where acct_id = #v_from_acct_id# and balance - #v_price# >= 0 and version = #AR2.version#);
//count1은 위의 업데이트가 성공했음을 의미합니다.
if(count==1){
//상태 1의 전송 성공 주문 삽입
insert into t_order (price,status) values (#v_price#,1);
//단계 로그 삽입
insert into t_transfer_step_log (transfer_order_id,step) values (#v_transfer_order_id#,1);
commit;
return 1;
}else{
//상태 2의 전송 실패 주문 삽입
insert into t_order (price,status) values (#v_price#,2);
commit;
return 2;
}
}
step3:
if(step2 ==1){
//은행 A의 공제가 성공했음을 나타냅니다.
4단계 실행;
}else if(step2 ==2){
//은행 A에서 공제가 실패했음을 나타냅니다.
6단계 실행;
}
step4:라이브러리 B에 대한 작업은 다음과 같습니다:
B1: BR1= (select * from t_order where transfer_order_id = #v_transfer_order_id#);
B2: if(BR1!=null){
return BR1.status==1?1:2;
}else{
B3 실행;
}
B3: 트랜잭션 시작;
B4: BR2= (select 1 from t_acct where acct_id = #v_to_acct_id#);
B5:int count = (update t_acct set balance = balance + #v_price#, version = version + 1 where acct_id = #v_to_acct_id# and version = #BR2.version#);
if(count==1){
//상태 1로 주문 삽입하기
insert into t_order (price,status) values (#v_price#,1);
//로그 삽입
insert into t_transfer_step_log (transfer_order_id,step) values (#v_transfer_order_id#,1);
commit;
return 1;
}else{
//를 여기에 넣으면 동시성이 있음을 나타내며, 0을 반환합니다.
rollback;
return 0;
}
step5:
if(step4 ==1){
//은행 B에 돈을 추가하는 것이 성공했음을 나타냅니다.
7단계 실행;
}
step6:C 라이브러리 작업으로
C1: AR1= (select 1 from t_transfer_order where id = #v_transfer_order_id#);
C2: if(AR1.status==1 || AR1.status=2){
return AR1.status=1?"전송 성공":"전송 실패";
}
C3: 트랜잭션 시작;
C4:int count = (udpate t_transfer_order set status = 2,version = version+1 where id = #v_transfer_order_id# and version = version + #AR1.version#)
C5: if(count==1){
commit;
return "전송 실패";
}else{
rollback;
return " ;
}
step7:C 라이브러리 작업으로
C1: AR1= (select 1 from t_transfer_order where id = #v_transfer_order_id#);
C2: if(AR1.status==1 || AR1.status=2){
return AR1.status=1?"전송 성공":"전송 실패";
}
C3: 트랜잭션 시작;
C4:int count = (udpate t_transfer_order set status = 1,version = version+1 where id = #v_transfer_order_id# and version = version + #AR1.version#)
C5: if(count==1){
commit;
return "전송 성공";
}else{
rollback;
return " ;
}
새 보상 작업도 추가해야 하며, 그 절차는 다음과 같습니다:
while(true){
List list = select * from t_transfer_order where status = 0 and addtime+10*60<unix_timestamp(now());
if(list {
//보간 레코드 없음, 루프 종료
break;
}
//처리할 목록을 반복합니다.
for(Object r:list){
//처리를 위해 위의 step2를 호출하면 최종 주문 상태가 1 또는 2로 변경됩니다.
}
}
참고로 이 작업의 처리에 문제가 있다면 데드 루프일 수 있으며, 이 문제를 해결하는 방법에 대해 생각해 보세요.





