Spring Boot中操作数据库的几种并发事务方式

当有多个并发事务时,会发生丢失更新异常。来自一个或多个事务的更新可能会丢失,因为其他事务会用其结果覆盖它。

让我们通过一个例子来检验一下。考虑以下执行事务的方法。

public void withdraw(Long accountId, double amount) {

Account account = accountRepository.findById(accountId).orElseThrow(() -> {
throw new IllegalStateException("account does not exist: " + accountId);

});

double newBalance = (account.getBalance() - amount);
if (newBalance < 0) {
throw new IllegalStateException("there's not enough balance");

}

account.setBalance(newBalance);

accountRepository.save(account);

}

只要在任何给定时间点只有单个事务交易,这段代码会按预期工作。

当有多个同时事务时会发生什么?

在这种情况下,上述代码将无法正常工作。线程 1 对 newBalance 所做的修改线程 2 是看不到的。因此,它可能会破坏数据。当我们用 @Transactional 对方法进行注解时,行为不会发生变化。反正它只是定义应用程序的事务边界。

如何防止损失更新异常?

请注意,Spring 默认遵循底层数据存储的隔离级别。Postgres 的默认隔离级别是 READ_COMMITTED。这意味着它只能看到查询开始前提交的数据,而看不到未提交的数据或查询执行期间并发事务提交的更改。

实际上,我们可以通过原子更新操作来解决这个问题!

怎么做?

使用本地更新查询,在数据库中执行直接更新,而不是使用普通 ORM 风格的 "选择、修改和保存"。

@Transactional
public void withdraw(Long accountId, double amount) {

Double currentBalance = accountRepository.getBalance(accountId);
if (currentBalance < 0) {
throw new IllegalStateException("there's not enough balance");

};

accountRepository.update(accountId, amount);

}

因此,我们使用了自定义更新方法,而不是通常的保存方法。这种更新方法具体是怎样的呢?

下面是在存储库类中添加的更新方法:

@Transactional

@Modifying

@Query(nativeQuery = true ,

clearAutomatically=true ,

flushAutomatically=true ,

value = """

update account

set balance = (balance - :amount)

where id = :accountId """

)
public int update(Long accountId, Double amount);

请注意,我们在这两个方法中都使用了 @Transactional 注解。但它们属于两种不同类型的 Bean:一种来自服务,另一种来自存储库类。因此,更新方法遵循自己的事务定义。

  • @Modifying 会触发注解为 UPDATE 查询的方法,而不是 SELECT 查询。
  • 由于在执行Update修改查询后,实体管理器(EntityManager)中可能会包含过时的实体,所以它不会自动清除它,因此,我们需要明确说明 clearAutomatically=true。
  • 在执行Update修改查询之前,我们还需要自动清除持久化上下文中的任何受管实体。因此使用 flushAutomatically=true。

实现并发安全的更多方法

1、对任何更新使用悲观锁

将下面的注解与现有的事务注解一起使用:

@Lock(LockModeType.PESSIMISTIC_WRITE)

2、使用数据存储特定的咨询锁

在 postgres 中使用 pg_try_advisory_xact_lock 咨询advisory锁,同时使用超时和键(通常是数据库主键)。

将其与retry 重试模板一起使用,这样它就会不断重试,直到获得主键锁的指定超时为止。

示例:

@Transactional
public void tryWithLock(String key, Duration timeout, Runnable operation) {

lock(key.getKey(), timeout);
// your DB updates run here. operation.run();

}

private void lock(final String key, Duration timeout) { //尝试获取锁,直到超时结束 retryTemplate.execute(retryContext -> {
boolean acquired = jdbcTemplate

.queryForObject("select pg_try_advisory_xact_lock(pg_catalog.hashtextextended(?, 0))", Boolean.class, key);

if (!acquired) {
throw new AdvisoryLockNotAcquiredException("Advisory lock not acquired for key '" + key + "'");

}
return null ;

});

}

您可以直接在 JPA 查询中使用咨询锁,这样会简单得多。

@Transactional

@Query(value = """

select c

from Account c

where c.id = :accountId

and pg_try _advisory_xact_lock(

pg_catalog.hashtextextended('account', c.id)

) is true """

)
public Account findByIdWithPessimisticAdvisoryLocking(Long accountId);

3、在 POJO 类中使用带版本号的乐观锁

在 POJO 类中添加注释为 @Version 的属性。

然后使用常规的 Spring JPA 查询来获取更新数据。

在将更新写入数据库之前,Spring JPA 会自动检查版本。如果有任何脏写入,事务将中止,客户端可以使用新版本重新尝试事务。这最适合大容量系统。

4、使用悲观的 NO_WAIT 锁定

@Transactional

@Lock(LockModeType.PESSIMISTIC_WRITE)

@Query("select c from Account c where c.id = :accountId")

@QueryHints({

@QueryHint(name = "javax.persistence.lock.timeout", value = (LockOptions.NO_WAIT + ""))

})
public Account findByIdWithPessimisticNoWaitLocking(Long accountId);

在这种情况下,线程不会因为写操作释放锁而无限期阻塞。相反,它会在上述 javax.persistence.lock.timeout 之后立即返回锁获取失败。如果需要,我们也可以处理此异常并重试事务。

https://www.jdon.com/71719.html

相关推荐
用户0044729538256 小时前
练歌镜:让眼镜听懂“循环这一句”
javascript
yuyuxun19 小时前
协同过滤的校园二手物品智能推荐系统的设计与实现 毕业设计-附源码40082
javascript·vue.js·spring boot·python·数据分析·django·课程设计
总要学点什么12 小时前
第105篇:三维特效-流动图片墙体效果——让箭头沿墙长滚,它俩竟是同一份代码
开发语言·javascript·ecmascript·gis·webgl·材质·cesium
Agent世界14 小时前
用 CDP 做发布自动化:四道机器闸门与产物验收
前端·javascript
总要学点什么14 小时前
第104篇:三维特效-流动墙体效果——让贴图沿墙流动,它却只肯走一条对角线
开发语言·javascript·ecmascript·gis·webgl·材质·cesium
程序员大阳14 小时前
副队长ES6+教程(3)--解构赋值
前端·javascript·es6·赋值·解构
神膘护体小月半14 小时前
css - 元素背景图由 hover 触发,初次加载时闪动的解决办法
前端·javascript·css
程序员大阳15 小时前
副队长ES6+教程(2)--let 与 const
前端·javascript·es6·const·let·var
曦樂~16 小时前
【Web Development】03JavaScript 基础和事件
开发语言·前端·javascript
码云社区16 小时前
智慧物业系统选型指南:房屋档案、账单管理与报修协作
前端·javascript·vue.js·智慧物业