Spring杂谈之事务与事务抽象

日常开发中,Spring框架已经成为了主流必不可少的一部分,同样在事务处理上,Spring给我们提供了一致的
事务管理抽象,这个抽象是Spring最重要的抽象之一,能实现为不同的事务Api提供一致的编程模型,本篇我
们将从事务开始学习Spring的事务抽象

事务

事务(Transaction),一般是指要做的或所做的事情。在计算机术语中是指访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。事务通常由高级数据库操纵语言或编程语言(如SQL,C++或Java)书写的用户程序的执行所引起,并用形如begin transaction和end transaction语句(或函数调用)来界定。事务由事务开始(begin transaction)和事务结束(end transaction)之间执行的全体操作组成。

为什么需要事务

事务是为了解决数据安全操作提出的解决方案,事务的控制实际上就是控制数据的安全访问与隔离。举一个简单的例子:如果我们去银行转账,A账户将自己的1000元转账给B,那么业务实现的逻辑首先是将A的余额减少1000,然后往B的余额增加100,假如这个过程中出现意外,导致过程中断,A已经扣款成功,B还没来及增加,就会导致B损失了1000元,所以必须做出控制,要求A账户转帐业务撤销。这才能保证业务的正确性,完成这个操走就需要事务,将A账户资金减少和B账户资金增加放到同一个事务里,要么全部执行成功,要么全部撤销,这样就保证了数据的安全性。

事务四大特性

综上所述,要是满足之前的需求,事务必须要满足四大特性:

  1. 原子性(atomicity):事务是数据库的逻辑工作单位,而且是必须是原子工作单位,对于其数据修改,要么全部执行,要么全部不执行。

  2. 一致性(consistency):事务在完成时,必须是所有的数据都保持一致状态。在相关数据库中,所有规则都必须应用于事务的修改,以保持所有数据的完整性。(实例:转账,两个账户余额相加,值不变。)

  3. 隔离性(isolation):一个事务的执行不能被其他事务所影响。

  4. 持久性(durability):一个事务一旦提交,事物的操作便永久性的保存在DB中。即便是在数据库系统遇到故障的情况下也不会丢失

Java事务类型

在Java中事务类型有三种:JDBC事务、JTA事务以及容器事务

JDBC事务

在jdbc中处理事务,都是通过connection完成,在同一事务中所有的操作,都在使用同一个connection对象完成,JDBC默认是开启事务的,并且默认完成提交操作。而在JDBC中有三种事务有关的操作:

setAutoCommit:设置是否要自动提交事务,如果为true则表示自动提交,每一个sql独立存在一个事务,如果设置为false,则需要手动commit进行提交

commit:手动提交事务

rollback:手动回滚结束事务

使用一个JDBC事务的基本步骤如下:

@Test
public void testTX(){
    String url = "jdbc:mysql://127.0.0.1:3306/test";
    String username = "root";
    String password = "1234";

    String sourceUserId = "leo";
    String desUserId = "xnn";
    int money = 500;

    Connection connection = null;
    try {
        //1.加载数据库驱动程序
        Class.forName("com.mysql.jdbc.Driver");
        //获得数据库连接
        connection = DriverManager.getConnection(url, username, password);
        //开启事务
        connection.setAutoCommit(false);//如果为true的话,sql语句会分别执行,修改数据库;如果为false的话,会激活事务

        //多条数据操作数据
        Statement sql = connection.createStatement();
        sql.executeUpdate("UPDATE user_info SET balance = balance-" + money + " WHERE user_id = '" + sourceUserId+"'");
        sql.executeUpdate("UPDATE user_info SET balance = balance+" + money + " WHERE user_id = '" + desUserId+"'");
        //提交事务
        connection.commit();

    } catch (SQLException e) {
        e.printStackTrace();
        try{
            //回滚
            connection.rollback();
        }catch (SQLException ex){
        }

    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    } finally {
        try {
            connection.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

从代码中我们可以看出来JDBC事务的缺点:

1.冗长、重复 2.需要显示事务控制 3.需要显示处理受检查异常

并且JDBC仅仅是为了完成事务操作提供了基础的API支持,通过操作JDBC我们可以将多个sql语句放到同一个事务中,保证ACID特性,但是当遇到跨库跨表的sql,简单的JDBC事务就无法满足了,基于这种问题,JTA事务出现了

JTA事务

JTA(Java Transaction API)提供了跨数据库连接(或其他JTA资源)的事务管理能力。JTA事务管理则由JTA容器实现,J2ee框架中事务管理器与应用程序,资源管理器,以及应用服务器之间的事务通讯

JTA的构成

在JTA中有几个重要的概念:

1).高层应用事务界定接口,供事务客户界定事务边界

2).X/Open XA协议(资源之间的一种标准化的接口)的标准Java映射 ,它可以使事务性的资源管理器参与由外部事务管理器控制的事务中

3).高层事务管理器接口,允许应用程序为其管理的程序界定事务的边界范围

