文章目录
一.关于事务
1.事务概念
事务(Transaction) 是数据库操作中的一个基本概念,指的是一组作为单个工作单元执行的操作。这些操作要么全部执行成功并提交(commit),要么全部失败并回滚(rollback),确保数据库的一致性、完整性和可靠性。
事务通常应用于多步骤的数据库操作中,以确保数据在各种操作中的一致性,特别是在出现系统故障、错误或并发操作时。事务可以跨越多个SQL语句,这些语句共同构成一个完整的任务。
2.事务四个基本特性
事务的四个基本特性(ACID),事务的核心特性可以用 ACID 四个字母来概括:
- 原子性(Atomicity):
事务中的所有操作要么全部成功,要么全部失败回滚。如果在事务执行过程中出现任何错误,已经执行的操作必须撤销(回滚),数据库恢复到事务开始前的状态。 - 一致性(Consistency):
事务开始前和结束后,数据库必须处于一致的状态。事务的执行不能破坏数据库的规则(如约束、触发器等),任何违反数据库一致性规则的操作都会导致事务失败并回滚。 - 隔离性(Isolation):
事务的执行过程与其他事务是隔离的,一个事务的执行不应受到其他事务的干扰。并发事务互不影响,每个事务的中间状态对其他事务不可见。
数据库系统提供不同的隔离级别(如读未提交、读已提交、可重复读、可串行化)来控制并发事务之间的相互影响。 - 持久性(Durability):
一旦事务提交,它对数据库的修改是永久的,即使系统发生故障也不会丢失。数据库系统通常会使用日志、备份等机制确保事务的持久性。
3. 事务的生命周期
- 开始事务:
通过客户端或应用程序显式发起一个事务。在这期间执行的所有操作都被认为是这个事务的一部分。 - 执行操作:
事务中执行多个SQL语句,如插入、更新、删除等。 - 提交事务:
如果所有操作都成功,事务提交,数据库将保存这些操作的结果,数据状态更新。 - 回滚事务:
如果事务中某些操作失败或发生错误,事务回滚,数据库将撤销已经执行的操作,恢复到事务开始前的状态。
4.事务的隔离级别
为了控制多个事务并发执行时的互相影响,数据库提供了多种隔离级别:
读未提交(Read Uncommitted):最低的隔离级别,允许一个事务读取另一个事务未提交的数据。这可能导致脏读、不可重复读和幻读问题。
读已提交(Read Committed):允许一个事务只能读取已经提交的其他事务的数据。这解决了脏读的问题,但仍可能存在不可重复读和幻读问题。
可重复读(Repeatable Read):保证一个事务在执行期间多次读取相同的数据时,其结果是一致的。解决了脏读、不可重复读的问题,但仍可能存在幻读问题。
串行化(Serializable):提供最高的隔离级别,通过强制事务串行执行来解决所有并发问题,包括脏读、不可重复读和幻读。虽然能够确保数据的完全一致性,但也导致了性能上的损失。
MySQL默认的隔离级别是可重复读(Repeatable Read)。在这个级别下,事务在读取数据时,会锁定读取的数据,防止其他事务对这些数据进行修改,直到当前事务提交或回滚。这意味着在一个事务中,读取的数据保持一致性,并且在事务结束之前不会被其他事务修改。
5.事务的应用场景
- 银行转账:A用户向B用户转账,涉及A账户扣款和B账户入账,这两步操作必须作为一个事务,确保要么两步都成功,要么都失败。
- 订单处理:在电商系统中,生成订单和扣除库存往往需要保证原子性,避免出现生成了订单但库存未更新的情况。
举例说明事务:事务中的两步操作,从账户A中扣除100元。向账户B中添加100元。这两步操作必须放在一个事务中。如果第一步成功,第二步失败(比如网络错误),数据库应回滚第一步的操作,恢复到未扣除100元的状态。
BEGIN TRANSACTION;
-- 第一步:从A账户扣除100元
UPDATE accounts SET balance = balance - 100 WHERE user_id = 'A';
-- 第二步:向B账户添加100元
UPDATE accounts SET balance = balance + 100 WHERE user_id = 'B';
-- 如果两步操作都成功,提交事务
COMMIT;
-- 如果任何一步失败,回滚事务
ROLLBACK;
事务的核心作用是通过ACID特性,确保数据操作在各种可能的异常情况下仍保持一致性和可靠性,防止数据错误和损坏。通过事务的使用,开发者可以更加自信地处理复杂的数据库操作,确保数据在不同步骤间保持一致。
二.回顾原生JDBC手动管理事务
为什么需要完全记住并且背熟JDBC流程? 因为这样去看Spring声明式事务或者去了解分布式事务,能让我们抓住主线,关注到重要的东西。
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/xiaofa_lawyer";
String user = "root";
String password = "123456";
Connection conn = null;
PreparedStatement pstmt1 = null;
PreparedStatement pstmt2 = null;
try {
// 1. 建立数据库连接
conn = DriverManager.getConnection(url, user, password);
// 2. 关闭自动提交,开始手动管理事务
conn.setAutoCommit(false);
// 3. 执行SQL操作
String sql1 = "INSERT INTO t_user1 (name) VALUES (?)";
pstmt1 = conn.prepareStatement(sql1);
pstmt1.setString(1, "jinbiao111");
pstmt1.executeUpdate();
String sql2 = "INSERT INTO t_user2 (name) VALUES (?)";
pstmt2 = conn.prepareStatement(sql2);
pstmt2.setString(1, "jinbiao222");
pstmt2.executeUpdate();
// 4. 如果SQL操作成功,提交事务
conn.commit();
System.out.println("事务提交成功。");
} catch (SQLException e) {
// 5. 如果发生异常,回滚事务
if (conn != null) {
try {
System.out.println("发生错误,回滚事务。");
conn.rollback();
} catch (SQLException rollbackEx) {
rollbackEx.printStackTrace();
}
}
e.printStackTrace();
} finally {
// 6. 关闭资源
try {
if (pstmt1 != null) pstmt1.close();
if (pstmt2 != null) pstmt2.close();
if (conn != null) conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
测试结果,这里因为没有异常,对两张表都插入成功,日志提示:“事务提交成功。”
三.Spring编程式事务
在Spring中,除了通过声明式事务(使用注解或XML配置)管理事务外,还可以使用编程式事务管理。编程式事务允许开发者在代码中通过API显式控制事务的开始、提交和回滚操作。Spring 提供了 TransactionTemplate 和 PlatformTransactionManager 两种主要的方式来实现编程式事务管理。
1.使用 TransactionTemplate 进行编程式事务管理
TransactionTemplate 是Spring中推荐使用的方式,能够简化事务管理,它通过模板模式封装了事务处理逻辑。
@Autowired
private PlatformTransactionManager platformTransactionManager;
/**
* 编程式事务1:使用 TransactionTemplate 进行编程式事务管理
*/
public void programmingTransaction1() {
TransactionTemplate transactionTemplate = new TransactionTemplate(platformTransactionManager);
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
try {
// 执行业务逻辑
jdbcTemplate.update("insert into t_user1 (name) values (?)", "jinbiao333");
log.info("事务中执行的操作");
// 你可以根据特定条件回滚事务
if (someCondition()) {
log.info("事务回滚");
status.setRollbackOnly(); // 手动回滚事务
}
} catch (Exception e) {
log.info("事务回滚");
status.setRollbackOnly(); // 发生异常时自动回滚
throw e;
}
}
});
}
/**
* 判断满足某个条件事务回滚
* @return
*/
private boolean someCondition() {
return true;
}
2.使用 PlatformTransactionManager 进行编程式事务管理
/**
* 编程式事务2:使用 PlatformTransactionManager 进行编程式事务管理
*/
public void programmingTransaction2() {
//定义一个数据源
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setDriverClassName("com.mysql.jdbc.Driver");
druidDataSource.setUrl("jdbc:mysql://localhost:3306/xiaofa_lawyer");
druidDataSource.setUsername("root");
druidDataSource.setPassword("123456");
//定义一个JdbcTemplate,用来方便执行数据库增删改查
JdbcTemplate jdbcTemplate = new JdbcTemplate(druidDataSource);
//1.定义事务管理器,给其指定一个数据源(可以把事务管理器想象为一个人,这个人来负责事务的控制操作)
PlatformTransactionManager platformTransactionManager = new DataSourceTransactionManager(druidDataSource);
//2.定义事务属性:TransactionDefinition,TransactionDefinition可以用来配置事务的属性信息,比如事务隔离级别、事务超时时间、事务传播方式、是否是只读事务等等。
TransactionDefinition transactionDefinition = new DefaultTransactionDefinition();
//3.获取事务:调用platformTransactionManager.getTransaction开启事务操作,得到事务状态(TransactionStatus)对象
TransactionStatus transactionStatus = platformTransactionManager.getTransaction(transactionDefinition);
//4.执行业务操作,下面就执行2个插入操作
try {
log.info("before:{}", jdbcTemplate.queryForList("SELECT * from t_user1"));
jdbcTemplate.update("insert into t_user1 (name) values (?)", "jinbiao555");
jdbcTemplate.update("insert into t_user2 (name) values (?)", "jinbiao666");
//5.提交事务:platformTransactionManager.commit
platformTransactionManager.commit(transactionStatus);
} catch (Exception e) {
//6.回滚事务:platformTransactionManager.rollback
platformTransactionManager.rollback(transactionStatus);
}
log.info("after:{}", jdbcTemplate.queryForList("SELECT * from t_user1"));
}
编程式事务过程简化了一下,如下:
- 定义事务属性信息:TransactionDefinition transactionDefinition = new DefaultTransactionDefinition();
- 定义事务管理器:PlatformTransactionManager platformTransactionManager = new DataSourceTransactionManager(dataSource);
- 获取事务:TransactionStatus transactionStatus = platformTransactionManager.getTransaction(transactionDefinition);
- 执行sql操作:比如上面通过JdbcTemplate的各种方法执行各种sql操作
- 提交事务(platformTransactionManager.commit)或者回滚事务(platformTransactionManager.rollback).
**PlatformTransactionManager接口有多个实现类,用来应对不同的环境。**比如你操作db用的是hibernate或者mybatis,那么用到的事务管理器是不一样的,常见的事务管理器实现有下面几个:
- JpaTransactionManager:如果你用jpa来操作db,那么需要用这个管理器来帮你控制事务。
- DataSourceTransactionManager:如果你用是指定数据源的方式,比如操作数据库用的是:JdbcTemplate、mybatis、ibatis,那么需要用这个管理器来帮你控制事务。
- HibernateTransactionManager:如果你用hibernate来操作db,那么需要用这个管理器来帮你控制事务。
- JtaTransactionManager:如果你用的是java中的jta来操作db,这种通常是分布式事务,此时需要用这种管理器来控制事务。
我们的案例中使用的是JdbcTemplate来操作db,所以用的是DataSourceTransactionManager这个管理器。
事务传播行为和隔离级别
在编程式事务中,可以通过 TransactionDefinition 设置事务的传播行为和隔离级别:
- 传播行为:
- PROPAGATION_REQUIRED(默认):加入现有事务或创建新事务。
- PROPAGATION_REQUIRES_NEW:创建一个新事务,暂停当前事务。
- PROPAGATION_SUPPORTS:支持现有事务,但如果没有事务,则以非事务方式执行。
四.编程式事务的应用场景
- 需要灵活控制事务边界,尤其是在一个方法中根据不同条件决定是否提交或回滚事务。
- 需要更复杂的事务逻辑,比如多个数据源之间的事务处理。
相比于声明式事务,编程式事务虽然更灵活,但也更复杂。一般情况下,推荐使用声明式事务,只有在需要精细控制事务时才使用编程式事务。
五.总结
了解原生JDBC手动管理事务、Spring编程式事务,对我们学习Spring声明式事务,分布式事务还是有很大帮助的~