乐观锁是一种确保多个事务不会相互覆盖更改的机制。这是通过在实体中维护一个版本号来实现的,该版本号在每次事务中都会被检查并更新。如果两个事务尝试同时更新同一个实体,其中一个事务将因OptimisticLockException
而失败。
在企业应用中,对数据库的并发访问至关重要。应用程序必须在不锁定的情况下独立执行事务,以确保数据的完整性和一致性。乐观锁允许两个线程在不阻塞彼此的情况下读取或修改同一行数据。然而,这种方法也存在一个问题需要考虑。
例如,Alice 有一个银行账户,余额为 32,000 美元。她希望通过移动应用程序从另一个账户转账 10,000 美元到她的账户。与此同时,Sarah 意识到她忘记支付本月的房租,并决定通过 ATM 支付 1,000 美元。她们同时向服务器提交了请求,分别为 Alice 创建了事务 A,为 Sarah 创建了事务 B。
事务 A 和事务 B 可以同时修改数据。由于 Sarah 的事务,事务 B 将 Alice 的账户余额从 32,000 美元更新为 33,000 美元。然而,Alice 仍然看到她的余额为 32,000 美元,并假设她将其从 32,000 美元更新为 42,000 美元。这是正确的吗?嗯,实际上并非如此。Alice 将她的余额从 33,000 美元更新为 43,000 美元,因为事务尚未提交更改。这种现象被称为陈旧数据。
我们期望 Alice 将她的银行账户余额从 32,000 美元更新为 42,000 美元,因为她存入了 10,000 美元。然而,由于 Sarah 同时存入了 1,000 美元,Alice 的余额实际上从 32,000 美元更新为 33,000 美元。后来,当 Alice 检查她的余额时,她假设它从 32,000 美元更新为 42,000 美元,但实际上由于 Sarah 的事务,它从 33,000 美元更新为 43,000 美元。
如何解决这个问题?我们可以通过添加版本列来防止第二个事务(用户)更新陈旧数据。乐观锁通过检查版本列来检测数据的变化。如果服务有许多读写操作,这种方法非常合适。我们可以在 Spring Boot 中使用 @Version
注解来实现这一点。
让我们通过一个 BankAccount
实体的示例来演示乐观锁的实现,其中多个请求尝试同时更新账户余额。
1. 创建一个 Spring Boot 项目
使用 Spring Initializr 创建一个新的 Spring Boot 项目,并添加以下依赖项:
- • Spring Web
- • Spring Data JPA
- • H2 数据库(为了简化)
- • Lombok(可选)
2. 定义 BankAccount
实体
BankAccount
实体将包含一个 @Version
字段来实现乐观锁。
@Entity
public class BankAccount {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String accountHolderName;
private BigDecimal balance;
@Version
private int version;
// 构造方法、Getter 和 Setter
public BankAccount() {}
public BankAccount(String accountHolderName, BigDecimal balance) {
this.accountHolderName = accountHolderName;
this.balance = balance;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getAccountHolderName() {
return accountHolderName;
}
public void setAccountHolderName(String accountHolderName) {
this.accountHolderName = accountHolderName;
}
public BigDecimal getBalance() {
return balance;
}
public void setBalance(BigDecimal balance) {
this.balance = balance;
}
public int getVersion() {
return version;
}
public void setVersion(int version) {
this.version = version;
}
}
3. 创建 BankAccountRepository
@Repository
public interface BankAccountRepository extends JpaRepository<BankAccount, Long> {
}
4. 创建 BankAccountService
@Service
public class BankAccountService {
@Autowired
private BankAccountRepository bankAccountRepository;
@Transactional
public BankAccount deposit(Long accountId, BigDecimal amount) {
BankAccount bankAccount = bankAccountRepository.findById(accountId)
.orElseThrow(() -> new RuntimeException("Bank account not found"));
bankAccount.setBalance(bankAccount.getBalance().add(amount));
return bankAccountRepository.save(bankAccount);
}
}
5. 创建 BankAccountController
@RestController
@RequestMapping("/accounts")
public class BankAccountController {
@Autowired
private BankAccountService bankAccountService;
@PutMapping("/{id}/deposit")
public BankAccount deposit(@PathVariable Long id, @RequestParam BigDecimal amount) {
return bankAccountService.deposit(id, amount);
}
}
6. 模拟并发请求
让我们模拟两个同时发生的存款请求:
- 1. 初始状态:假设
BankAccount
实体id = 1
的数据如下:
- •
accountHolderName
: “Alice” - •
balance
:1000.00
- •
version
:0
- 2. 请求 1:
- • 输入:存款
500.00
。 - • 端点:
PUT /accounts/1/deposit?amount=500.00
- 3. 请求 2(在请求 1 完成之前到达):
- • 输入:存款
300.00
。 - • 端点:
PUT /accounts/1/deposit?amount=300.00
- 4. 结果:
- • 请求 1 更新了
BankAccount
实体,balance
变为1500.00
,version
增加到1
。 - • 请求 2 尝试更新
BankAccount
实体,但由于它读取的version
(0
)与当前version
(1
)不匹配,因此失败并抛出OptimisticLockException
。
7. 示例输入和输出
数据库中的初始数据:INSERT INTO BANK_ACCOUNT (id, account_holder_name, balance, version) VALUES (1, 'Alice', 1000.00, 0);
请求 1 输入:PUT /accounts/1/deposit?amount=500.00
请求 1 输出:
{
"id": 1,
"accountHolderName": "Alice",
"balance": 1500.00,
"version": 1
}
请求 2 输入:PUT /accounts/1/deposit?amount=300.00
请求 2 输出(异常):
{
"timestamp": "2024-09-01T12:34:56.789+00:00",
"status": 409,
"error": "Conflict",
"message": "Optimistic lock exception occurred",
"path": "/accounts/1/deposit"
}
8. 处理 OptimisticLockException
为了处理该异常,可以创建一个全局异常处理器:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(OptimisticLockException.class)
@ResponseStatus(HttpStatus.CONFLICT)
public String handleOptimisticLockException(OptimisticLockException e) {
return "Optimistic lock exception occurred: " + e.getMessage();
}
}
总结
在这个示例中,我们使用 JPA 的 @Version
注解在 Spring Boot 应用程序中实现了乐观锁,并通过 BankAccount
实体演示了两个同时发生的存款请求如何导致 OptimisticLockException
。这种情况在金融系统中尤为重要,因为账户余额的一致性至关重要。
没有回复内容