JTA中的重要接口

JTA中的重要接口主要位于javax.transaction包中

1).UserTransaction:让应用程序得以控制事务的开始、挂起、提交与回滚等操作,由java或者ejb组件调用

2).TransactionManager:用于应用服务管理事务的状态

3).Transaction:用于控制执行事务操作

4).XAResource:用于在分布式事务环境下,协调事务管理器和资源管理器的工作

5).XID:用来为事务标示的java映射id

需要注意的是前三个接口仅存在于 javaee.jar 中,在javaSe中并不存在。

JTA事务编程的基本步骤

我们来看下使用JTA事务的基本步骤,通过简单的java编码完成一个简单的JTA事务过程:

//配置JTA事务,建立对应的数据源
//1.建立事务:通过创建UserTransaction类的实例来开始一个事务
Context ctx = new InitialContext(p) ;
UserTransaction trans = (UserTransaction) ctx.lookup("javax. Transaction.UserTransaction");
//开始事务
trans.begin();
//找到数据源,进行绑定
DataSource ds = (DataSource) ctx.lookup("mysqldb");
//建立数据库连接
Connection mycon = ds.getConnection();
//执行了sql操作
stmt.executeUpdate(sqlS);
//提交事务
trans.commit();
//关闭连接
mycon.close();
JTA事务的优缺点

可以看出,JTA的优点很明显,提供了分布式下的事务解决方案,并且执行严格的ACID操作,但是,标准的JTA事务在日常开发中并不常用,其原因就是JTA的缺点导致的,例如JTA的实现相当复杂,JTA UserTransaction需要从JNDI获取,即我们如果要实现JTA一般情况下也需要实现JNDI,并且JTA只是个简易的容器,使用复杂,在灵活的需求下很难实现代码复用,因此我们需要一个能给我们进行完善容器事务操作的框架

Spring事务与事务抽象

Spring给我们封装了一套事务机制,并且提供了完善的事务抽象,将事务所需要的步骤进行抽象划分,并以编程的方式提供一个标准API,如下:

try{
 
//1.开启事务
//2.执行数据库操作
 
//3.提交事务
 
}catch(Exception ex){
 
//处理异常
//4.回滚事务
 
}finally{
 
//关闭连接,资源清理
}
Spring事务抽象.png
Spring的事务抽象

Spring的抽象事务模型基于接口PlatformTransactionManager ,该接口有不同的多种实现,每一种实现都有对应的一个特定的数据访问技术,大体如下:

Spring事务抽象模型.png

Spring事务抽象实现.png

可以看到,Spring并不是提供了完整的事务操作Api,而是提供了多种事务管理器,将事务的职责托管给了Hibernate、JTA等持久化机制平台框架来实现,而仅仅提供一个通用的事务管理器接口--org.springframework.transaction.PlatformTransactionManager ,并且给各大持久化事务平台框架提供了对应的事务管理器,用来限制其通用行为,但是具体事务实现将由各大平台自己去实现:

Public interface PlatformTransactionManager{  
       // 由TransactionDefinition得到TransactionStatus对象
       TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
    // 提交
       Void commit(TransactionStatus status) throws TransactionException;  
       // 回滚
       Void rollback(TransactionStatus status) throws TransactionException;  
}
在Spring中使用各个事务

接下来我们来看看,如果使用Spring事务抽象来使用各个事务平台,该如何使用:

Spring JDBC事务

如果应用程序中直接使用JDBC来进行持久化操作,可以使用DataSourceTransactionManager来处理事务边界,而使用DataSourceTransactionManager,你需要使用xml配置将其装配到应用上下文中:

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
     <property name="dataSource" ref="dataSource" />
</bean>

而配置完毕以后,在JDBC实例中,DataSourceTransactionManager是通过调用java.sql.Connection来管理事务 ,在sql所在的方法执行完毕以后,将会自动调用连接的commit()方法来提交事务,同样的如果执行过程中出现了异常,将会触发调用rollback()方法进行回滚 。

