ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

LogMiner vs 裸日志解析(八):长事务与回滚,数据一致性埋雷

LogMiner vs 裸日志解析(八):长事务与回滚,数据一致性埋雷 前面聊了性能、内存、数据类型这些“看得见”的问题。今天聊一个更隐蔽、但后果更严重的坑——长事务和回滚导致的数据一致性问题。这个问题隐蔽在哪它不报错不崩溃任务看起来跑得好好的。但下游拿到的数据可能有一小部分是“不该存在”的。等发现的时候数据已经错了。先看两个真实场景场景一回滚的数据被发出去了。业务系统里有一个转账事务。先扣A账户的钱再给B账户加钱中间业务逻辑判断失败事务回滚了。但CDC链路已经把“A账户扣钱”这个变更发出去了。下游数据库里A账户的钱少了B账户的钱没变。这笔钱凭空消失了。问题出在LogMiner在处理回滚的时候把回滚语句本身当成了SQL_REDO上报而不是标记为SQL_UNDO。场景二长事务被拆成多段后半段解析出来是乱的。一个批量处理事务跑了两个小时涉及几百万行数据横跨了多个redo log文件。第一个日志文件解析完了第二个日志文件解析到一半任务重启了。检查点记录的位置在第二个文件的中间——但这个事务的上下文XID、已解析的Change Vector、事务状态全丢了。重启后从检查点继续读读到的是一堆孤立的Change Vector——不知道属于哪个事务、不知道是提交还是回滚、不知道跟前面的变更怎么关联。解析出来的数据全是残片。为什么回滚会“笨拙且容易出错”要理解这个问题得先搞清楚LogMiner处理回滚的机制。Oracle的redo log里回滚不是“撤销记录”是“反向操作记录”。当一个事务回滚时Oracle不是把之前写的redo记录删掉而是写入新的Change Vector把数据改回原来的样子。这些Change Vector的操作码是OP 5.1撤销修改Body里存的是前镜像。LogMiner在解析的时候读到OP 5.1它面临一个选择方案A把它标记为SQL_UNDO表示“这是一个撤销操作”方案B把它标记为SQL_REDO表示“这是一个新的变更”LogMiner选择了方案B。为什么因为LogMiner的设计初衷是“诊断工具”它假设你只是想看redo log里记录了什么。回滚产生的反向操作在物理层面确实是一条新的redo记录。LogMiner忠实地把它当作一条redo记录来上报。但对CDC来说这就是灾难。CDC需要的是“最终生效的变更”。如果事务回滚了这个事务的所有变更都不应该输出。但LogMiner把回滚操作本身也当成了一条变更——下游会收到一条“把salary从8000改回5000”的记录。如果下游只是简单执行这条记录数据最终会是对的。但如果下游有复杂的业务逻辑——比如触发器、计算字段、外键关联——这条“回滚变更”可能触发一系列不该发生的操作。更糟糕的是如果下游没有正确处理SQL_UNDO和SQL_REDO的区别可能会把回滚操作也当成正常变更来消费。长事务的问题更复杂长事务横跨多个解析窗口是另一个大坑。LogMiner的解析是分批次进行的。每次查询V$LOGMNR_CONTENTS它会从指定的起始SCN开始扫描一定范围的redo记录返回结果然后结束这次查询。下一次查询从上次结束的位置继续。如果一个事务横跨了多个查询窗口会怎样假设事务T在窗口1里修改了数据块A在窗口2里修改了数据块B在窗口3里提交。窗口1解析完的时候事务T还没提交。LogMiner把数据块A的变更缓存在内存里等提交。但窗口1结束了LogMiner会话可能被关闭取决于配置。内存里的缓存被释放了。事务T的上下文丢了。窗口2开始读到数据块B的变更。LogMiner不知道这个变更属于事务T——没有上下文。它可能把这个变更当成一个独立的事务来输出。窗口3读到COMMIT。但事务T的数据块A变更已经丢了。下游收到的是不完整的事务——只有数据块B的变更没有数据块A的变更。数据不一致。社区里的真实案例Debezium的GitHub上有人报过类似的问题。有个用户发现当启用internal.log.mining.use.cte.query时已提交的事务会卡在Debezium里不发送。社区有人尝试修复但问题依然存在。Flink CDC的Issue #1940里用户反馈SCN快速增长时LogMiner追不上数据。根因是lastProcessedScn反馈机制有缺陷——下一轮用上一轮的SCN作为起始点查不到数据SCN就卡住了。这会导致部分事务的变更丢失。还有用户反馈长事务的处理会导致内存持续增长最终OOM。这些问题的根因都一样LogMiner的事务边界和回滚语义处理方式是Oracle内部决定的上层工具无法干预。Debezium能做的是在它那层加一些补偿逻辑——比如检测到回滚时过滤掉已发出的数据。但如果LogMiner已经把回滚数据发出去了Debezium很难准确判断哪些该过滤、哪些不该过滤。而且回滚的判断需要上下文——知道哪些变更属于同一个事务、这个事务最终是提交还是回滚。如果上下文在LogMiner那层就丢了Debezium拿到的就是一盘散沙。裸日志解析怎么做TLA的做法是在二进制层面判断事务状态从机制上避开这些问题。第一按XID归集不依赖LogMiner的事务边界。每个Change Vector都带XID。TLA读到Change Vector先提取XID按XID分组。同一个事务的所有碎片无论分散在多少个redo record、多少个日志文件里都能归到一起。第二在二进制层面判断提交还是回滚。OP 5.2是事务开始OP 5.4是事务提交OP 5.1是回滚操作。TLA直接解析这些操作码判断事务的最终状态。以OP 5.4结束的事务 → 已提交 → 输出所有变更以OP 5.1结束的事务 → 已回滚 → 丢弃所有变更这个判断不依赖LogMiner不依赖数据库接口纯粹在二进制层面完成。第三回滚的事务根本不输出。TLA不会把回滚操作当成一条“变更”发出去。回滚的事务所有Change Vector都在解析阶段被丢弃。下游不会收到“本不该存在的数据”。第四长事务的上下文不会丢。TLA是流式解析——边读边解析事务的上下文XID、状态、已处理的Change Vector在解析引擎内部维护不依赖任何外部组件。事务T的数据块A变更和B变更即使跨了多个日志文件TLA也能通过XID把它们归到一起。读到COMMIT的时候统一输出。检查点只在COMMIT边界打——确认一个事务的所有Change Vector都处理完了才记录检查点。恢复的时候从检查点继续不会遇到“半个事务”的问题。我们团队在做TLA的时候事务状态判断和回滚处理就是按这个思路实现的。在二进制层面判断事务的最终状态只有提交的事务才输出回滚的事务直接丢弃。流式解析上下文在引擎内部维护不依赖任何外部组件。目前Oracle版本已经跑通了后续MySQL、PG和国产数据库的支持也在推进中。一个直观的对比假设有一个事务先修改表A再修改表B最后回滚。方案解析结果数据一致性LogMiner表A变更 表B变更 回滚操作可能被当作SQL_REDO上报下游可能收到“不该存在的数据”裸日志解析整个事务被识别为回滚所有变更丢弃下游收不到任何变更再假设一个长事务横跨三个日志文件中途任务重启。方案解析结果数据一致性LogMiner上下文可能丢失后半段变成残片可能输出不完整的事务裸日志解析按XID归集跨文件关联检查点对齐事务完整不丢不重再说一个容易被忽略的点回滚不一定是“全回滚”。Oracle支持保存点Savepoint。一个事务可以回滚到某个保存点而不是全部回滚。这意味着事务的一部分操作生效了另一部分被撤销了。对CDC来说这比全回滚更复杂——你需要知道哪些变更生效了、哪些被撤销了。LogMiner处理保存点回滚的能力有限。有些情况下它会把保存点之前的变更和之后的变更混在一起下游拿到的事务是不完整的。裸日志解析可以在二进制层面跟踪保存点——Oracle在redo里会记录保存点的操作码。解析器可以根据这些操作码准确地判断哪些变更生效、哪些被撤销。说句实在话长事务和回滚导致的数据一致性问题是LogMiner最隐蔽的坑。它不报错不崩溃任务看起来跑得好好的。但下游拿到的数据可能有一部分是“不该存在”的。等发现的时候数据已经错了。这个问题的根因在于事务边界和回滚语义的处理方式由LogMiner内部决定上层工具无法干预。Debezium能做的是在外层打补丁但补丁解决不了根因。LogMiner把回滚数据发出去了Debezium很难准确判断该不该过滤。TLA的裸日志解析在二进制层面判断事务状态——只有已提交的事务才输出回滚的事务根本不输出。从机制上保证了数据一致性。这是“LogMiner vs 裸日志解析”系列的第八篇。后面会继续聊其他几个坑——SCN追不上、在线日志读写冲突等等。欢迎交流。补充文中提到的案例和Issue来自公开的GitHub讨论和技术社区可自行查证。实际效果受硬件配置、数据库版本、事务大小等因素影响。
返回列表