EagleBear2002 的博客

这里必须根绝一切犹豫,这里任何怯懦都无济于事

数据库临时事务(Ad hoc transaction)

摘要

IPADS 发表了与 Ad Hoc 事务相关的文章共三篇,内容基本相同。其中第三篇内容最为详细。

  1. Ad Hoc Transactions in Web Applications: The Good, the Bad, and the Ugly | Proceedings of the 2022 International Conference on Management of Data (acm.org)
  2. Ad Hoc Transactions: What They Are and Why We Should Care | ACM SIGMOD Record
  3. Ad Hoc Transactions through the Looking Glass: An Empirical Study of Application-Level Transactions in Web Applications | ACM Transactions on Database Systems

为什么需要临时事务

数据库事务无法满足 Web 应用的要求:

  • 高隔离级别的事务性能不好,低隔离级别的事务解决不了并发数据访问带来的问题;
  • 事务的粒度对应用程序来讲不够自由;
  • 跨越多个 Web 请求的事务没办法通过数据库来实现(这一条是数据库事务无法支持的,必须要临时事务才能支持);
  • 异构系统之间的事务没办法很好地由数据库支持;

应用层临时事务需要做到既利用低隔离级别的数据库防止性能下降,又要实现应用层的事务机制防止数据一致性错误等问题。

应用层临时事务其优势在于开发灵活性和高性能,以在线文档编辑应用为例,大部分 WEB 应用支持用户在线撰写文档,这整个流程涉及到多个 WEB 请求,比如加载页面,保存更新,应用程序需要在这两个请求的过程中保证原子性,那么用户进行文档修改可以看成是一种应用层临时事务,通过应用层次的锁机制即可完成,而无需调用底层的数据库事务机制,更加的高效便捷。对于其它类似的场景,一般由开发者自行开发决定。

MySQL 的默认隔离级别是 RR,PostgreSQL 的默认隔离级别是 RC。一般为了提高性能,WEB 应用可以在部分业务场景下使用 RC 级别的隔离。

临时事务的特点

电商应用:

1
2
3
4
5
6
7
8
9
10
11
12
13
IN: sku_id, requested                         // 1
lock(sku_id); // 2
sku := SELECT * FROM SKUs WHERE id = sku_id; // 3
if sku.quantity >= requested: // 4
sku.quantity -= requested; // 5
TRANSACTION START; // 6
UPDATE SKUs SET quantity = sku.quantity WHERE id = sku.id;
UPDATE Products SET updated_at = now() WHERE id = sku.product_id;
catagory_ids := SELECT catagory_id FROM
Catagories JOIN Products USING catagory_id WHERE product_id = sku.product_id;
UPDATE Catagories Set updated_at = now() WHERE id IN catagory_ids
TRANSACTION COMMIT;
unlock(sku_id);

其它利用应用特点避免使用强隔离级别,或者使用应用层的锁代替数据内部锁的情况。

在应用层,我们只要保证程序正确就行了,因此我们就能利用领域知识去实现类似事务的语义从而保证程序正确。同时,没有了数据库原生的支持,对应用程序开发人员也提出了更高的要求。

如何构造应用层临时事务

从两个角度来看:一是并发控制,二是故障处理。通过并发控制满足数据一致性要求,故障处理解决 WEB 宕机等其它故障情况下数据回退需求。

并发控制

  • 使用数据库自带的行锁(悲观锁):通过使用数据库提供的行级锁机制(如 SELECT FOR UPDATE 语句)来保证数据在读取或更新时的排他性,防止其他事务同时修改相同的数据。这种方法会在事务开始时直接对数据行进行锁定,但可能会导致性能问题和并发度下降。
  • 使用数据库表进行锁控制:创建专门的表来存储锁的信息,通过事务来查询和更新该表的状态来实现锁定。这种方式需要精心设计表结构和锁管理逻辑,并且需要处理并发情况下的竞争和死锁问题。这里 Insert 的时候可能需要依赖唯一键的约束,Update 的时候能依据锁的状态再更新。
  • 使用外部系统(如 Redis、Zookeeper)进行锁控制:借助外部系统提供的原子操作,比如 Redis 的 CAS(Compare-and-Swap)来实现分布式锁。但这种方式需要考虑到外部系统的可用性、一致性和性能,以及应用程序本身对不一致状态的容忍性。
  • 乐观锁机制(MVCC 思想):通过在数据库表中增加一个版本号字段(例如 ActiveRecord 的 lock_version),在更新数据时检查版本号变化,若版本号不一致则视为冲突。这种方法假设冲突的概率较低,并且适用于较少冲突的场景,否则可能会导致较多的重试和性能损失。

上述机制都是并发访问控制的经典解决方案。

故障处理

在故障处理方面,一般有乐观锁和悲观锁两种方式,使用悲观锁的时候应用程序要保证上锁顺序,避免出现死锁。使用乐观锁的时候一般直接返回给用户错误,让用户自己重试即可。

应用服务、数据库、缓存,不管哪个发生故障,都要保证业务逻辑的处理能够继续正常进行,处理的方式和业务逻辑强相关。最基本的,需要保证之前的锁不会阻塞业务逻辑的运行,保证系统状态的回滚。

可以改进的地方

笔者认为,作者主要的贡献在于全面的调研了 WEB 应用中临时事务并人工分析了其中可能存在的一致性错误(例如商品超卖)和故障恢复问题(例如回溯失败),由于这些问题由人工分析得出,不具备普适性,故前文未作介绍。论文也引发笔者的一些思考,在非 WEB 应用中是否存在临时事务的应用,临时事务的应用带来的一致性错误等问题,可否归属于代码错误并可藉由源代码审计发现,更进一步能否开发一款自动化工具,辅助人工分析或自动化分析临时事务中的逻辑错误。

论文解读:Ad Hoc Transactions in WEB Applications-腾讯云开发者社区-腾讯云 (tencent.com)

06-在应用层重新实现事务- Ad Hoc Transaction in Web Applications - 知乎 (zhihu.com)