Hibernate事务

如果应用程序的持久化是通过Hibernate来实现的,那么你需要使用HibernateTransactionManager,对于Hibernate3及以上版本,需要在Spring上下文定义中添加如下的Bean声明:

<bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
     <property name="sessionFactory" ref="sessionFactory" />
</bean>

Java持久化Api事务--JPA

事实上JPA规范的实现来自Hibernate,如果你打算使用JPA事务的话,你需要使用JpaTransactionManager来管理事务,配置如下:

<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
     <property name="sessionFactory" ref="sessionFactory" />
</bean>

JpaTransactionManager只需要装配一个JPA实体管理工厂(javax.persistence.EntityManagerFactory接口的任意实现),而JpaTransactionManager将与由工厂所产生的JPA EntityManager 合作来构建事务

PlatformTransactionManager 及其相关属性

事务管理器接口PlatformTransactionManager通过getTransaction 方法来得到事务,参数为TransactionDefinition类,而这个类定义类事务的基本属性:

1.传播行为  
2.隔离规则
3.回滚规则
4.事务超时设置
5.事务是否只读

而TransactionDefinition 接口的定义如下:

public interface TransactionDefinition {
    int getPropagationBehavior(); // 返回事务的传播行为
    int getIsolationLevel(); // 返回事务的隔离级别,事务管理器根据它来控制另外一个事务可以看到本事务内的哪些数据
    int getTimeout();  // 返回事务必须在多少秒内完成
    boolean isReadOnly(); // 事务是否只读,事务管理器能够根据这个返回值进行优化,确保事务是只读的
}

其中最重要的是事务的传播行为以及隔离规则,我们先来看看事务的七种传播行为,如表格所示:

PROPAGATION_REQUIRED 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中
PROPAGATION_SUPPORTS 支持当前事务,如果当前没有事务,就以非事务方式执行
PROPAGATION_MANDATORY 支持当前事务,如果当前没有事务,就抛出异常
PROPAGATION_REQUIRES_NEW 新建事务,如果当前存在事务,把当前事务挂起
PROPAGATION_NOT_SUPPORTED 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起
PROPAGATION_NEVER 以非事务方式执行,如果当前存在事务,则抛出异常
PROPAGATION_NESTED 如果一个活动的事务存在,则运行在一个嵌套的事务中. 如果没有活动事务, 则按TransactionDefinition.PROPAGATION_REQUIRED 属性执行

注:虽然有七种,但是其中三种都是Spring迭代留下的,一般我们只会配置REQUIRED和第四种REQUIRES_NEW

事务隔离级别

在看事务隔离级别前我们先来看看如果不进行事务隔离,所产生的脏读不可重复读幻读效应是什么

脏读

脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务获取数据进行操作,结果将刚刚未提交的数据获取到了

不可重复读

不可重复读是指在一个事务内,多次读同一数据,前后读取的结果不一致。在A事务还没有结束时,另外一个事务B也访问该同一数据。那么,在A事务中的两 次读数据的过程中,由于B事务对当前数据进行修改操作,导致A事务两次读取的数据不一致,因此称为是不 可重复读。

幻读

幻读是指当事务不是独立执行时发生的一种现象,例如A事务对表中的一个数据进行了修改,这种修改涉及到表中的全部数据行。 同时,B事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,就会发生操作A事务的用户发现表中还有没有修改的数据行,就好象 发生了幻觉一样。

为了解决这些问题,Spring将事务分为了五个隔离级别,每个隔离级别分别对应的效果如下:

DEFAULT 使用数据库设置的隔离级别 ( 默认 ) ,由 DBA 默认的数据库隔离级别设置来决定隔离级别
READ_UNCOMMITTED 会出现脏读、不可重复读、幻读 ,同样并发效率最高
READ_COMMITTED 会出现不可重复读、幻读问题 (锁定当前读取的行数据)
REPEATABLE_READ 会出幻读(锁定读取的所有行)
SERIALIZABLE 会造成锁表,效率最差,但是不会出现任何脏读、幻读、不可重复读的情况

注意:一般建议开启READ_COMMITTED级别,即只读已经提交的数据,至于不可重复读和幻读问题,可以通过数据库乐观锁版本号方式解决

推荐阅读更多精彩内容