深入理解数据库事务(超详细)

一、事务的介绍

事务是一组操作的集合,事务会把所有操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。 

二、事务的基本操作

2.1  事务操作方式一

 例子:    转账场景(张三向李四转账)

  1. -- 1. 查询张三账户余额
  2. select * from account where name = '张三';
  3. -- 2. 将张三账户余额-1000
  4. update account set money = money - 1000 where name = '张三';
  5. -- 此语句出错后张三钱减少但是李四钱没有增加
  6. 模拟sql语句错误
  7. -- 3. 将李四账户余额+1000
  8. update account set money = money + 1000 where name = '李四';
  9. -- 查看事务提交方式
  10. SELECT @@AUTOCOMMIT;
  11. -- 设置事务提交方式,1为自动提交,0为手动提交,该设置只对当前会话有效
  12. SET @@AUTOCOMMIT = 0;
  13. -- 提交事务
  14. COMMIT;
  15. -- 回滚事务
  16. ROLLBACK;
  17. -- 设置手动提交后上面代码改为:
  18. select * from account where name = '张三';
  19. update account set money = money - 1000 where name = '张三';
  20. update account set money = money + 1000 where name = '李四';
  21. commit;

2.2  事务操作方式二

 开启事务:
  START TRANSACTION 或 BEGIN ;
提交事务:
  COMMIT;
回滚事务:
  ROLLBACK;

操作实例:

  1. start transaction;
  2. select * from account where name = '张三';
  3. update account set money = money - 1000 where name = '张三';
  4. update account set money = money + 1000 where name = '李四';
  5. commit;

2.3  实际开发的案例 

一个实际开发中常见的例子是银行系统中的转账业务。在进行资金转移时,我们需要保证转出和转入两个账户的金额变动是原子性的,要么全部成功,要么全部失败。

以下是一个示例代码片段:

// 假设有两个账户:accountA 和 accountB
String accountA = "A";
String accountB = "B";
double transferAmount = 100.0; // 转账金额

// 获取数据库连接
Connection connection = getConnection();
try {
    connection.setAutoCommit(false); // 设置手动提交事务

    // 查询账户 A 的余额
    double balanceA = queryBalance(accountA);

    // 查询账户 B 的余额
    double balanceB = queryBalance(accountB);

    if (balanceA >= transferAmount) { // 检查账户 A 的余额是否足够
        // 扣除账户 A 的金额
        updateBalance(connection, accountA, balanceA - transferAmount);

        // 增加账户 B 的金额
        updateBalance(connection, accountB, balanceB + transferAmount);

        connection.commit(); // 提交事务
        System.out.println("转账成功!");
    } else {
        System.out.println("转账失败:账户 A 余额不足!");
    }
} catch (SQLException e) {
    connection.rollback(); // 发生异常,回滚事务
    System.out.println("转账失败:" + e.getMessage());
} finally {
    connection.close(); // 关闭数据库连接
}

在上述示例中,我们使用connection.setAutoCommit(false)将自动提交事务的选项关闭,并手动控制事务的提交和回滚。当余额足够时,我们更新账户 A 和账户 B 的余额,并使用connection.commit()提交事务。如果发生异常或余额不足的情况,我们使用connection.rollback()回滚事务。

通过使用事务,我们可以确保转账过程中的数据一致性和可靠性。只有当两个账户的金额都成功更新后,才会执行事务的提交操作,否则会回滚到事务开始前的状态。

这是一个简单的示例,实际应用中可能涉及更多复杂的业务逻辑和错误处理。但这个例子展示了如何在实际开发中使用START TRANSACTIONBEGIN来管理事务,确保转账操作的一致性和可靠性。

2.4  Springboot事务的案例

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED)
    public void transferMoney(String fromUser, String toUser, double amount) {
        try {
            User from = userRepository.findByUsername(fromUser);
            User to = userRepository.findByUsername(toUser);

            // 检查余额是否足够
            if (from.getBalance() < amount) {
                throw new InsufficientBalanceException("Insufficient balance in the account");
            }

            // 扣除转出账户的金额
            from.setBalance(from.getBalance() - amount);
            userRepository.save(from);

            // 增加转入账户的金额
            to.setBalance(to.getBalance() + amount);
            userRepository.save(to);
        } catch (InsufficientBalanceException e) {
            // 处理余额不足的异常
            throw new TransactionFailedException("Transaction failed: " + e.getMessage());
        } catch (Exception e) {
            // 处理其他异常
            throw new TransactionFailedException("Transaction failed due to an unexpected error");
        }
    }
}

