Spring 声明式事务

事务概述

  • 在JavaEE企业级开发的应用领域,为了保证数据的 完整性和一致性 ,必须引入数据库事务的概念,所以事务管理是企业级应用程序开发中必不可少的技术。
  • 事务就是一组由于逻辑上紧密关联而合并成一个整体(工作单元)的多个数据库操作,这些操作 要么都执行,要么都不执行
  • 事务的四个关键属性(ACID)
    • 原子性 (atomicity):“原子”的本意是“不可再分”,事务的原子性表现为一个事务中涉及到的多个操作在逻辑上缺一不可。事务的原子性要求事务中的所有操作要么都执行,要么都不执行。
    • 一致性 (consistency):“一致”指的是数据的一致,具体是指:所有数据都处于满足业务规则的一致性状态。一致性原则要求:一个事务中不管涉及到多少个操作,都必须保证事务执行之前数据是正确的,事务执行之后数据仍然是正确的。如果一个事务在执行的过程中,其中某一个或某几个操作失败了,则必须将其他所有操作撤销,将数据恢复到事务执行之前的状态,这就是回滚。
    • 隔离性 (isolation):在应用程序实际运行过程中,事务往往是并发执行的,所以很有可能有许多事务同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。隔离性原则要求多个事务在并发执行过程中不会互相干扰。
    • 持久性(durability):持久性原则要求事务执行完成后,对数据的修改永久的保存下来,不会因各种系统错误或其他意外情况而受到影响。通常情况下,事务对数据的修改应该被写入到持久化存储器中。

拓展: 在大数据中的新型数据库中存在严格一致性


Spring 中的数据库管理

  1. 编程式事务管理
    1. 使用原生的JDBC API进行事务管理

      [1]获取数据库连接Connection对象
      [2]取消事务的自动提交
      [3]执行操作
      [4]正常完成操作时手动提交事务
      [5]执行失败时回滚事务
      [6]关闭相关资源

    2. 评价

      使用原生的JDBC API实现事务管理是所有事务管理方式的基石,同时也是最典型的编程式事务管理。编程式事务管理需要将事务管理代码嵌入到业务方法中来控制事务的提交和回滚。在使用编程的方式管理事务时,必须在每个事务操作中包含额外的事务管理代码。相对于核心业务而言,事务管理的代码显然属于非核心业务,如果多个模块都使用同样模式的代码进行事务管理,显然会造成较大程度的代码冗余。

声明式事务

大多数情况下声明式事务比编程式事务管理更好:它将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。
事务管理代码的固定模式作为一种横切关注点,可以通过AOP方法模块化,进而借助Spring AOP框架实现声明式事务管理。
Spring在不同的事务管理API之上定义了一个抽象层,通过配置的方式使其生效,从而让应用程序开发人员不必了解事务管理API的底层实现细节,就可以使用Spring的事务管理机制。
Spring 既支持编程式事务管理,也支持声明式的事务管理

1
2
3
4
5
6
7
8
9
10
11
12
13
Spring 中的Around可以实现编程式事务
获取数据库连接Connection对象

try{
取消事务的自动提交
执行操作
正常完成操作时手动提交事务
}
catch(){
执行失败时回滚事务
}finally{
关闭相关资源
}

Spring 提供的事务管理器

Spring从不同的事务管理API中抽象出了一整套事务管理机制,让事务管理代码从特定的事务技术中独立出来。开发人员通过配置的方式进行事务管理,而不必了解其底层是如何实现的。
Spring的核心事务管理抽象是PlatformTransactionManager。它为事务管理封装了一组独立于技术的方法。无论使用Spring的哪种事务管理策略(编程式或声明式),事务管理器都是必须的。
事务管理器可以以普通的bean的形式声明在Spring IOC容器中。
事务管理器的主要实现

  • DataSourceTransactionManager:在应用程序中只需要处理一个数据源,而且通过JDBC存取。
  • JtaTransactionManager:在JavaEE应用服务器上用JTA(Java Transaction API)进行事务管理
  • HibernateTransactionManager:用Hibernate框架存取数据库

测试

  1. 需求

    测试数据库

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    CREATE TABLE book (
    isbn VARCHAR (50) PRIMARY KEY,
    book_name VARCHAR (100),
    price INT
    ) ;
    CREATE TABLE book_stock (
    isbn VARCHAR (50) PRIMARY KEY,
    stock INT,
    CHECK (stock > 0)
    ) ;

    CREATE TABLE account (
    username VARCHAR (50) PRIMARY KEY,
    balance INT,
    CHECK (balance > 0)
    ) ;

    INSERT INTO account (`username`,`balance`) VALUES ('Tom',100000);
    INSERT INTO account (`username`,`balance`) VALUES ('Jerry',150000);

    INSERT INTO book (`isbn`,`book_name`,`price`) VALUES ('ISBN-001','book01',100);
    INSERT INTO book (`isbn`,`book_name`,`price`) VALUES ('ISBN-002','book02',200);
    INSERT INTO book (`isbn`,`book_name`,`price`) VALUES ('ISBN-003','book03',300);
    INSERT INTO book (`isbn`,`book_name`,`price`) VALUES ('ISBN-004','book04',400);
    INSERT INTO book (`isbn`,`book_name`,`price`) VALUES ('ISBN-005','book05',500);

    INSERT INTO book_stock (`isbn`,`stock`) VALUES ('ISBN-001',1000);
    INSERT INTO book_stock (`isbn`,`stock`) VALUES ('ISBN-002',2000);
    INSERT INTO book_stock (`isbn`,`stock`) VALUES ('ISBN-003',3000);
    INSERT INTO book_stock (`isbn`,`stock`) VALUES ('ISBN-004',4000);
    INSERT INTO book_stock (`isbn`,`stock`) VALUES ('ISBN-005',5000);

