Spring 中的事务原子性

Transaction atomicity in Spring

SQL/Spring 中的事务原子性是什么意思,不是什么意思?

我在想下面这个案例。如果我错了请纠正我:

此代码不正确:

@Transactional
public void voteUp(long fooId) {
    Foo foo = fooMapper.select(fooId); // SELECT * FROM foo WHERE fooId == #{fooId}
    foo.setVotes(foo.getVotes() + 1);
    fooMapper.update(foo); // UPDATE foo SET votes = #{votes} (...) WHERE fooId == #{fooId}
}

尽管它是事务性的,但如果 voteUp 在许多 machines/in 许多线程上同时调用,并不意味着 "votes" 的值将始终递增 1?如果是这样,是否意味着一次只能执行一个事务,导致效率下降(特别是如果 voteUp 的代码在事务中做了更多的事情)?

唯一正确的做法是这样(?):

/* not necessarily */ @Transactional 
public void voteUp(long fooId) {
    fooMapper.voteUp(fooId); // UPDATE foo SET votes = votes + 1 WHERE fooId == #{fooId}
}

在示例中,我使用 myBatis 连接到数据库,但我认为如果我使用休眠或普通 SQL 语句,问题仍然相同。

隔离级别决定了事务中数据视图的可靠性。最可靠的隔离级别是可序列化的(这确实会影响数据库的性能),但通常的默认值是 read-committed:

In this isolation level, a lock-based concurrency control DBMS implementation keeps write locks (acquired on selected data) until the end of the transaction, but read locks are released as soon as the SELECT operation is performed (so the non-repeatable reads phenomenon can occur in this isolation level, as discussed below). As in the previous level, range-locks are not managed.

Putting it in simpler words, read committed is an isolation level that guarantees that any data read is committed at the moment it is read. It simply restricts the reader from seeing any intermediate, uncommitted, 'dirty' read. It makes no promise whatsoever that if the transaction re-issues the read, it will find the same data; data is free to change after it is read.

在第一个示例中,在 select 和更新之间,其他一些进程可以更改计数器的值:select 发生,然后其他一些进程更改计数器的值,然后更新作用于更改的行。

将隔离级别更改为 repeatable-read 应确保第一个示例中的增量正常工作。当然,第二个例子是正确的,是一个更好的解决方案。

不仅仅是原子性。一个标准的数据库事务必须具有以下特征:

  • 原子
  • 一致
  • 隔离
  • 耐用

这些是 "ACID" 要求。您标记的 "incorrect" 实际上仍然是原子的,但不是孤立的。要使其 isolated(因此并发更新仍然会给您正确的结果),您可以将并发处理委托给数据库(set vote = vote+1)或使用框架的功能来正确处理隔离。

https://en.wikipedia.org/wiki/Database_transaction

根据 documentation,当您使用 @Transactional 注释方法时,Spring 会创建一个与您注释的 class 具有相同接口的代理。当您调用对象的方法时,所有调用都通过代理对象传递。代理对象在 try catch 构造中包装了 class 的事务方法。您的原始对象代码:

@Transactional
public void voteUp(long fooId) {
    Foo foo = fooMapper.select(fooId); // SELECT * FROM foo WHERE fooId == #{fooId}
    foo.setVotes(foo.getVotes() + 1);
    fooMapper.update(foo); // UPDATE foo SET votes = #{votes} (...) WHERE fooId == #{fooId}
}

代理对象大概是这样的:

//It's all approximately just to show you a way how Spring does it. 
public void voteUp(long fooId) {
    EntityTransaction tx = em.getTransaction();
    tx.begin();
    try{
       originalObject.voteUp(fooId);
       tx.commit();
    }catch(Exception e){
       tx.rallback();
       throw e;
    }
 }

因此,即使 voteUp 在许多 machines/in 多个线程上并发调用,"votes" 的值也将始终递增 1。因为一个线程中的事务将阻塞 table 以从其他线程写入数据。

你是对的:如果voteUp方法需要很长时间,就会导致效率下降。这意味着你的方法,由@Transactional 注释不应该花费很长时间。

你说得对,如果你的 ORM 库允许这种方式,你可以不经选择地更新你的数据库记录。

@Transactional 在这种情况下用于管理 SQL 事务,它不添加任何线程安全。 Spring 事务管理器除了要求数据库开始一个新事务外并没有做太多事情,所以你需要参考 RDBMS 的文档并阅读它的事务语义。

所以是的,即使 SELECT 和 UPDATE 是同一事务的一部分,您的第一个示例中也会存在竞争条件。您的问题有两种可能的解决方案:

1- 行锁定:获取对您要修改的行的锁定将阻止任何其他 SQL 事务修改其值。

2- 乐观锁定:乐观锁定实际上不使用任何锁。您所做的是使用一个您确定会在该行更新时更改的值。例如,您可以 re-write 您的更新语句为:

UPDATE foo SET votes = #{votes} (...) WHERE fooId == #{fooId} AND votes = #{oldNoOfVotes}

如果没有更新任何行,则意味着另一个进程已经更改了该行的值,然后您可以重试或抛出异常。

根据:https://www.baeldung.com/spring-transactional-propagation-isolation#5-serializable-isolation

第一个代码是正确的: @Transactional(隔离= Isolation.SERIALIZABLE) ,但不适用于: @Transactional(隔离= Isolation.READ_COMMITTED).

即这取决于设置的隔离级别。

默认为 DEFAULT,取决于底层数据库。