Java 事务

概念:

通常观念认为,事务仅与数据库相关。

事务必须服从 ISO/IEC 所制定的 ACID 原则。

[ISO/IEC] 国际制定标准的组织

[ACID] 原子性(atomicity)、一致性(consistency)、隔离性(isolation)、持久性(durability)的缩写

事务特性:原子性:事务执行过程中的任何失败都将导致事务所做的修改失效。对于数据修改,要么全部执行,要么全部不执行

一致性:当事务执行失败时,所有被该事务影响的数据都应该恢复到事务执行前的状态。事务执行前后,数据状态保持一致性(例如转账,转账之后两人总金额不变)

隔离性:在事务执行过程中对数据的修改,在事务提交之前对其他事务不可见。一个事务的处理不能影响另一个事务的处理

持久性:事务处理结束,其效果在数据库中持久化。即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作通俗理解,事务是一组原子操作单元,从数据库角度说,就是一组 SQL 指令。

事务是为了解决数据安全操作提出的,事务控制实际上就是控制数据的安全访问。

事务的类型:

JDBC 事务

用 Connection 对象控制,JDBC 的 Connection 接口提供了两种事务模式:自动提交、手工提交。

Connection : java.sql.Connection

JDBC 为使用 Java 进行数据库的事务操作提供了最基本的支持。可以将多个 SQL 语句放到同一个事务中,保证其 ACID 特性。优点: API 比较简单,可以实现最基本的事务操作,性能也相对较好。

缺点:事务的范围局限于一个数据库连接。一个 JDBC 事务不能跨越多个数据库。

涉及到多数据库的操作或者分布式场景,JDBC事务就无能为力了

简单代码实现:

public void JdbcTransfer() {
java.sql.Connection conn = null;
try{
String url = "jdbc:mysql://localhost:3306/test?user=root&password=root"; //定义连接数据库的url
conn = DriverManager.getConnection(url);
// 将自动提交设置为 false,
//若设置为 true 则数据库将会把每一次数据更新认定为一个事务并自动提交
conn.setAutoCommit(false);

// 获取一个执行、发送 SQL 语句的对象
stmt = conn.createStatement();
// 将 A 账户中的金额减少 500
stmt.execute("\
update t_account set amount = amount - 500 where account_id = 'A'");
// 将 B 账户中的金额增加 500
stmt.execute("\
update t_account set amount = amount + 500 where account_id = 'B'");

// 提交事务
conn.commit();
// 事务提交:转账的两步操作同时成功
} catch(SQLException sqle){
try{
// 发生异常,回滚在本事务中的操作
conn.rollback();
// 事务回滚:转账的两步操作完全撤销
stmt.close();
conn.close();
}catch(Exception ignore){
ignore.printStackTrace();
}
sqle.printStackTrace();
}
}

JTA (Java Transaction API)事务

JTA 是一种高层的,与实现无关的,与协议无关的 API,应用程序和应用服务器可以使用 JTA来访问事务。可以理解为 JTA 是我们使用事务必要的工具,可以帮我们解决分布式事务的问题。

如果计划用 JTA 界定事务,需要有一个实现 javax.sql.XADataSource、javax.sql.XAConnection和 javax.sql.XAResource 接口的 JDBC 驱动程序。

这个实现这些接口的驱动程序可以参与 JTA 事务。

一个 XADataSource 对象就是一个 XAConnection 对象的工厂,XAConnection 是参与 JTA 事务的 JDBC 连接。J2EE 应用程序使用 JNDI 查询数据源。

JNDI:是 J2EE 里面一个重要的规范,使用 JNDI 不用代码写一长串去连接数据库,只需要引入使用数据库的配置文件,便于解耦合。

使用 JTA 事务必须使用 XADataSource 来产生数据库连接,产生的连接为 XA 连接。

符合 XA 规范的相关接口类才能使用 JTA 事务。

XA(javax.sql.XAConnection)和非XA(javax.sql.Connection)连接的区别在于:XA可以参与JTA事务,而且不支持自动提交

相关代码引用:

public void JtaTransfer() {
javax.transaction.UserTransaction tx = null;
java.sql.Connection conn = null;
try{
tx = (javax.transaction.UserTransaction) context.lookup("java:comp/UserTransaction"); //取得JTA事务,本例中是由Jboss容器管理
javax.sql.DataSource ds = (javax.sql.DataSource) context.lookup("java:/XAOracleDS"); //取得数据库连接池,必须有支持XA的数据库、驱动程序
tx.begin();
conn = ds.getConnection();

// 将自动提交设置为 false,
//若设置为 true 则数据库将会把每一次数据更新认定为一个事务并自动提交
conn.setAutoCommit(false);

stmt = conn.createStatement();
// 将 A 账户中的金额减少 500
stmt.execute("\
update t_account set amount = amount - 500 where account_id = 'A'");
// 将 B 账户中的金额增加 500
stmt.execute("\
update t_account set amount = amount + 500 where account_id = 'B'");

// 提交事务
tx.commit();
// 事务提交:转账的两步操作同时成功
} catch(SQLException sqle){
try{
// 发生异常,回滚在本事务中的操做
tx.rollback();
// 事务回滚:转账的两步操作完全撤销
stmt.close();
conn.close();
}catch(Exception ignore){

}
sqle.printStackTrace();
}

}JTA 提供了UserTransaction用来处理事务,不过不光依靠这个类就能将 JDBC 操作转换为 JTA 操作,还是需要符合 XA 协议的接口。