在上面的例子中,我们使用了更多的事务配置选项。@Transactional注解的propagation属性指定了事务的传播行为,这里使用的是Propagation.REQUIRED,表示如果当前没有事务,则创建一个新的事务;如果已经存在事务,则加入到当前事务中。isolation属性指定了事务的隔离级别,这里使用的是Isolation.READ_COMMITTED,表示读取已提交的数据。

在转账过程中,我们首先检查转出账户的余额是否足够,如果不足则抛出自定义的InsufficientBalanceException异常。然后,我们分别更新转出账户和转入账户的余额,并将它们保存到数据库中。如果在转账过程中发生了异常,我们会捕获并处理它们,然后抛出自定义的TransactionFailedException异常。

这个案例展示了如何在Spring Boot中使用事务来确保转账操作的原子性,并处理一些常见的异常情况。根据实际需求,你可以根据业务逻辑进行更多的定制和扩展。

@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED)@Transactional注解的参数配置,用于指定事务的传播行为和隔离级别。

  • propagation = Propagation.REQUIRED:表示如果当前没有事务,就创建一个新的事务;如果已经存在事务,则加入到当前事务中。这是最常用的传播行为,它确保了一组操作要么都成功要么都回滚。

  • isolation = Isolation.READ_COMMITTED:表示事务的隔离级别为"读取已提交"。在这个隔离级别下,一个事务只能读取到已提交的数据。这可以避免脏读(读取到未提交的数据)。

在上述例子中,@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED)将事务的传播行为设置为默认的Propagation.REQUIRED,并将隔离级别设置为Isolation.READ_COMMITTED。这意味着每次调用transferMoney方法时,将创建一个新的事务(如果没有现有事务),并且该事务只能读取到已提交的数据。

三、事务的四大特性

  • 原子性(Atomicity):事务是不可分割的最小操作但愿,要么全部成功,要么全部失败
  • 一致性(Consistency):事务完成时,必须使所有数据都保持一致状态
  • 隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行
  • 持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的

四、并发事务问题

问题 描述
脏读 一个事务读到另一个事务还没提交的数据
不可重复读 一个事务先后读取同一条记录,但两次读取的数据不同
幻读 一个事务按照条件查询数据时,没有对应的数据行,但是再插入数据时,又发现这行数据已经存在

常用的并发控制机制包括:

  1. 锁(Locking):使用锁来控制并发事务对数据的访问和修改,确保在某个事务读取或修改数据时,其他事务不能同时进行相同的操作。

  2. 事务隔离级别(Isolation Level):通过设置不同的事务隔离级别,定义了事务之间的可见性、并发控制的粒度等规则。

  3. 多版本并发控制(MVCC):每个事务在读取数据时,会看到一个特定的版本,而不是直接读取最新的数据。这样可以避免脏读、不可重复读和幻读问题。

  4. 时间戳排序(Timestamp Ordering):使用时间戳来对事务进行排序,根据时间戳来判断事务的执行顺序,确保事务按照正确的顺序读取和修改数据。

实际开发中,为了解决并发事务问题,需要根据具体情况选择适当的并发控制机制和事务隔离级别,并进行合理的设计和优化。同时,也需要进行充分的测试和验证,确保系统在高并发环境下依然能够保持数据的一致性和可靠性。

五、事务的隔离级别

并发事务隔离级别:

隔离级别 脏读 不可重复读 幻读
Read uncommitted(读未提交)
Read committed(读已提交) ×
Repeatable Read(可重复读)(默认的隔离级别) × ×
Serializable(串行化) × × ×
  • √表示在当前隔离级别下该问题会出现
  • Serializable 性能最低;Read uncommitted 性能最高,数据安全性最差

注意:事务隔离级别越高,数据越安全,但是性能越低。


 查看事务隔离级别:
  SELECT @@TRANSACTION_ISOLATION;
设置事务隔离级别:
  SET [ SESSION | GLOBAL ] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE };
     SESSION 是会话级别,表示只针对当前会话有效,GLOBAL 表示对所有会话有效

猜你喜欢

转载自blog.csdn.net/weixin_55772633/article/details/132131955
今日推荐