配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<context:component-scan base-package="xyz.lyhcc"></context:component-scan>

<context:property-placeholder location="classpath:db.properties"/>
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="user" value="${jdbc.user}"></property>
<property name="password" value="${jdbc.password}"></property>
<property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property>
<property name="driverClass" value="${jdbc.driverClass}"></property>
</bean>

<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<constructor-arg name="dataSource" ref="dataSource"></constructor-arg>
</bean>

<!-- 配置事务管理器 -->
<bean id="tm" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<constructor-arg name="dataSource" ref="dataSource"></constructor-arg>
</bean>
<!-- 启动事务注解 -->
<tx:annotation-driven transaction-manager="tm"/>
<!-- 在事务方法中添加@Transactional注解 -->

Spring事务参数

  1. isolation Isolation 事务的隔离级别

  2. propagation Propagation 事务传播行为

  3. noRollbackFor Class[] 哪些异常事务不回滚

  4. noRollbackForClassName String[] String全类名

  5. rollbackFor Class[] 哪些异常事务需要回滚

  6. rollbackForClassName String[]

  7. readOnly boolean 设置事务为只读事务

    可以进行事务优化
    readOnly=true 加快查询速度,不管事务那一推操作, 是不对事务进行加锁,只有查询是被允许的

    1
    org.springframework.dao.TransientDataAccessResourceException: PreparedStatementCallback; SQL [UPDATE book_stock SET stock=stock-1 WHERE isbn=?]; Connection is read-only. Queries leading to data modification are not allowed; nested exception is java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed
  8. timeout int 超时,指定时长后终止事务,并回滚,跑出异常

    1
    org.springframework.transaction.TransactionTimedOutException: Transaction timed out

异常分类:

运行时(非检查异常):可以不用处理,默认会回滚
编译时异常(检查异常):必须处理
默认不回滚


数据库事务并发问题

假设现在有两个事务:Transaction01和Transaction02并发执行。

脏读

[1]Transaction01将某条记录的AGE值从20修改为30。
[2]Transaction02读取了Transaction01更新后的值:30。
[3]Transaction01回滚,AGE值恢复到了20。
[4]Transaction02读取到的30就是一个无效的值。

不可重复读

[1]Transaction01读取了AGE值为20。
[2]Transaction02将AGE值修改为30。
[3]Transaction01再次读取AGE值为30,和第一次读取不一致。

幻读

[1]Transaction01读取了STUDENT表中的一部分数据。
[2]Transaction02向STUDENT表中插入了新的行。
[3]Transaction01读取了STUDENT表时,多出了一些行。

事务隔离级别

数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。 一个事务与其他事务隔离的程度称为隔离级别。 SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。

读未提交:READ UNCOMMITTED

允许Transaction01读取Transaction02未提交的修改。

读已提交:READ COMMITTED

要求Transaction01只能读取Transaction02已提交的修改。

可重复读:REPEATABLE READ

确保Transaction01可以多次从一个字段中读取到相同的值,  
即Transaction01执行期间禁止其它事务对这个字段进行更新。

串行化:SERIALIZABLE

一般不再使用

各个隔离级别解决并发问题的能力见下表

/ 脏读 不可重复读 幻读
READ UNCOMMITTED
READ COMMITTED
REPEATABLE READ
SERIALIZABLE

MySql 在REPEATABLE READ隔离级别下任何问题都没有

各种数据库产品对事务隔离级别的支持程度

/ Oracle MySQL
READ UNCOMMITTED ×
READ COMMITTED
REPEATABLE READ × √(默认)
SERIALIZABLE

Spring 基于XML实现事务隔离级别设置

在Spring 2.x事务通知中,可以在tx:method元素中指定隔离级别


注意:

  1. Spring 事务使用代理机制
  2. 隔离级别根据自己的业务进行调整

Spring事务的传播行为

简介

当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。事务的传播行为可以由传播属性指定。Spring定义了7种类传播行为

测试

说明

  1. REQUIRED传播行为

    当bookService的purchase()方法被另一个事务方法checkout()调用时,它默认会在现有的事务内运行。这个默认的传播行为就是REQUIRED。因此在checkout()方法的开始和终止边界内只有一个事务。这个事务只在checkout()方法结束的时候被提交,结果用户一本书都买不了

  2. REQUIRES_NEW传播行为

    表示该方法必须启动一个新事务,并在自己的事务内运行。如果有事务在运行,就应该先挂起它。

  3. 补充

    在Spring 2.x事务通知中,可以像下面这样在tx:method元素中设定传播事务属性。


总结:

  • REQUIRED 搭顺风车
    • 事务的属性继承于大事务
    • 将之前的connection传递给这个方法使用
  • REQUIRES_NEW 开自己的车
    • 事务属性可以任意调整
    • 这个方法直接使用新的connection

事务控制(XML实现)

基于xml配置的事务,依赖tx和aop命名空间

  1. Spring 中提供事务管理器(事务切面)配置这个事务管理器
  2. 配置事务方法
  3. 告诉Spring哪些方法是事务方法,(事务切面按照我们的切入点表达式去切入方法)

指明哪些方法是事务方法,切入点表达式只是说,事务管理器要切入这些方法,哪些方法需要加事务需要tx:method指定

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×