一、事务的介绍
事务是一组操作的集合,事务会把所有操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。
二、事务的基本操作
2.1 事务操作方式一
例子: 转账场景(张三向李四转账)
-- 1. 查询张三账户余额
select * from account where name = '张三';
-- 2. 将张三账户余额-1000
update account set money = money - 1000 where name = '张三';
-- 此语句出错后张三钱减少但是李四钱没有增加
模拟sql语句错误
-- 3. 将李四账户余额+1000
update account set money = money + 1000 where name = '李四';
-- 查看事务提交方式
SELECT @@AUTOCOMMIT;
-- 设置事务提交方式,1为自动提交,0为手动提交,该设置只对当前会话有效
SET @@AUTOCOMMIT = 0;
-- 提交事务
COMMIT;
-- 回滚事务
ROLLBACK;
-- 设置手动提交后上面代码改为:
select * from account where name = '张三';
update account set money = money - 1000 where name = '张三';
update account set money = money + 1000 where name = '李四';
commit;
2.2 事务操作方式二
开启事务: START TRANSACTION 或 BEGIN ;
提交事务: COMMIT;
回滚事务: ROLLBACK;
操作实例:
start transaction;
select * from account where name = '张三';
update account set money = money - 1000 where name = '张三';
update account set money = money + 1000 where name = '李四';
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 TRANSACTION
或BEGIN
来管理事务,确保转账操作的一致性和可靠性。
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):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的
四、并发事务问题
问题 | 描述 |
---|---|
脏读 | 一个事务读到另一个事务还没提交的数据 |
不可重复读 | 一个事务先后读取同一条记录,但两次读取的数据不同 |
幻读 | 一个事务按照条件查询数据时,没有对应的数据行,但是再插入数据时,又发现这行数据已经存在 |
常用的并发控制机制包括:
-
锁(Locking):使用锁来控制并发事务对数据的访问和修改,确保在某个事务读取或修改数据时,其他事务不能同时进行相同的操作。
-
事务隔离级别(Isolation Level):通过设置不同的事务隔离级别,定义了事务之间的可见性、并发控制的粒度等规则。
-
多版本并发控制(MVCC):每个事务在读取数据时,会看到一个特定的版本,而不是直接读取最新的数据。这样可以避免脏读、不可重复读和幻读问题。
-
时间戳排序(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 表示对所有会话有效