Clickhouse分享: 读已提交
前情回顾
总结之前的原子写入文章
, 其实原子写入解决
的是数据脏写
的问题, 但是存在数据脏读
的问题, 即当写入失败回退时, 部分成功的节点的数据, 已经能够被访问了.
那么从数据库经典的事务级别来说, 实现原子写入就实现了ACID的原子性
, 但由于脏读
状态的存在, 目前的事务级是读未提交
.
读未提交
实际上在OLAP应用中, 对大多数业务的影响并没有那么大, 相对于总数为亿行级别的数据, 单次写入个数在10万以内, 因此数据的误差只有万分之一到千分之一, 在大多数业务系统中并不会有那么大的影响.
但对于某些数据量比较小的用户, 读未提交
还是会对他们产生比较大的疑问, 因此实现读已提交
的隔离级别, 虽非必要, 但是最好如此.
OLAP系统究竟需要怎么样的隔离级别, 这是一个非常好的问题. 隔离级别需要应用层来确定, 但从目前观察到的业务状态,
重复读
其实在OLAP中没有必要, 因为OLAP系统中, 用户的SQL很多有依赖关系, 不会出现SQL2的某些数值, 需要从SQL1的结果中获取的场景, 因此可重复读
基本上没有实现的意义.
实现读已提交
在这个架构中需要引入全局的Zookeeper作为协调者. 在ZK上会维护一个processing_insert
的文件夹, 其中每个文件表示正在写入的批次信息, 大多数情况下该目录下只有一个文件, 因为一般只有一个写入的Flink的任务在工作.
和原子写入
一样, 一次Flink的Snapshot批次开始的时候, 会产生一个Label, 并写入到ZK中, 之后所有的写入都复用该Label, 每次攒批写入也都有自己的Label, 每个写入的DP都有一个insert_id
和一个batch_id
和原子写入
不一样的是, 如果在snapshot过程读取数据的话, 读引擎会去ZK中拿到processing_insert
路径, 并获取到其中的batch_id
, 读取数据的时候, 会自动过滤该id, 也就是这个snapshot批次写入的数据, 读不可见.
这里写入local表的时候, 不需要设计2个状态的DP, 查询的可见性由计算层来维护了
出现回滚的时候, 会将历史所有的数据删除, 这里不需要删除ZK上的记录, 因为下一次Flink重启的时候batch_id
依然是上次的, 数据写入将会重试, 然后就没必要删除数据了.
删除数据必须要设置为幂等的, 如果删除数据一致无法成功, 此时只能保障让人工来处理.
对于重试的问题, 和原子写入
一样, batch_id
相同并且insert_id
不同, 因此在prepare
的时候需要执行commit
local table的操作.
为什么使用Label而不是使用时间戳? 一, 分布式时间戳生成比较麻烦; 二, Flink写入一半只有一个, 只需要判断两种状态, 不需要判断前后时序关系
Flink集成
跟原子写入
一样通过两阶段写入的方式处理, 但过程有点不一致:
- prepare阶段: 执行local的表的commit操作, 写入DP, 并删除无用的DP, 此时local查询时, 该DP已经可见
- commit阶段: 只删除ZK上的路径, 查询时会自动查询底层的数据.