实际案例:如何在银行转账操作中避免死锁

时间:2024-12-19 12:15:51

实际案例:如何在银行转账操作中避免死锁

目录

  1. 引言
  2. 案例背景
  3. 死锁问题分析
  4. 解决方案设计
    • 4.1 按固定顺序访问资源
    • 4.2 使用悲观锁
    • 4.3 涉及重试机制
  5. 完整代码实现
    • 5.1 数据库表设计
    • 5.2 Entity类
    • 5.3 Repository接口
    • 5.4 Service类
    • 5.5 Controller类
  6. 图例说明
  7. 结论

1. 引言

在银行业务场景中,通常会涉及到多个账户间的转账操作。当并发操作较多时,可能导致死锁问题。为了确保转账业务的正常进行,避免死锁显得尤为重要。本文将通过实际案例,详细说明如何在银行转账操作中有效避免死锁。

2. 案例背景

假设我们有一个银行系统,用户可以在不同账户之间进行转账。由于系统中存在大量的并发操作,我们需要采取措施避免由于争用资源而产生的死锁,以确保系统稳定运行。

3. 死锁问题分析

在转账操作中,典型的死锁情况如下图所示:

事务A: 更新账户A -> 等待更新账户B
事务B: 更新账户B -> 等待更新账户A

如果事务A先锁定账户A,然后事务B锁定账户B,并且各自等待对方释放其持有的锁,就会形成死锁。这将导致这两个事务互相等待,无法继续执行。

下面的图1展示了死锁的状况:

图1:死锁情况
+---------------+         +--------------+
|   事务A       |         |    事务B      |
|               |         |              |
| 锁住账户A     | ---+    | 锁住账户B    | ---+
|   等待账户B   |    |    | 等待账户A    |    |
+---------------+    |    +--------------+    |
                      |                      |
                      +----------------------+

4. 解决方案设计

4.1 按固定顺序访问资源

为了避免死锁,我们可以按照固定顺序访问资源。例如,所有事务在访问账户资源时,总是按照账户ID的字典顺序进行访问。这可以有效避免循环等待的问题。假如对账户按照ID排序后,事务A和事务B都按顺序访问账户,则不会出现死锁。

图2:按顺序访问资源避免死锁
+---------------+         +--------------+
|   事务A       |         |    事务B      |
|               |         |              |
| 锁住账户A     |         | 锁住账户A    |
| 锁住账户B     |         | 锁住账户B    |
+---------------+         +--------------+

4.2 使用悲观锁

通过使用悲观锁,可以确保在读取数据时即加锁,防止其他事务同时访问该数据。这样虽然增加了一些锁的开销,但可以有效降低死锁发生的概率。在代码实现中,我们通过数据库的悲观锁定机制(如SELECT ... FOR UPDATE)来实现。

4.3 涉及重试机制

考虑到某些情况下可能依然会出现死锁,可以在检测到死锁时,进行一定次数的重试,以确保操作最终完成。通常重试机制会设置一个限时,以避免无限次重试。

5. 完整代码实现

以下是Spring框架下使用Java实现银行转账操作避免死锁的完整代码,并对关键部分做详细说明:

5.1 数据库表设计

CREATE TABLE Account (
    id VARCHAR(20) NOT NULL PRIMARY KEY,
    balance DOUBLE NOT NULL,
    version INT NOT NULL
);

表格Account存储了银行账户信息,包括账户ID、余额和版本号。

5.2 Entity类

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Version;

@Entity
public class Account {
    
    @Id
    private String id;
    private double balance;
    
    @Version
    private int version;

    // getters and setters
}

实体类Account映射到数据库表,其中包括账户ID、余额及版本信息。@Version注解用于乐观锁实现。

5.3 Repository接口

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import javax.persistence.LockModeType;
import java.util.Optional;

public interface AccountRepository extends JpaRepository<Account, String> {

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT a FROM Account a WHERE a.id = :id")
    Optional<Account> findByIdWithLock(@Param("id") String id);
}

AccountRepository接口定义了用于查询并锁定账户的方法findByIdWithLock,该方法使用了悲观锁,确保其他事务不能同时访问该数据。

5.4 Service类

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class TransferService {

    @Autowired
    private AccountRepository accountRepository;

    /**
     * 转账操作,按账户ID顺序访问资源,避免死锁
     */
    @Transactional
    public void transferMoney(String fromAccountId, String toAccountId, double amount) {
        if (fromAccountId.compareTo(toAccountId) < 0) {
            updateAccounts(fromAccountId, toAccountId, amount);
        } else {
            updateAccounts(toAccountId, fromAccountId, -amount);
        }
    }

    private void updateAccounts(String accountId1, String accountId2, double amount) {
        Account account1 = accountRepository.findByIdWithLock(accountId1).orElseThrow();
        Account account2 = accountRepository.findByIdWithLock(accountId2).orElseThrow();

        account1.setBalance(account1.getBalance() - amount);
        accountRepository.save(account1);

        account2.setBalance(account2.getBalance() + amount);
        accountRepository.save(account2);
    }

    /**
     * 重试机制,以应对死锁
     */
    public void executeTransferWithRetries(String fromAccountId, String toAccountId, double amount, int maxRetries) {
        int attempt = 0;
        while (true) {
            try {
                transferMoney(fromAccountId, toAccountId, amount);
                break;
            } catch (DeadlockLoserDataAccessException e) {
                if (++attempt > maxRetries) {
                    throw e;
                }
                // 在重试之前加一些延迟,以避免立即再次冲突
                try {
                    Thread.sleep(100);
                } catch (InterruptedException ex) {
                    Thread.currentThread().interrupt();
                    throw new RuntimeException(ex);
                }
            }
        }
    }
}

TransferService类中,

  • transferMoney方法按顺序访问账户资源。
  • executeTransferWithRetries方法包含了重试机制,如果发生死锁,最多尝试maxRetries次。

5.5 Controller类

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TransferController {

    @Autowired
    private TransferService transferService;

    @PostMapping("/transfer")
    public String transferMoney(@RequestParam String fromAccountId, @RequestParam String toAccountId, @RequestParam double amount) {
        try {
            transferService.executeTransferWithRetries(fromAccountId, toAccountId, amount, 3);
            return "Transfer successful";
        } catch (Exception e) {
            return "Transfer failed: " + e.getMessage();
        }
    }
}

TransferController类中,

  • transferMoney方法接收HTTP POST请求并调用TransferService进行转账操作。

6. 图例说明

以下两个图例分别展示了死锁情况和通过按顺序访问资源解决死锁的示意图。

图1:死锁情况

+---------------+         +--------------+
|   事务A       |         |    事务B      |
|               |         |              |
| 锁住账户A     | ---+    | 锁住账户B    | ---+
|   等待账户B   |    |    | 等待账户A    |    |
+---------------+    |    +--------------+    |
                      |                      |
                      +----------------------+

图2:按顺序访问资源避免死锁

+---------------+         +--------------+
|   事务A       |         |    事务B      |
|               |         |              |
| 锁住账户A     |         | 锁住账户A    |
| 锁住账户B     |         | 锁住账户B    |
+---------------+         +--------------+

7. 结论

通过上述案例,我们展示了如何在银行转账操作中有效避免死锁问题。主要方法包括按固定顺序访问资源、使用悲观锁以及在必要时进行重试机制。这些措施能够显著减少死锁的发生,确保系统的高效稳定运行。在实际应用中,还需根据具体业务需求和系统特性,灵活调整和优化解决方案。