Clickhouse分享: 读已提交

前情回顾

总结之前的原子写入文章, 其实原子写入解决的是数据脏写的问题, 但是存在数据脏读的问题, 即当写入失败回退时, 部分成功的节点的数据, 已经能够被访问了.

那么从数据库经典的事务级别来说, 实现原子写入就实现了ACID的原子性, 但由于脏读状态的存在, 目前的事务级是读未提交.

读未提交实际上在OLAP应用中, 对大多数业务的影响并没有那么大, 相对于总数为亿行级别的数据, 单次写入个数在10万以内, 因此数据的误差只有万分之一到千分之一, 在大多数业务系统中并不会有那么大的影响.

但对于某些数据量比较小的用户, 读未提交还是会对他们产生比较大的疑问, 因此实现读已提交的隔离级别, 虽非必要, 但是最好如此.

OLAP系统究竟需要怎么样的隔离级别, 这是一个非常好的问题. 隔离级别需要应用层来确定, 但从目前观察到的业务状态, 重复读其实在OLAP中没有必要, 因为OLAP系统中, 用户的SQL很多有依赖关系, 不会出现SQL2的某些数值, 需要从SQL1的结果中获取的场景, 因此可重复读基本上没有实现的意义.

实现读已提交

image-20211108150144214

在这个架构中需要引入全局的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的时候需要执行commitlocal table的操作.

为什么使用Label而不是使用时间戳? 一, 分布式时间戳生成比较麻烦; 二, Flink写入一半只有一个, 只需要判断两种状态, 不需要判断前后时序关系

Flink集成

原子写入一样通过两阶段写入的方式处理, 但过程有点不一致:

  • prepare阶段: 执行local的表的commit操作, 写入DP, 并删除无用的DP, 此时local查询时, 该DP已经可见
  • commit阶段: 只删除ZK上的路径, 查询时会自动查询底层的数据.