Java 事务
概念:
通常观念认为,事务仅与数据库相关。
事务必须服从 ISO/IEC 所制定的 ACID 原则。
事务特性:
-
原子性:事务执行过程中的任何失败都将导致事务所做的修改失效。
对于数据修改,要么全部执行,要么全部不执行
-
一致性:当事务执行失败时,所有被该事务影响的数据都应该恢复到事务执行前的状态。
事务执行前后,数据状态保持一致性(例如转账,转账之后两人总金额不变)
-
隔离性:在事务执行过程中对数据的修改,在事务提交之前对其他事务不可见。
一个事务的处理不能影响另一个事务的处理
-
持久性:事务处理结束,其效果在数据库中持久化。
即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作
通俗理解,事务是一组原子操作单元,从数据库角度说,就是一组 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 开启事务注解
<tx:annotation-driven transaction-manager="transactionManager"/>
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 事务处理器
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
由于我们使用较多的是
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):一个事务的操作导致另一个事务前后两次查询的结果数据量不同
不可重复读是同一数据不一样,幻读是数据量不一样