一. 背景
《Ad Hoc Transactions in WEB Applications: The Good, the Bad, and the Ugly》由上海交通大学并行与分布式系统研究所发表于SIGMOD22,论文主要调研了在WEB应用中处理数据并发操作类业务的主流方法,包括基于数据库事务、使用ORM框架以及利用编程语言实现的应用层临时事务。在对临时事务开展研究后发现,临时事务在关键API(例如结算)中被广泛应用,虽然灵活性较高,但也容易导致并发错误,甚至对实际业务产生严重影响。同时,在高竞争负载下,这些临时事务可以通过利用应用程序的语义来提升性能。这项研究对临时事务的理解及数据库系统的改进有重要指导意义。[1]
数据库事务是数据库管理系统中用于保证数据一致性和完整性的一种处理方式。它是一组数据库操作的集合,这些操作要么全部执行成功,要么全部不执行,保证了数据的有效性。[2]
谈到事务就不得不提及数据库的隔离,此外在已有数据库事务的基础上为什么开发者还会选择开发应用层临时事务以及如何构造临时事务,对此,下文将依次介绍数据库事务的隔离级别,应用层临时事务的优势,构造应用层临时事务以及论文带来的一些思考。
二. 数据库事务隔离级别
数据库的隔离级别是指在多个并发事务同时访问数据库时,数据库管理系统为了确保事务并发执行时数据的一致性所采取的控制措施和规定。常见的数据库隔离级别包括:
- 读未提交(Read Uncommitted, RU):最低级别的隔离级别,允许一个事务可以读取另一个事务未提交的数据。这可能导致脏读(读取到未提交的数据)问题。
- 读提交(Read Committed,RC):保证一个事务不会读取到另一个未提交事务的数据,只能读取已经提交的数据。但是在同一个事务内的两次相同查询可能会返回不同的结果,因为其他事务可以同时修改数据。
- 可重复读(Repeatable Read, RR):确保一个事务在执行期间多次读取同一行数据时,不会读取到其他事务已提交的对该行数据的修改。可以防止脏读和不可重复读。
- 串行化(Serializable):提供最高的隔离级别,在这个级别下,所有的事务按照严格的顺序依次执行,事务之间彼此完全隔离,可以防止脏读、不可重复读和幻读(即同一个查询在不同时间点返回不同的结果集)。
三. 应用层临时事务的优势
每个隔离级别都有其自身的特点和适用场景,随着隔离级别的提高,事务的并发性能通常会降低,因为需要更多的锁或其他机制来确保数据的一致性。选择合适的隔离级别需要考虑到应用的需求和对并发性能的要求。
大多数应用使用数据库的时候都不会使用串行化的隔离级别,因为性能太差。如图1-2所示,MySQL 的默认隔离级别是RR,PostgreSQL的默认隔离级别是RC。一般为了提高性能,WEB应用可以在部分业务场景下使用RC级别的隔离[3]。
作者在论文中提及了其调研的开源WEB应用的应用层临时事务使用情况,在重要的数据操作流程中,例如交易、消费,大多采用应用层临时事务。通常开发者使用应用层临时事务的原因有以下三点:
- 数据库事务的粒度无法满足应用的需求;
- 数据库无法实现跨WEB的事务请求;
- 数据库难以支持异构系统之间的事务等。
在一般的数据库使用场景下,伴随着数据库的隔离级别提升,性能下降十分严重,为此,应用层临时事务需要做到既利用低隔离级别的数据库防止性能下降,又要实现应用层的事务机制防止数据一致性错误等问题。
应用层临时事务其优势在于开发灵活性和高性能,以在线文档编辑应用为例,大部分WEB应用支持用户在线撰写文档,这整个流程涉及到多个 WEB请求,比如加载页面,保存更新,应用程序需要在这两个请求的过程中保证原子性,那么用户进行文档修改可以看成是一种应用层临时事务,通过应用层次的锁机制即可完成,而无需调用底层的数据库事务机制,更加的高效便捷。对于其它类似的场景,一般由开发者自行开发决定。
图1 MYSQL默认隔离级别
图2 PostgreSQL默认隔离级别
四. 构造应用层临时事务
那么如何构造应用层临时事务。从两个角度来看:一是并发控制,二是故障处理。通过并发控制满足数据一致性要求,故障处理解决WEB宕机等其它故障情况下数据回退需求。
4.1
并发控制
- 使用数据库自带的行锁(悲观锁):通过使用数据库提供的行级锁机制(如SELECT FOR UPDATE语句)来保证数据在读取或更新时的排他性,防止其他事务同时修改相同的数据。这种方法会在事务开始时直接对数据行进行锁定,但可能会导致性能问题和并发度下降。
- 使用数据库表进行锁控制:创建专门的表来存储锁的信息,通过事务来查询和更新该表的状态来实现锁定。这种方式需要精心设计表结构和锁管理逻辑,并且需要处理并发情况下的竞争和死锁问题。
- 使用外部系统(如Redis、Zookeeper)进行锁控制:借助外部系统提供的原子操作,比如Redis的CAS(Compare-and-Swap)来实现分布式锁。但这种方式需要考虑到外部系统的可用性、一致性和性能,以及应用程序本身对不一致状态的容忍性。
- 乐观锁机制:通过在数据库表中增加一个版本号字段(例如ActiveRecord的lock_version),在更新数据时检查版本号变化,若版本号不一致则视为冲突。这种方法假设冲突的概率较低,并且适用于较少冲突的场景,否则可能会导致较多的重试和性能损失。[4]
上述机制都是并发访问控制的经典解决方案。
4.2
故障处理
在故障处理方面,一般有乐观锁和悲观锁两种方式,使用悲观锁的时候应用程序要保证上锁顺序,避免出现死锁。使用乐观锁的时候一般直接返回给用户错误,让用户自己重试即可。[5]
应用服务、数据库、缓存,不管哪个发生故障,都要保证业务逻辑的处理能够继续正常进行,处理的方式和业务逻辑强相关。最基本的,需要保证之前的锁不会阻塞业务逻辑的运行,保证系统状态的回滚。
最后论文对已有的应用层临时事务实现机制进行了性能分析,感兴趣的读者可以阅读原文,本文限于篇幅不再赘述。
五. 总结&思考
论文指出了开发人员普遍使用应用层临时事务的现象,而非使用更模块化的数据库事务。两种潜在原因:一是数据库事务在处理涉及多个存储后端的业务逻辑时存在局限性,另一种可能是性能问题。
为了解决这些问题,论文提出了一些可能的解决方案。首先,对乐观并发控制(OCC)原语的需求,建议在ORM层面提供新的OCC原语。其次,提议开发一个应用级别的代理模块,以提供数据库系统的高级功能,并且建议开发支持工具,以帮助定位、识别和修复与应用层临时事务相关的问题。
笔者认为,作者主要的贡献在于全面的调研了WEB应用中临时事务并人工分析了其中可能存在的一致性错误(例如商品超卖)和故障恢复问题(例如回溯失败),由于这些问题由人工分析得出,不具备普适性,故前文未作介绍。论文也引发笔者的一些思考,在非WEB应用中是否存在临时事务的应用,临时事务的应用带来的一致性错误等问题,可否归属于代码错误并可藉由源代码审计发现,更进一步能否开发一款自动化工具,辅助人工分析或自动化分析临时事务中的逻辑错误。
参考文献
[1] Ad Hoc Transactions in WEB Applications: The Good, the Bad, and the Ugly
[2] https://www.zhihu.com/tardis/zm/art/43493165?source_id=1005
[3] https://github.com/ept/hermitage
[4] ActiveRecord Locking https://api.rubyonrails.org/classes/ActiveRecord/Locking.html
[5] Designing Data-Intensive Applications, Chapter 7
内容编辑:创新研究院 马胜 责任编辑:创新研究院 陈佛忠
本公众号原创文章仅代表作者观点,不代表绿盟科技立场。所有原创内容版权均属绿盟科技研究通讯。未经授权,严禁任何媒体以及微信公众号复制、转载、摘编或以其他方式使用,转载须注明来自绿盟科技研究通讯并附上本文链接。
关于我们
绿盟科技研究通讯由绿盟科技创新研究院负责运营,绿盟科技创新研究院是绿盟科技的前沿技术研究部门,包括星云实验室、天枢实验室和孵化中心。团队成员由来自清华、北大、哈工大、中科院、北邮等多所重点院校的博士和硕士组成。
绿盟科技创新研究院作为“中关村科技园区海淀园博士后工作站分站”的重要培养单位之一,与清华大学进行博士后联合培养,科研成果已涵盖各类国家课题项目、国家专利、国家标准、高水平学术论文、出版专业书籍等。
我们持续探索信息安全领域的前沿学术方向,从实践出发,结合公司资源和先进技术,实现概念级的原型系统,进而交付产品线孵化产品并创造巨大的经济价值。