JTA 优点很明显,可以解决分布式事务,可是有很多缺点,实现复杂、笨重、限制代码复用性。

容器事务

容器事务主要是 J2EE 应用服务器提供的,容器事务大多是基于 JTA 完成,这是一个基于 JNDI 的,相当复杂的 API 实现。

通过 EJB 容器提供的容器事务管理机制(CMT)完成功能。ETJ: 将Java Beans的运行正式从客户端领域扩展到服务器领域

声明式事务

通过 AOP(面向切面)方式在方法前使用编程式事务的方法开启事务,在方法后提交或回滚。用配置文件的方法或注解方法(如:@Transactional)控制事务。注解在方法上是方法自动启动事务,在类上是整个类中的方法都使用事务

Spring 开启事务注解

SpringBoot 事务处理

只需要简单的 @Transactional 注解可实现

简单代码实现:

@Service
public class PersonService {
@Resource
private PersonMapper personMapper;

@Resource
private CompanyMapper companyMapper;
//rollbackFor:触发回滚的异常,默认是RuntimeException和Error
//isolation: 事务的隔离级别,默认是Isolation.DEFAULT也就是数据库自身的默认隔离级别
@Transactional(rollbackFor = {RuntimeException.class, Error.class})
public void saveOne(Person person) {
Company company = new Company();
company.setName("tenmao:" + person.getName());
companyMapper.insertOne(company);
personMapper.insertOne(person);
}
}

编程式事务

手动开启、提交、回滚事务。

Spring 编程式事务实现需要在配置文件中配置相应的事务处理器,用 AOP、事务标签、注解方式实现事务。

例如 JDBC 事务处理器

由于我们使用较多的是 SpringBoot 开发,这里就不多对 Spring 配置文件进行更多的阐述了。

无论是 Spring 还是 SpringBoot 实现编程式事务都需要在实体类中获取事务处理器对象,手动进行开启事务和提交事务各种操作。

SpringBoot 简单代码实现:

TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
for (int i = 0; i < 10; i++) {
Post post = new Post();
if (i == 5) { post.setContent("dddddddddddddddddddddddddddddddddddddddddddd");
} else
post.setContent("post" + i);
post.setWeight(i);
postService.save(post);

}
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
e.printStackTrace();
}

事务的属性

事务的隔离级别 (Transaction Isolation Levels)

JDBC 对事务的支持

JDBC 提供了5种不同的事务隔离级别,在 Connection 中进行了定义。

TRANSACTION_NONE JDBC 驱动不支持事务

TRANSACTION_READ_UNCOMMITTED 允许脏读、不可重复读和幻读。

TRANSACTION_READ_COMMITTED 禁止脏读,但允许不可重复读和幻读。

TRANSACTION_REPEATABLE_READ 禁止脏读和不可重复读,单运行幻读。

TRANSACTION_SERIALIZABLE 禁止脏读、不可重复读和幻读。Connection 提供了一个 auto-commit 的属性来指定事务何时结束。

当 auto-commit 为 true 时,当每个独立 SQL 操作的执行完毕,事务立即自动提交,也就是说每个 SQL 操作都是一个事务。

当 auto-commit 为 false 时,每个事务都必须显示调用 commit 方法进行提交,或者显示调用 rollback 方法进行回滚。auto-commit 默认为 true。

保存点(SavePoint):JDBC 定义了 SavePoint 接口,提供在一个更细粒度的事务控制机制。当设置了一个保存点后,可以 rollback 到该保存点处的状态,而不是 rollback 整个事务。

Connection接口的 setSavepoint 和 releaseSavepoint 方法可以设置和释放保存点。

JDBC 规范虽然定义了事务的以上支持行为,但是各个 JDBC 驱动,数据库厂商对事务的支持程度可能各不相同。

事务的传播行为

PROPAGATION_REQUIRED: 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。

PROPAGATION_SUPPORTS: 支持当前事务,如果当前没有事务,就以非事务方式执行。


PROPAGATION_MANDATORY: 支持当前事务,如果当前没有事务,就抛出异常。


PROPAGATION_REQUIRES_NEW:新建事务,如果当前存在事务,把当前事务挂起。


PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。


PROPAGATION_NEVER: 以非事务方式执行,如果当前存在事务,则抛出异常。


虽然有7种,但是常用的就第一种REQUIRED和第四种REQUIRES_NEW

事务并发处理可能引起的问题

脏读(dirty read):一个事务读取了另一个事务尚未提交的数据

不可重复读(non-repeatable read):一个事务的操作导致另一个事务前后两次读取到不同的数据

幻读(phantom read):一个事务的操作导致另一个事务前后两次查询的结果数据量不同不可重复读是同一数据不一样,幻读是数据量不一样