트랜잭션
트랜잭션은 하나 이상의 SQL 문을 포함하는 작업입니다. 이 문들은 모두 성공적으로 실행되거나 모두 실패하며, 나눌 수 없는 하나의 작업 단위를 이룹니다.
개요
단일 쿼리 또는 DDL 문은 암시적 트랜잭션입니다. 쿼리나 DDL을 포함하는 다중 문 트랜잭션은 지원하지 않습니다. 기본적으로 각 쓰기 작업은 암시적 트랜잭션이며, 여러 쓰기 작업을 하나의 명시적 트랜잭션으로 묶을 수 있습니다. Doris는 현재 중첩 트랜잭션을 지원하지 않습니다.
명시적 트랜잭션
명시적 트랜잭션은 사용자가 트랜잭션의 시작, 커밋 또는 롤백을 명시적으로 수행해야 합니다. 현재 명시적 트랜잭션 내에서는 DDL 문과 쿼리 문을 지원하지 않습니다.
BEGIN;
[INSERT, UPDATE, DELETE statement]
COMMIT; / ROLLBACK;
암시적 트랜잭션
암시적 트랜잭션은 사용자가 실행할 SQL 문 앞뒤에 트랜잭션을 시작하고 커밋하는 문을 명시적으로 추가하지 않는 트랜잭션입니다.
Doris에서는 Group Commit을 제외한 모든 데이터 적재 문이 실행을 시작할 때 트랜잭션을 엽니다. 문이 성공적으로 완료되면 트랜잭션이 자동으로 커밋되고, 실패하면 자동으로 롤백됩니다. 각 쿼리나 DDL도 암시적 트랜잭션입니다.
격리 수준
Doris가 현재 지원하는 유일한 격리 수준은 READ COMMITTED입니다. READ COMMITTED 격리 수준에서는 문이 실행을 시작하기 전에 커밋된 데이터만 볼 수 있으며, 커밋되지 않은 데이터는 볼 수 없습니다.
단일 문이 실행되면 해당 문이 시작되는 시점에 접근하는 테이블의 스냅샷을 생성합니다. 즉, 단일 문은 실행을 시작하기 전에 완료된 다른 트랜잭션의 커밋만 볼 수 있습니다. 문이 실행되는 동안 다른 트랜잭션이 수행한 커밋은 보이지 않습니다.
다중 문 트랜잭션 내에서 문이 실행되는 경우:
- 해당 문이 실행을 시작하기 전에 커밋된 데이터만 볼 수 있습니다. 첫 번째 문과 두 번째 문 사이에 다른 트랜잭션이 커밋되면, 같은 트랜잭션에서 연속으로 실행되는 두 문이 서로 다른 데이터를 볼 수 있습니다.
- 현재는 같은 트랜잭션에서 앞서 실행된 문이 변경한 내용을 볼 수 없습니다.
손실 및 중복 방지
Doris는 손실과 중복 없는 쓰기를 지원하기 위해 두 가지 메커니즘을 제공합니다. Label 메커니즘은 단일 트랜잭션의 중복을 방지하고, 두 단계 커밋은 여러 트랜잭션 간에 중복 방지를 조율할 수 있도록 합니다.
Label 메커니즘
Doris 트랜잭션이나 데이터 적재에는 Label을 설정할 수 있습니다. Label은 일반적으로 비즈니스 로직상 의미가 있는 사용자 정의 문자열이며, 설정하지 않으면 내부적으로 UUID 문자열이 생성됩니다. Label의 주된 목적은 트랜잭션이나 데이터 적재 작업을 고유하게 식별하고, 같은 Label을 가진 트랜잭션이나 데이터 적재가 한 번만 성공적으로 실행되도록 보장하는 것입니다. Label 메커니즘은 적재된 데이터에 손실이나 중복이 없도록 보장합니다. 업스트림 데이터 소스가 At-Least-Once 의미론을 보장할 수 있다면, Doris Label 메커니즘과 함께 Exactly-Once 의미론을 구현할 수 있습니다. Label은 데이터베이스 내에서 고유합니다.
Doris는 시간과 개수를 기준으로 Label을 정리합니다. 기본적으로 Label 수가 2000개를 초과하면 제거가 시작되며, 3일이 지난 Label도 기본적으로 제거됩니다. Label이 제거되면 같은 이름의 Label로 다시 성공적으로 실행할 수 있습니다. 즉, 중복 제거 의미론이 더 이상 적용되지 않습니다.
Label은 일반적으로 business logic + time 형식으로 설정하며, 예를 들면 my_business1_20220330_125000과 같습니다. 이 Label은 보통 비즈니스 my_business1에서 2022-03-30 12:50:00에 생성한 데이터 배치를 나타냅니다. 이러한 Label 체계를 사용하면 비즈니스 측에서 Label로 데이터 적재 작업 상태를 조회하여 해당 배치와 시간의 데이터가 이미 성공적으로 적재되었는지 명확하게 확인할 수 있습니다. 아직 적재되지 않았다면 같은 Label로 데이터 적재를 재시도할 수 있습니다.
StreamLoad 2PC
StreamLoad 2PC는 주로 Flink가 Doris에 데이터를 쓸 때 EOS 의미론을 지원하는 데 사용됩니다.
명시적 트랜잭션 작업
트랜잭션 시작
BEGIN;
BEGIN WITH LABEL {user_label};
현재 Session에서 이미 트랜잭션이 진행 중인 상태로 이 문을 실행하면 Doris는 이 문을 무시합니다. 즉, 트랜잭션은 중첩할 수 없습니다.
트랜잭션 커밋
COMMIT;
현재 트랜잭션에서 수행한 모든 변경 사항을 커밋합니다.
트랜잭션 롤백
ROLLBACK;
현재 트랜잭션의 모든 변경 사항을 취소합니다.
트랜잭션은 Session 수준에서 동작합니다. Session이 중단되거나 닫히면 트랜잭션도 자동으로 롤백됩니다.
여러 SQL 문으로 쓰기
Doris는 현재 트랜잭션 내에서 두 가지 쓰기 방식을 지원합니다.
단일 테이블에 여러 INSERT INTO VALUES 쓰기
테이블 구조가 다음과 같다고 가정합니다.
CREATE TABLE `dt` (
`id` INT(11) NOT NULL,
`name` VARCHAR(50) NULL,
`score` INT(11) NULL
) ENGINE=OLAP
UNIQUE KEY(`id`)
DISTRIBUTED BY HASH(`id`) BUCKETS 1
PROPERTIES (
"replication_num" = "1"
);
데이터를 씁니다.
mysql> BEGIN;
Query OK, 0 rows affected (0.01 sec)
{'label':'txn_insert_b55db21aad7451b-b5b6c339704920c5', 'status':'PREPARE', 'txnId':''}
mysql> INSERT INTO dt (id, name, score) VALUES (1, "Emily", 25), (2, "Benjamin", 35), (3, "Olivia", 28), (4, "Alexander", 60), (5, "Ava", 17);
Query OK, 5 rows affected (0.08 sec)
{'label':'txn_insert_b55db21aad7451b-b5b6c339704920c5', 'status':'PREPARE', 'txnId':'10013'}
mysql> INSERT INTO dt VALUES (6, "William", 69), (7, "Sophia", 32), (8, "James", 64), (9, "Emma", 37), (10, "Liam", 64);
Query OK, 5 rows affected (0.00 sec)
{'label':'txn_insert_b55db21aad7451b-b5b6c339704920c5', 'status':'PREPARE', 'txnId':'10013'}
mysql> COMMIT;
Query OK, 0 rows affected (1.02 sec)
{'label':'txn_insert_b55db21aad7451b-b5b6c339704920c5', 'status':'VISIBLE', 'txnId':'10013'}
이 쓰기 방식은 쓰기의 원자성을 보장할 뿐 아니라 Doris에서 INSERT INTO VALUES의 쓰기 성능도 향상시킵니다.
사용자가 Group Commit과 트랜잭션 쓰기를 모두 활성화하면 트랜잭션 쓰기가 적용됩니다.
여러 테이블에 걸쳐 여러 INSERT INTO SELECT, UPDATE 및 DELETE 쓰기
위와 같은 스키마를 가진 세 테이블 dt1, dt2, dt3이 있다고 가정합니다. 각 테이블의 데이터는 다음과 같습니다.
mysql> SELECT * FROM dt1;
+------+-----------+-------+
| id | name | score |
+------+-----------+-------+
| 1 | Emily | 25 |
| 2 | Benjamin | 35 |
| 3 | Olivia | 28 |
| 4 | Alexander | 60 |
| 5 | Ava | 17 |
+------+-----------+-------+
5 rows in set (0.04 sec)
mysql> SELECT * FROM dt2;
+------+---------+-------+
| id | name | score |
+------+---------+-------+
| 6 | William | 69 |
| 7 | Sophia | 32 |
| 8 | James | 64 |
| 9 | Emma | 37 |
| 10 | Liam | 64 |
+------+---------+-------+
5 rows in set (0.03 sec)
mysql> SELECT * FROM dt3;
Empty set (0.03 sec)
dt1과 dt2의 데이터를 dt3에 적재하면서 테이블 dt1의 점수를 업데이트하고 테이블 dt2의 데이터를 삭제하는 트랜잭션 쓰기를 수행합니다.
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
{'label':'txn_insert_442a6311f6c541ae-b57d7f00fa5db028', 'status':'PREPARE', 'txnId':''}
# The status of the load task is PREPARE
mysql> INSERT INTO dt3 SELECT * FROM dt1;
Query OK, 5 rows affected (0.07 sec)
{'label':'txn_insert_442a6311f6c541ae-b57d7f00fa5db028', 'status':'PREPARE', 'txnId':'11024'}
mysql> INSERT INTO dt3 SELECT * FROM dt2;
Query OK, 5 rows affected (0.08 sec)
{'label':'txn_insert_442a6311f6c541ae-b57d7f00fa5db028', 'status':'PREPARE', 'txnId':'11025'}
mysql> UPDATE dt1 SET score = score + 10 WHERE id >= 4;
Query OK, 2 rows affected (0.07 sec)
{'label':'txn_insert_442a6311f6c541ae-b57d7f00fa5db028', 'status':'PREPARE', 'txnId':'11026'}
mysql> DELETE FROM dt2 WHERE id >= 9;
Query OK, 0 rows affected (0.01 sec)
{'label':'txn_insert_442a6311f6c541ae-b57d7f00fa5db028', 'status':'PREPARE', 'txnId':'11027'}
mysql> COMMIT;
Query OK, 0 rows affected (0.03 sec)
{'label':'txn_insert_442a6311f6c541ae-b57d7f00fa5db028', 'status':'VISIBLE', 'txnId':'11024'}
데이터를 쿼리합니다.
# Scores for rows with id >= 4 are increased by 10
mysql> SELECT * FROM dt1;
+------+-----------+-------+
| id | name | score |
+------+-----------+-------+
| 1 | Emily | 25 |
| 2 | Benjamin | 35 |
| 3 | Olivia | 28 |
| 4 | Alexander | 70 |
| 5 | Ava | 27 |
+------+-----------+-------+
5 rows in set (0.01 sec)
# Rows with id >= 9 are deleted
mysql> SELECT * FROM dt2;
+------+---------+-------+
| id | name | score |
+------+---------+-------+
| 6 | William | 69 |
| 7 | Sophia | 32 |
| 8 | James | 64 |
+------+---------+-------+
3 rows in set (0.02 sec)
# The committed data from dt1 and dt2 is written into dt3
mysql> SELECT * FROM dt3;
+------+-----------+-------+
| id | name | score |
+------+-----------+-------+
| 1 | Emily | 25 |
| 2 | Benjamin | 35 |
| 3 | Olivia | 28 |
| 4 | Alexander | 60 |
| 5 | Ava | 17 |
| 6 | William | 69 |
| 7 | Sophia | 32 |
| 8 | James | 64 |
| 9 | Emma | 37 |
| 10 | Liam | 64 |
+------+-----------+-------+
10 rows in set (0.01 sec)
격리 수준
Doris 트랜잭션 쓰기가 제공하는 격리 수준은 READ COMMITTED입니다. 다음 두 가지 사항에 유의하세요.
-
트랜잭션에 여러 문이 있는 경우, 각 문은 실행을 시작하는 시점에 커밋되어 있는 데이터를 읽습니다. 예를 들면 다음과 같습니다.
timestamp | ------------ Session 1 ------------ | ------------ Session 2 ------------
t1 | BEGIN; |
t2 | # read n rows from dt1 table |
| INSERT INTO dt3 SELECT * FROM dt1; |
t3 | | # write 2 rows to dt1 table
| | INSERT INTO dt1 VALUES(...), (...);
t4 | # read n + 2 rows from dt1 table |
| INSERT INTO dt3 SELECT * FROM dt1; |
t5 | COMMIT; | -
트랜잭션에 여러 문이 있는 경우, 각 문은 같은 트랜잭션 내의 다른 문이 변경한 내용을 읽을 수 없습니다. 예를 들면 다음과 같습니다.
트랜잭션을 시작하기 전에 테이블
dt1에 5개 행이 있고, 테이블dt2에 5개 행이 있으며, 테이블dt3은 비어 있다고 가정합니다. 다음 문을 실행합니다.BEGIN;
# 5 rows are written into dt2; after the transaction commits, dt2 has 10 rows in total
INSERT INTO dt2 SELECT * FROM dt1;
# 5 rows are written into dt3; the rows newly written to dt2 in the previous step cannot be read here
INSERT INTO dt3 SELECT * FROM dt2;
COMMIT;구체적인 예는 다음과 같습니다.
# Create tables and write data
CREATE TABLE `dt1` (
`id` INT(11) NOT NULL,
`name` VARCHAR(50) NULL,
`score` INT(11) NULL
) ENGINE=OLAP
DUPLICATE KEY(`id`)
DISTRIBUTED BY HASH(`id`) BUCKETS 1
PROPERTIES (
"replication_num" = "1"
);
CREATE TABLE dt2 LIKE dt1;
CREATE TABLE dt3 LIKE dt1;
INSERT INTO dt1 VALUES (1, "Emily", 25), (2, "Benjamin", 35), (3, "Olivia", 28), (4, "Alexander", 60), (5, "Ava", 17);
INSERT INTO dt2 VALUES (6, "William", 69), (7, "Sophia", 32), (8, "James", 64), (9, "Emma", 37), (10, "Liam", 64);
# Transactional writes
BEGIN;
INSERT INTO dt2 SELECT * FROM dt1;
INSERT INTO dt3 SELECT * FROM dt2;
COMMIT;
# Query
mysql> SELECT * FROM dt2;
+------+-----------+-------+
| id | name | score |
+------+-----------+-------+
| 6 | William | 69 |
| 7 | Sophia | 32 |
| 8 | James | 64 |
| 9 | Emma | 37 |
| 10 | Liam | 64 |
| 1 | Emily | 25 |
| 2 | Benjamin | 35 |
| 3 | Olivia | 28 |
| 4 | Alexander | 60 |
| 5 | Ava | 17 |
+------+-----------+-------+
10 rows in set (0.01 sec)
mysql> SELECT * FROM dt3;
+------+---------+-------+
| id | name | score |
+------+---------+-------+
| 6 | William | 69 |
| 7 | Sophia | 32 |
| 8 | James | 64 |
| 9 | Emma | 37 |
| 10 | Liam | 64 |
+------+---------+-------+
5 rows in set (0.01 sec)
트랜잭션 내 문 실행 실패
트랜잭션 내에서 문이 실패하면 해당 작업은 이미 자동으로 롤백된 상태입니다. 하지만 트랜잭션에서 성공적으로 실행된 다른 문은 여전히 커밋하거나 롤백할 수 있습니다. 트랜잭션이 성공적으로 커밋되면 성공적으로 실행된 문들의 변경 사항이 적용됩니다.
예를 들면 다음과 같습니다.
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
{'label':'txn_insert_c5940d31bf364f57-a48b628886415442', 'status':'PREPARE', 'txnId':''}
mysql> INSERT INTO dt3 SELECT * FROM dt1;
Query OK, 5 rows affected (0.07 sec)
{'label':'txn_insert_c5940d31bf364f57-a48b628886415442', 'status':'PREPARE', 'txnId':'11058'}
# A failed write is automatically rolled back
mysql> INSERT INTO dt3 SELECT * FROM dt2;
ERROR 5025 (HY000): Insert has filtered data in strict mode, tracking_url=http://172.21.16.12:9082/api/_load_error_log?file=__shard_3/error_log_insert_stmt_3d1fed266ce443f2-b54d2609c2ea6b11_3d1fed266ce443f2_b54d2609c2ea6b11
mysql> INSERT INTO dt3 SELECT * FROM dt2 WHERE id = 7;
Query OK, 1 row affected (0.07 sec)
mysql> COMMIT;
Query OK, 0 rows affected (0.02 sec)
{'label':'txn_insert_c5940d31bf364f57-a48b628886415442', 'status':'VISIBLE', 'txnId':'11058'}
쿼리합니다.
# The data from dt1 is written into dt3, the row with id = 7 from dt2 is written successfully, and the other writes failed
mysql> SELECT * FROM dt3;
+------+----------+-------+
| id | name | score |
+------+----------+-------+
| 1 | Emily | 25 |
| 2 | Benjamin | 35 |
| 3 | Olivia | 28 |
| 4 | Alexander | 60 |
| 5 | Ava | 17 |
| 7 | Sophia | 32 |
+------+----------+-------+
6 rows in set (0.01 sec)
자주 발생하는 문제
-
쓰기 대상인 여러 테이블은 같은 데이터베이스에 속해야 합니다. 그렇지 않으면
Transaction insert must be in the same database오류가 발생합니다. -
두 가지 트랜잭션 쓰기 모드인
INSERT INTO SELECT/UPDATE/DELETE와INSERT INTO VALUES는 함께 사용할 수 없습니다. 함께 사용하면Transaction insert can not insert into values and insert into select at the same time오류가 발생합니다. -
삭제 작업은 조건식을 사용하는 방식과 Using 절을 사용하는 방식, 두 가지 삭제 방식을 제공합니다. 격리 수준을 유지하려면 트랜잭션 내에서 같은 테이블에 대한 삭제를 쓰기보다 먼저 수행해야 합니다. 그렇지 않으면
Can not delete because there is a insert operation for the same table오류가 발생합니다. -
BEGIN으로 시작한 데이터 적재가 Doris에 설정된 제한 시간을 초과하면 트랜잭션이 롤백되고 데이터 적재가 실패합니다. 현재 제한 시간은 Session 변수insert_timeout과query_timeout중 더 큰 값을 사용합니다. -
JDBC로 Doris에 연결하여 트랜잭션 작업을 수행할 때는 JDBC URL에
useLocalSessionState=true를 추가하세요. 그렇지 않으면This is in a transaction, only insert, update, delete, commit, rollback is acceptable.오류가 발생할 수 있습니다.
Stream Load 2PC
1. HTTP 헤더에 two_phase_commit:true를 설정하여 두 단계 커밋을 활성화합니다.
curl --location-trusted -u user:passwd -H "two_phase_commit:true" -T test.txt http://fe_host:http_port/api/{db}/{table}/_stream_load
{
"TxnId": 18036,
"Label": "55c8ffc9-1c40-4d51-b75e-f2265b3602ef",
"TwoPhaseCommit": "true",
"Status": "Success",
"Message": "OK",
"NumberTotalRows": 100,
"NumberLoadedRows": 100,
"NumberFilteredRows": 0,
"NumberUnselectedRows": 0,
"LoadBytes": 1031,
"LoadTimeMs": 77,
"BeginTxnTimeMs": 1,
"StreamLoadPutTimeMs": 1,
"ReadDataTimeMs": 0,
"WriteDataTimeMs": 58,
"CommitAndPublishTimeMs": 0
}
2. 트랜잭션의 커밋 작업을 실행합니다(요청은 FE 또는 BE 중 어느 쪽으로든 보낼 수 있습니다).
-
트랜잭션 ID로 트랜잭션을 지정할 수 있습니다.
curl -X PUT --location-trusted -u user:passwd -H "txn_id:18036" -H "txn_operation:commit" http://fe_host:http_port/api/{db}/{table}/_stream_load_2pc
{
"status": "Success",
"msg": "transaction [18036] commit successfully."
} -
Label로 트랜잭션을 지정할 수도 있습니다.
curl -X PUT --location-trusted -u user:passwd -H "label:55c8ffc9-1c40-4d51-b75e-f2265b3602ef" -H "txn_operation:commit" http://fe_host:http_port/api/{db}/{table}/_stream_load_2pc
{
"status": "Success",
"msg": "label [55c8ffc9-1c40-4d51-b75e-f2265b3602ef] commit successfully."
}
3. 트랜잭션의 중단 작업을 실행합니다(요청은 FE 또는 BE 중 어느 쪽으로든 보낼 수 있습니다).
-
트랜잭션 ID로 트랜잭션을 지정할 수 있습니다.
curl -X PUT --location-trusted -u user:passwd -H "txn_id:18037" -H "txn_operation:abort" http://fe_host:http_port/api/{db}/{table}/_stream_load_2pc
{
"status": "Success",
"msg": "transaction [18037] abort successfully."
} -
Label로 트랜잭션을 지정할 수도 있습니다.
curl -X PUT --location-trusted -u user:passwd -H "label:55c8ffc9-1c40-4d51-b75e-f2265b3602ef" -H "txn_operation:abort" http://fe_host:http_port/api/{db}/{table}/_stream_load_2pc
{
"status": "Success",
"msg": "label [55c8ffc9-1c40-4d51-b75e-f2265b3602ef] abort successfully."
}
Broker Load 다중 테이블 트랜잭션
모든 Broker Load 데이터 적재 작업은 원자적으로 적용됩니다. 같은 데이터 적재 작업에서 여러 테이블에 적재하는 경우에도 원자성이 보장됩니다. Label 메커니즘을 사용하면 손실과 중복 없는 데이터 적재도 보장할 수 있습니다.
다음 예에서는 HDFS에서 데이터를 적재하며, 와일드카드로 두 파일 배치를 찾아 두 테이블에 적재합니다.
LOAD LABEL example_db.label2
(
DATA INFILE("hdfs://hdfs_host:hdfs_port/input/file-10*")
INTO TABLE `my_table1`
PARTITION (p1)
COLUMNS TERMINATED BY ","
(k1, tmp_k2, tmp_k3)
SET (
k2 = tmp_k2 + 1,
k3 = tmp_k3 + 1
)
DATA INFILE("hdfs://hdfs_host:hdfs_port/input/file-20*")
INTO TABLE `my_table2`
COLUMNS TERMINATED BY ","
(k1, k2, k3)
)
WITH BROKER hdfs
(
"username"="hdfs_user",
"password"="hdfs_password"
);
와일드카드로 두 파일 배치인 file-10*와 file-20*를 찾아 각각 두 테이블 my_table1과 my_table2에 적재합니다. 이 중 my_table1에는 파티션 p1에 데이터를 적재하며, 원본 파일의 두 번째 열과 세 번째 열의 값에 각각 1을 더한 후 적재합니다.