1、Hive事务核心流程
1.1、时序图
1.2、核心流程图
2、Hive事务HDFS文件存储剖析
2.1、表结构
前提是设置 set hive.txn.manager=org.apache.hadoop.hive.ql.lockmgr.DbTxnManager;
使用DbTxnManager Db事务管理器
CREATE TABLE employee (id int, name string, salary int)
STORED AS ORC TBLPROPERTIES ('transactional' = 'true');
2.2、插入数据解析
INSERT INTO employee VALUES
(1, 'Jerry', 5000),
(2, 'Tom', 8000),
(3, 'Kate', 6000);
INSERT语句会在一个事务中运行。它会创建名为delta的目录,存放事务的信息和表的数据
/user/hive/warehouse/employee/delta_0000001_0000001_0000
/user/hive/warehouse/employee/delta_0000001_0000001_0000/_orc_acid_version
/user/hive/warehouse/employee/delta_0000001_0000001_0000/bucket_00000
目录名称的格式为delta_minWID_maxWID_stmtID,即 delta前缀、写事务的 ID 范围、以及语句ID。具体来说:
- 所有INSERT语句都会创建delta目录。UPDATE语句也会创建delta目录,但会先创建一个delete目录,即先删除、后插入。delete 目录的前缀是 delete_delta
- Hive会为所有的事务生成一个全局唯一的ID,包括读操作和写操作。针对写事务(INSERT、DELETE等),Hive还会创建一个写事务ID(Write ID),该ID在表范围内唯一。写事务ID会编码到delta和delete目录的名称中
- 语句ID(Statement ID)则是当一个事务中有多条写入语句时使用的,用作唯一标识。(MERGE语句可能会生成多个Statement ID)
再看文件内容,_orc_acid_version 的内容是 2,即当前 ACID 版本号是 2。它和版本 1 的主要区别是 UPDATE 语句采用了 split-update 特性,即上文提到的先删除、后插入。这个特性能够使 ACID 表支持条件下推等功能,具体可以查看 HIVE-14035。bucket_00000 文件则是写入的数据内容。由于这张表没有分区和分桶,所以只有这一个文件。事务表都以 ORC 格式存储的,我们可以使用 hive自带工具 来查看文件的内容:
hive --orcfiledump -d /user/hive/warehouse/employee/delta_0000001_0000001_0000
{"operation":0,"originalTransaction":1,"bucket":536870912,"rowId":0,"currentTransaction":1,"row":{"id":1,"name":"Jerry","salary":5000}}
{"operation":0,"originalTransaction":1,"bucket":536870912,"rowId":1,"currentTransaction":1,"row":{"id":2,"name":"Tom","salary":8000}}
{"operation":0,"originalTransaction":1,"bucket":536870912,"rowId":2,"currentTransaction":1,"row":{"id":3,"name":"Kate","salary":6000}}
输出内容被格式化为了一行行的 JSON 字符串,我们可以看到具体数据是在 row 这个键中的,其它键则是 Hive 用来实现事务特性所使用的,具体含义为:
- operation : 0 表示插入,1 表示更新,2 表示删除。由于使用了 split-update,UPDATE 是不会出现的
- originalTransaction : 是该条记录的原始写事务 ID。对于 INSERT 操作,该值和 currentTransaction 是一致的。对于 DELETE,则是该条记录第一次插入时的写事务 ID
bucket : 是一个 32 位整型,由 BucketCodec 编码,各个二进制位的含义为:
- 1-3 位:编码版本,当前是 001
- 4 位:保留
- 5-16 位:分桶 ID,由 0 开始。分桶 ID 是由 CLUSTERED BY 子句所指定的字段、以及分桶的数量决定的。该值和 bucket_N 中的 N 一致
- 17-20 位:保留
- 21-32 位:语句 ID
- 举例来说,整型 536936448 的二进制格式为 00100000000000010000000000000000,即它是按版本 1 的格式编码的,分桶 ID 为 1
- rowId : 是一个自增的唯一 ID,在写事务和分桶的组合中唯一
- currentTransaction : 当前的写事务 ID
- row : 具体数据。对于 DELETE 语句,则为 null
我们可以注意到,文件中的数据会按 (originalTransaction,bucket,rowId) 进行排序,这点对后面的读取操作非常关键,这些信息还可以通过 row__id 这个虚拟列进行查看:
SELECT row__id, id, name, salary FROM employee;
{"writeid":1,"bucketid":536870912,"rowid":0} 1 Jerry 5000
{"writeid":1,"bucketid":536870912,"rowid":1} 2 Tom 8000
{"writeid":1,"bucketid":536870912,"rowid":2} 3 Kate 6000
2.3、更新数据
UPDATE employee SET salary = 7000 WHERE id = 2;
这条语句会先查询出所有符合条件的记录,获取它们的 row__id 信息,然后分别创建delete和 delta目录:
/user/hive/warehouse/employee/delta_0000001_0000001_0000/bucket_00000
/user/hive/warehouse/employee/delete_delta_0000002_0000002_0000/bucket_00000
/user/hive/warehouse/employee/delta_0000002_0000002_0000/bucket_00000
delete_delta_0000002_0000002_0000/bucket_00000包含了删除的记录 :
原始 originalTransaction 为1,currentTransaction 为2(简单的说,就是当前事务比之前的事务大,其实说白了就是之后对之前的数据进行一些变更操作,否则就是原始事务和当前事务是相同的)
{"operation":2,"originalTransaction":1,"bucket":536870912,"rowId":1,"currentTransaction":2,"row":null}
delta_0000002_0000002_0000/bucket_00000 包含更新后的数据:
{"operation":0,"originalTransaction":2,"bucket":536870912,"rowId":0,"currentTransaction":2,"row":{"id":2,"name":"Tom","salary":7000}}
DELETE 语句的工作方式类似,同样是先查询,后生成delete目录
2.4、合并表
MERGE语句和MySQL的INSERT ON UPDATE 功能类似,它可以将来源表的数据合并到目标表中
CREATE TABLE employee_update (id int, name string, salary int);
INSERT INTO employee_update VALUES
(2, 'Tom', 7000),
(4, 'Mary', 9000);
MERGE INTO employee AS a
USING employee_update AS b ON a.id = b.id
WHEN MATCHED THEN UPDATE SET salary = b.salary
WHEN NOT MATCHED THEN INSERT VALUES (b.id, b.name, b.salary);
其实就是说,根据表a和表b的id对比,如果有相同的,将b表的薪资赋值给a表的薪资,如果不相同,对a表进行数据插入
这条语句会更新 Tom 的薪资字段,并插入一条 Mary 的新记录。多条WHEN子句会被视为不同的语句,有各自的语句 ID(Statement ID)
INSERT子句会创建delta_0000002_0000002_0000文件,内容是Mary的数据
UPDATE语句则会创建delete_delta_0000002_0000002_0001和 delta_0000002_0000002_0001两个文件,删除并新增 Tom 的数据
按道理来说应该应该其实是 :
delta_0000002_0000002_0000 和 delete_delta_0000003_0000003_0000、delta_0000003_0000003_0000
但是由于Tom薪资的更新和Mary的新记录在同一条SQL中,属于同一个事务,所以就如下:
/user/hive/warehouse/employee/delta_0000002_0000002_0000
/user/hive/warehouse/employee/delete_delta_0000002_0000002_0001
/user/hive/warehouse/employee/delta_0000002_0000002_0001
2.5、压缩
随着写操作的积累,表中的delta和delete文件会越来越多。事务表的读取过程中需要合并所有文件,数量一多势必会影响效率。此外,小文件对 HDFS 这样的文件系统也是不够友好的。因此,Hive引入了压缩(Compaction)的概念,分为Minor和Major两类
Minor Compaction会将所有的delta文件压缩为一个文件,delete也压缩为一个。压缩后的结果文件名中会包含写事务ID范围,同时省略掉语句 ID。压缩过程是在Hive Metastore中运行的,会根据一定阈值自动触发。
比如说 :
delta 目录超过一定的阈值,就会触发minor压缩
hive.compactor.delta.num.threshold
delta文件数量和base数量的百分比会触发,major压缩
hive.compactor.delta.pct.threshold
具体其他参数参考 : https://cwiki.apache.org/confluence/display/hive/hive+transac...
我们也可以使用如下语句人工触发:ALTER TABLE employee COMPACT 'minor';
以上文中的 MERGE 语句的结果举例,在运行了一次Minor Compaction后,文件目录结构将变为:
/user/hive/warehouse/employee/delete_delta_0000001_0000002
/user/hive/warehouse/employee/delta_0000001_0000002
在delta_0000001_0000002/bucket_00000文件中,数据会被排序和合并起来,因此文件中将包含两行 Tom 的数据。Minor Compaction 不会删除任何数据
Major Compaction则会将所有文件合并为一个文件,以 base_N的形式命名,其中N表示最新的写事务ID。已删除的数据将在这个过程中被剔除。row__id 则按原样保留
/user/hive/warehouse/employee/base_0000002
需要注意的是,在 Minor 或 Major Compaction 执行之后,原来的文件不会被立刻删除。这是因为删除的动作是在另一个名为 Cleaner 的线程中执行的。因此,表中可能同时存在不同事务 ID 的文件组合,这在读取过程中需要做特殊处理
2.6、读取过程
我们可以看到 ACID 事务表中会包含三类文件,分别是 base、delta、以及 delete。文件中的每一行数据都会以 row__id 作为标识并排序。从 ACID 事务表中读取数据就是对这些文件进行合并,从而得到最新事务的结果。这一过程是在 OrcInputFormat 和 OrcRawRecordMerger 类中实现的,本质上是一个合并排序的算法。
以下列文件为例,产生这些文件的操作为:插入三条记录,进行一次 Major Compaction,然后更新两条记录。1-0-0-1 是对 originalTransaction - bucketId - rowId - currentTransaction 的缩写
+----------+ +----------+ +----------+
| base_1 | | delete_2 | | delta_2 |
+----------+ +----------+ +----------+
| 1-0-0-1 | | 1-0-1-2 | | 2-0-1-2 |
| 1-0-1-1 | | 1-0-2-2 | | 2-0-2-2 |
| 1-0-2-1 | +----------+ +----------+
+----------+
合并过程为:
对所有数据行按照 (originalTransaction, bucketId, rowId) 正序排列,(currentTransaction) 倒序排列,即:
- 1-0-0-1
- 1-0-1-2
- 1-0-1-1
- 1-0-2-2
- 1-0-2-1
- 2-0-1-2
- 2-0-2-2
- 获取第一条记录;
- 如果当前记录的 row__id 和上条数据一样,则跳过;
如果当前记录的操作类型为 DELETE,也跳过;
- 通过以上两条规则,对于 1-0-1-2 和 1-0-1-1,这条记录会被跳过;
- 如果没有跳过,记录将被输出给下游;
- 重复以上过程
最终结果 : - 1-0-0-1
- 2-0-1-2
- 2-0-2-2
如感兴趣,点赞加关注,谢谢!!!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。