2.3 事务处理与ACID取舍


2.3 事务处理与ACID取舍

本节摘要:分析引擎的事务不是为了扛并发下单,而是为了保证"批量操作的要么全有、要么全无"。DuckDB 用乐观并发的多版本机制实现 ACID:读不阻塞读,写与写互斥。本节讲清它在分析场景的三大真实用途,以及 optimistic 策略的边界。

从一次失败导入说起

场景很日常:你把三批清洗好的数据追加进分析库,跑到第二批发现上游格式变了,脏数据已经进库一半。没有事务的世界里,你要手工找出"哪些行是第二批的"然后小心剔除;有事务的世界里,一句回滚回到导入前的干净状态。分析场景的事务需求就这么朴素:不是每秒几千笔下单,而是让批量操作具备"要么全有、要么全无"的性质。DuckDB 为此提供了完整的 ACID 支持,只是实现策略和 OLTP 引擎大不相同。

ACID 在分析语境下的逐项解读

特性 OLTP 引擎的典型关注 DuckDB 的分析化解读
原子性 单笔交易不出现半笔 批量导入与改写要么全成要么全消
一致性 约束不被并发破坏 约束校验 + 崩溃后日志重放恢复
隔离性 并发写同一行的排队 快照隔离,读到一致版本
持久性 提交即落盘 提交进预写日志,检查点合并(第3.3节展开)

四项里最值得展开的是隔离性。DuckDB 采用乐观并发加多版本:写入不改原数据,而是创建新版本;每个事务看到的是它开始时的那个一致快照。读查询永远不会被写操作卡住——你可以在分析跑一半时放心地另起一个查询,它读到的数据不会"半新半旧"。代价是写与写互斥:同一时刻只允许一个写事务在跑,第二个写事务要等前一个提交或回滚。

写事务的一生

状态图里"校验并生效"五个字藏着乐观并发的前提:它默认冲突很少发生,出冲突才处理。单进程内本来就只允许一个写者,冲突源主要是"写事务提交时,检查点恰好在进行"。遇到这种冲突,引擎会请事务重试——应用层只需捕获重试提示再提交一次。

三大真实用途与代码

用途一:批量导入的原子性。 多源数据合并进一张表,任何一批失败就整体重来,不留半成品:

BEGIN TRANSACTION; INSERT INTO trades_clean SELECT * FROM read_csv_auto('batch_a.csv'); INSERT INTO trades_clean SELECT * FROM read_csv_auto('batch_b.csv'); -- 假如 batch_b 的行数对不上预期,主动放弃全部 -- ROLLBACK; COMMIT;

用途二:先验证后生效。 在事务里做破坏性改写,跑一遍校验查询,不满意就回滚——数据库版本的可回退"草稿":

BEGIN TRANSACTION; UPDATE trades_clean SET category = 'unknown' WHERE category IS NULL; -- 校验:影响行数是否符合预期 SELECT count(*) FROM trades_clean WHERE category = 'unknown'; COMMIT; -- 数字不对就 ROLLBACK

用途三:读写一致性。 长分析查询运行期间,另一个会话写入了新数据——正在跑的查询仍按它启动时的快照读完,结论不会前后矛盾。这对"边算边有人补数"的小团队是实打实的安宁。

乐观策略的边界

三个提醒。其一,长写事务会拖住检查点:预写日志一直收不拢,文件持续变大,所以写事务讲究"短平快",别把交互式探索和大批量写入搅在一个事务里。其二,写互斥意味着"多线程同时写同一库"的模式行不通——要并行,先各自写各自的文件再合并(主线案例第7章就用这招)。其三,快照隔离读的是旧版本,长查询背后数据可能已经前进,需要"最新"结论时,重跑一次查询比纠结快照更实际。三条都指向同一个姿态:分析场景里,事务是保险装置不是常态通道——把它用在刀刃上(批量原子性、可回退的破坏性操作),其余时候让查询保持轻装,这套引擎的效率优势才能全速兑现。

多版本在存储里长什么样

快照隔离的原理值得往存储层多看一眼,因为它和第3章的内容直接咬合。多版本的意思是:你 UPDATE 一行时,引擎不覆盖原数据,而是把新值写成新版本、在版本链上挂个指针;老版本要等"没有任何事务可能再读它"才被回收。读查询拿着自己的事务起点当标尺,沿着版本链找"我能看见的最新版本"。这解释了本节开头那个现象——读写互不干扰,因为读的根本不是同一份数据。

版本链的代价也要认识:更新密集的表会悄悄膨胀。老版本等回收、回收有节奏,一顿狂删狂改之后,文件里可能堆着大量"已经没人能看见"的陈旧版本。分析场景好在更新天然稀少(数据大多一次写入、反复查询),膨胀是例外不是常态;但如果你把分析库当队列用——频繁 UPDATE 状态字段——就会发现文件越滚越大而行数没变。解法在下一节预告里:让引擎做一次检查点整理(第3.3节),把活版本压实、陈旧版本丢弃。分析库少更新,是使用姿态也是健康秘诀

事务使用的四条纪律

把上面的原理与边界收拢成四条,按出错频率排序。第一条,写事务要短:一个事务干一件事,导入就导入、更新就更新,别把半小时的交互探索裹在事务里——长写事务拖住检查点,日志和版本链一起膨胀。第二条,破坏性操作先进事务:UPDATE 和 DELETE 前面加一行 BEGIN,跑完校验再 COMMIT,五秒钟的手续买到全量后悔药。第三条,校验看影响行数:改完先数一数动了几行,与你预期差一个量级就回滚——"影响行数"是事务给你配的天然对账单。第四条,多进程别共写:这是引擎的硬约束(第1.4节反例清单的根因),要并行写就各写各文件、后合并,主线案例第7章的收官会演示这个流程。四条都不难,难的是出事之前把它们当回事。

反方向的智慧:什么时候不用事务

讲完用途与纪律,补一刀反方向的:事务也有不该用的时候。纯只读的探索查询不需要 BEGIN——快照隔离本来就在起作用,显式事务只是包了层纸。单条语句自带原子性——DuckDB 里一条 INSERT 或 CREATE TABLE AS 就是天然的事务,语句要么全成要么全无,额外的 BEGIN COMMIT 是冗余动作。跨语句但无失败风险的组合也不必裹事务——两条连续的建表语句,第二条失败了你重跑一次就是,回滚机制没有减少任何工作量。事务的正确心智是"为需要全有全无语义的操作付费":BEGIN 之后的每条语句都在占用写锁、延缓检查点,空包事务是白付的租金。判断式很简单:问自己"中途失败时,我希望前面几条也作废吗"——答案是"希望"就用事务,答案是"无所谓"就省了。

本节要点回顾

  • 分析事务的朴素用途:批量原子性、先验证后生效、读写一致性快照——不是扛并发下单。
  • 乐观并发的交换条件:读永远不被写阻塞,换来写与写互斥,冲突靠重试解决。
  • 写事务要短:长写事务拖住检查点,日志膨胀;大批量写与交互探索分开做。
  • 并行写的正解:多线程各自写文件再合并,而不是共写一个库。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U