数据库事务是否可以防止竞态条件?

40

我不太清楚数据库系统中的事务是做什么的。我知道它们可以用来完全回滚一系列更新操作(例如从一个账户扣款并将其添加到另一个账户),但这是它们全部的作用吗?具体地说,它们能否用于防止竞态条件?例如:

// Java/JPA example
em.getTransaction().begin();
User u = em.find(User.class, 123);
u.credits += 10;
em.persist(u); // Note added in 2016: this line is actually not needed
em.getTransaction().commit();

我知道这个可能可以被写成单个的更新查询,但那并不总是情况。

这段代码能够防止竞态条件吗?

我主要关心的是MySQL5 + InnoDB,但一般性的回答也可以。


3
这是一个读取-修改-写入循环的示例,如果您不使用基于行版本控制的乐观并发控制,或者在具有强SERIALIZABLE支持的数据库中使用SERIALIZABLE隔离级别,则不安全。请参阅我最近撰写的关于此主题的文章:http://blog.2ndquadrant.com/postgresql-anti-patterns-read-modify-write-cycles/ - Craig Ringer
4个回答

40
TL/DR: 事务并不会本质上防止所有的竞态条件。在所有实际的数据库实现中,你仍然需要锁定、中止和重试处理或其他保护措施。 事务并不是一个秘密酱汁,你可以添加到查询中,使它们免受所有并发效应的影响

隔离性

你的问题涉及到ACID中的I——隔离性。学术上的纯理念是,事务应该提供完美的隔离性,以便结果与每个事务串行执行时相同。实际上,在真实的RDBMS实现中很少有这种情况;能力因实现而异,规则可以通过使用较弱的隔离级别(如READ COMMITTED)而被削弱。在实践中,即使在SERIALIZABLE隔离级别下,你也不能假设事务可以防止所有竞态条件

一些关系型数据库比其他数据库具有更强的能力。例如,PostgreSQL 9.2及更新版本具有相当不错的SERIALIZABLE隔离级别,可以检测到大部分(但不是全部)可能存在的事务交互,并且终止与之冲突的所有事务,从而可以安全地并行运行事务。
很少有系统(如果有的话)具有真正完美的SERIALIZABLE隔离级别,可以防止所有可能的竞争和异常情况,包括锁升级和锁顺序死锁等问题。
即使使用强隔离级别,一些系统(如PostgreSQL)仍会终止有冲突的事务,而不是让它们等待并按顺序运行。因此,您的应用程序必须记住正在进行的操作并重新尝试事务。因此,虽然事务已经防止了并发相关的异常存储到数据库中,但是它所采用的方式对于应用程序来说不是透明的。
原子性

数据库事务的主要目的之一是提供原子提交。在提交事务之前,更改不会生效。当您提交时,就其他事务而言,所有更改都同时生效。任何事务都无法仅看到事务所做的某些更改。同样地,如果您回滚,则任何其他事务都不会看到事务的更改;就好像您的事务从未存在过。

这就是ACID中的A。

耐久性

另一个是耐久性-ACID中的D。它指定了当您提交事务时,必须真正保存到能够经受得住故障(如断电或突然重新启动)的存储器中。

一致性:

请参见wikipedia

乐观并发控制

与使用锁定和/或高隔离级别不同,像Hibernate、EclipseLink等ORM通常使用乐观并发控制(通常称为“乐观锁定”)来克服较弱隔离级别的限制,同时保持性能。

这种方法的一个关键特点是它可以让您跨越多个事务进行工作,在具有高用户数并且可能存在长时间延迟的系统中,这是一个非常大的优势。

参考资料

除了文本链接外,还可以查看PostgreSQL文档中关于锁定、隔离和并发的章节。即使您使用不同的RDBMS,也可以从其解释的概念中学到很多东西。


1为了简单起见,我忽略了很少被实现的READ UNCOMMITTED隔离级别;它允许脏读。

2正如@meriton所指出的那样,反之并不一定成立。幻读会在SERIALIZABLE以下的任何级别中发生。正在进行的事务的某个部分看不到某些更改(由尚未提交的事务),然后正在进行的事务的下一个部分在另一个事务提交时确实看到了这些更改。

3好吧,如果我没记错的话,SQLite2通过锁定整个数据库来解决写入问题,但这并不是我所说的并发问题的理想解决方案。


挑剔一点:“任何事务都不能只看到其所做的更改中的一部分”也适用于幻读,在弱于Serializable的所有隔离级别中都可能发生。 - meriton
1
@meriton 幽灵读取是一个问题,但它们不会导致您只看到事务效果的一部分。除非您使用“READ UNCOMMITTED”,否则仍然保证您不会只看到某些事务的效果。 - Craig Ringer

18

数据库层支持事务的原子性,在不同程度上实现隔离级别。查看您的数据库管理系统文档以了解支持的隔离级别及其权衡。最强的隔离级别是Serializable,要求事务像一个接一个地执行。通常通过在数据库中使用独占锁来实现。这可能会导致死锁,而数据库管理系统会检测并通过回滚某些涉及的事务来修复它们。这种方法通常称为悲观锁定

许多对象关系映射器(包括JPA提供者)也支持乐观锁定,其中更新冲突不在数据库中预防,而是在应用程序层中检测,然后回滚事务。如果启用了乐观锁定,您的示例代码的典型执行将发出以下SQL查询:

select id, version, credits from user where id = 123;  

假设这个函数返回 (123, 13, 100)。

update user set version = 14, credit = 110 where id = 123 and version = 13;

数据库会告诉我们更新了多少行。如果是一行,则没有发生冲突的更新。如果是零行,则发生了冲突的更新,JPA提供程序将会处理。

rollback;

并抛出异常以便应用程序代码可以处理失败的事务,例如通过重试。

总结:使用这两种方法中的任何一种,都可以使您的语句不受竞态条件的影响而变得安全。


最强的隔离级别是 SERIALIZABLE 而不是 SERIALIZED。你说 "这是通过在数据库中使用排他锁实现的",但这取决于数据库系统和版本。PostgreSQL 能够通过检测事务依赖关系并中止可能发生冲突的事务,在 SERIALIZABLE 隔离级别下实现并发。 - Craig Ringer

4

这取决于隔离级别(在串行化中,它将防止竞争条件,因为通常在串行化隔离级别中,事务是按顺序处理的,而不是并行处理的(或者至少使用独占锁定,因此修改相同行的事务是按顺序执行的)。 为了防止竞争条件,最好手动锁定记录(例如,mysql支持“select ... for update”语句,它会在所选记录上获取写锁定)


2
实际上,在SERIALIZABLE事务中,DBMS必须以这样的方式处理事务,即如果按顺序处理,则具有相同的效果。只要DBMS满足该要求,它们可以是并发的,一些系统(例如PostgreSQL)就是这样做的。 - Craig Ringer

1

这取决于具体的关系型数据库管理系统。通常,事务在查询评估计划期间决定获取锁。有些可以请求表级锁,其他列级锁,其他记录级锁,第二种对性能更好。简短回答您的问题是肯定的。

换句话说,事务旨在将一组查询分组并将它们表示为原子操作。如果操作失败,则会回滚更改。我不确定您正在使用的适配器的功能,但如果符合事务的定义,那么应该没问题。

虽然这保证了防止竞态条件,但并没有明确防止饥饿或死锁。事务锁管理器负责此事。有时会使用表锁,但它们的代价是减少并发操作的数量。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接