第八章 事务管理

时间:2021-6-11 作者:qvyue

target

掌握事务的定义、特性
了解脏读、幻读、不可重复读
了解事务的隔离级别
了解Spring事务的几个API
了解编程式事务的实现
掌握声明式事务的实现(重点、难点)

1. 事务概述

1.1 定义

事务由事务开始和事务结束之间执行的全体操作组成。例如:在关系数据库中,一个事务可以是一条SQL语句,一组SQL语句或整个程序。

简单说,开车需要如下几步:1.打开车门、2.上车、3.扎上安全带、4.点火、5.挂挡、6.松手刹。那么开车这个事务就是以上6步,一步也不能少,中间有任何一步出现问题,整个事务都会失败。

1.2 特性:ACID

事务有四个特性:

  • 原子性(Atomicity):指事务包含的所有操作要么全部成功,要么全部失败回滚

  • 一致性(Consistency):指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。

    拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。

  • 隔离性(Isolation):通常来说,一个事务的操作对于其他的事务的不可见的,也就是说一般而言事务都是独立的。

    即要达到这么一种效果:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。

  • 持久性(Durability):事务一旦完成,那么该事务引起的数据变化将永久生效,不会改变。

1.3 隔离问题

事务是具有隔离性的,如果不考虑隔离,会产生如下几个常见的隔离问题:

  • 脏读:事务A读到了事务B没有提交的数据。

    如果事务B回滚了,则事务A就使用了错误的数据。

  • 不可重复读:一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。

    例如事务T1在读取某一数据,而事务T2立马修改了这个数据并且提交事务给数据库,事务T1再次读取该数据就得到了不同的结果,发生了不可重复读。

  • 虚读(幻读):幻读是事务非独立执行时发生的一种现象。

    例如事务T1对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为“1”并且提交给数据库。而操作事务T1的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像产生幻觉一样,这就是发生了幻读。

1.4 隔离级别

为了避免产生脏读、幻读、不可重复读这些隔离问题,事务提供了以下隔离级别:

① Serializable (串行化):可避免脏读、不可重复读、幻读的发生。

② Repeatable read (可重复读):可避免脏读、不可重复读的发生。

③ Read committed (读已提交):可避免脏读的发生。

④ Read uncommitted (读未提交):最低级别,任何情况都无法保证。

以上四种隔离级别最高的是Serializable级别,最低的是Read uncommitted级别,当然级别越高,执行效率就越低。

像Serializable这样的级别,就是以锁表的方式(类似于Java多线程中的锁)使得其他的线程只能在锁外等待,所以平时选用何种隔离级别应该根据实际情况。

在MySQL数据库中默认的隔离级别为Repeatable read (可重复读)。Oracle的默认隔离级别为Read committed(读已提交)

1.5 MySQL事务操作–简单

🌰用代码实现ABCD四个操作表示的一个事务:

Connection conn = null;
try{
  //1.获取连接
  conn = DriverManager.getConnection();
  //2.开启事务
 conn.setAutoCommit(false);
  A操作
  B操作
  C操作
  D操作
 //3.提交事务
    conn.commit();
}catch(e){
  //4.回滚事务
  conn.rollback();
}finally{
  //5.关闭资源
  conn.close();
}

1.6 MySQL事务操作–savepoint

需求:实现AB(必须),CD(可选的)这样一组事务。

例如:你上班以后发工资,银行会给你发短信。发工资这个事件,公司的账户会扣钱,你的账户会加钱,这是最重要的。当你的账户加钱了,银行需要给你发短信,可是由于信号不好,短信没法成功。问:工资发不发?

有人说:短信没法成功,工资回滚一下。(需要回滚吗?不需要)公司账户扣钱,你的账户加钱,这事必选项。你的短信发送成不成功是可选项。可以用savepoint去实现

Connection conn = null;
SavePoint savepoint = null;//保存点,记录操作的当前位置,之后可以回滚到指定位置
try{
  //1.获取连接
  conn = DriverManager.getConnection();
  //2.开启事务
 conn.setAutoCommit(false);
  A操作
  B操作
  savepoint = conn.setSavePoint();//设置保存点,表示工资已经发完,接下来执行发短信操作
  C操作//假设C或D那个操作出现了问题,则不执行conn.commit(),而是调到catch块。
  D操作
 //3.提交事务
    conn.commit();
}catch(e){
  //怎么判断是AB出问题了还是CD出问题了?如果AB出问题了,savepoint是null,如果CD出问题了,则savepoint有值
  if(savepoint != null){//CD异常
    //回滚到CD之前
    conn.rollback(savepoint);
    //提交AB
    conn.commit();
}else{//AB异常
   // 回滚到最初
    conn.rollback();
  }
}finally{
  //5.关闭资源
  conn.close();
}

2. Spring事务的API

Spring的事务管理抽象出了三个主要的接口:

  • PlatformTransactionManager:平台事务管理器

  • TransactionDefinition:事务定义

  • TransactionStatus:事务状态

2.1 PlatformTransactionManager

Spring为不同的持久层框架提供了不同的 PlatformTransactionManager 接口实现。

首先看一下 PlatformTransactionManager 的接口实现如下:

第八章 事务管理
PlatformTransactionManager的实现.png

其中,使用最广泛的是 DataSourceTransactionManager,它为Spring JDBC 和 mybatis提供了支持。

然后看一下,PlatformTransactionManager接口提供的方法:

  • TransactionStatus getTransaction(TransactionDefinition definition):用于获取事务状态信息。

  • void commit(TransactionStatus status):用于提交事务。

  • void rollback(TransactionStatus status):用于回滚事务。

在项目中,Spring 将 xml 中配置的事务详细信息封装到对象 TransactionDefinition 中,然后通过事务管理器的 getTransaction() 方法获得事务的状态(TransactionStatus),并对事务进行下一步的操作。

2.2 TransactionDefinition

TransactionDefinition 接口是事务定义(描述)的对象,它提供了事务相关信息获取的方法,其中包括五个操作,具体如下。

  • String getName():获取事务对象名称。

  • int getIsolationLevel():获取事务的隔离级别。

  • int getPropagationBehavior():获取事务的传播行为。

  • int getTimeout():获取事务的超时时间。

  • boolean isReadOnly():获取事务是否只读。

在上述五个方法的描述中,事务的传播行为是指在同一个方法中,不同操作前后所使用的事务。

第八章 事务管理
事务的传播行为.png
属性名称 描 述
PROPAGATION_REQUIRED required 支持当前事务。如果 A 方法已经在事务中,则 B 事务将直接使用。否则将创建新事务
PROPAGATION_SUPPORTS supports 支持当前事务。如果 A 方法已经在事务中,则 B 事务将直接使用。否则将以非事务状态执行
PROPAGATION_MANDATORY mandatory 支持当前事务。如果 A 方法没有事务,则抛出异常
PROPAGATION_REQUIRES_NEW requires_new 将创建新的事务,如果 A 方法已经在事务中,则将 A 事务挂起
PROPAGATION_NOT_SUPPORTED not_supported 不支持当前事务,总是以非事务状态执行。如果 A 方法已经在事务中,则将其挂起
PROPAGATION_NEVER never 不支持当前事务,如果 A 方法在事务中,则抛出异常
PROPAGATION.NESTED nested 嵌套事务,底层将使用 Savepoint 形成嵌套事务

在事务管理过程中,传播行为可以控制是否需要创建事务以及如何创建事务。

通常情况下,数据的查询不会改变原数据,所以不需要进行事务管理,而对于数据的增加、修改和删除等操作,必须进行事务管理。如果没有指定事务的传播行为,则 Spring 默认的传播行为是 required。

2.3 TransactionStatus

TransactionStatus 接口是事务的状态,它描述了某一时间点上事务的状态信息。其中包含六个操作,具体如下:

名称 说明
void flush() 刷新事务
boolean hasSavepoint() 获取是否存在保存点
boolean isCompleted() 获取事务是否完成
boolean isNewTransaction() 获取是否是新事务
boolean isRollbackOnly() 获取是否回滚
void setRollbackOnly() 设置事务回滚

3. 转账案例环境搭建

创建Java项目:Spring-08

3.1 导入jar包

commons-dbcp-1.4.jar
commons-logging-1.2.jar
commons-pool-1.6.jar
mysql-connector-java-8.0.15.jar
aspectjweaver-1.9.5.jar
spring-aop-4.3.9.RELEASE.jar
spring-beans-4.3.9.RELEASE.jar
spring-context-4.3.9.RELEASE.jar
spring-core-4.3.9.RELEASE.jar
spring-expression-4.3.9.RELEASE.jar
spring-jdbc-4.3.9.RELEASE.jar
spring-tx-4.3.9.RELEASE.jar

将以上 jar 包复制到 src路径下,然后全选,右键–>Build Path –> add to biuld path

3.2 数据库相关

在 MySQL 中创建一个名为 transfer 的数据库,然后在该数据库中创建一个 account 表,并向表中插入两条数据,其 SQL 执行语句如下所示:

  • 创建数据库:
create DATABASE transfer;
  • 使用数据库:
  use transfer;
  • 创建表:
create table account(
     id int PRIMARY KEY auto_increment,
     name VARCHAR(30),
     money int
 )
  • 插入数据:
    insert into account(name,money) values('rose','1000');
    insert into account(name,money) values('jerry','1000');

3.3 编写源码

① 创建 db.properties

在项目的 src 下创建一个名为 db.properties的配置文件,这里使用 Dbcp 数据源,需要在该文件中添加如下配置:

jdbc.driverClassName=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/transfer?useSSL=false
jdbc.username=root
jdbc.password=root

② 实现 DAO

  1. 创建 UserDao 接口

在项目的 src 目录下创建一个名为 com.lee.spring.dao 的包,在该包下创建一个接口 UserDao,并在接口中创建汇款和收款的方法,

package com.lee.spring.dao;

public interface UserDao {
    void out(String outer,Integer money);
    void in(String innerer,Integer money);
}

2)创建DAO层接口实现类

package com.lee.spring.dao.impl;

@Repository
public class UserDaoImpl implements UserDao {

    @Autowired
    JdbcTemplate jdbcTemplate;
    
    /**
     * 转出
     */
    @Override
    public void out(String outer, Integer money) {
        String sql = "update account set money = money - ? where name = ?";
        jdbcTemplate.update(sql,money,outer);
    }

    /**
     * 转入
     */
    @Override
    public void in(String innerer, Integer money) {
        String sql = "update account set money = money + ? where name = ?";
        jdbcTemplate.update(sql,money,innerer);
    }

}

③ 实现 Service

1)创建 Service 层接口

public interface UserService {
    void transfer(String outName , String inName , Integer money) ;
}

2)创建 Service 层接口实现类

@Service("userService")
public class UserServiceImpl implements UserService {
    
    @Autowired
    UserDao userDao;
    
    /**
     * 没有事务的转账
     */
    @Override
    public void transfer(String outName, String inName, Integer money) {
        userDao.out(outName, money);//转出钱
        userDao.in(inName, money);//转入钱
    }

}

④ 创建 Spring 配置文件

在src下新建配置文件 applicationContext.xml


⑤ 测试:

@Test
public void testTransfer() {
  ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
  UserService userService =(UserService) context.getBean("userService");
  userService.transfer("jerry", "rose", 100);
}

查看数据库:

第八章 事务管理

转账正常。如果在转账的时候出现一些异常呢?

@Service("userService")
public class UserServiceImpl implements UserService {
    
    @Autowired
    UserDao userDao;
    
    /**
     * 没有事务的转账
     */
    @Override
    public void transfer(String outName, String inName, int money) {
        userDao.out(outName, money);//转出钱
    int i = 1/0;//出现异常
        userDao.in(inName, money);//转入钱
    }

}

查看数据库:

第八章 事务管理

转账异常。

因为转出钱是一个事务,转入钱是一个事务。int i = 1/0会抛一个异常,程序终止,转入钱的方法没有执行。我们不希望这种情况的发生,所以引进事务

4. 编程式事务(了解)

Spring 的事务管理有两种方式:一种是传统的编程式事务管理,即通过编写代码实现的事务管理;另一种是基于 AOP 技术实现的声明式事务管理。

在实际开发中,编程式事务管理很少使用,基本上都是声明式事务。所以,简单了解编程式事务即可。

下面是编程式事务的实现(在转账案例的基础上修改):

第一步:配置文件中加入 事务管理器 + 事务模板


第二步:在业务类中加上事务实现:

直接调用TransactionTemplate对象的execute方法即可实现编程式事务,但是需要传入一个TransactionCallback对象。

一般情况下用匿名内部类生成一个TransactionCallback对象,然后重写TransactionCallback对象里面的doInTransaction()方法。

@Service("userService")
public class UserServiceImpl implements UserService {

    @Autowired
    UserDao userDao;
    
    @Autowired
    private TransactionTemplate transactionTemplate;

    @Override
    public void transfer(String outName, String inName, int money) {
        TransactionCallbackWithoutResult action = new TransactionCallbackWithoutResult() {
            
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
        
                userDao.out(outName, money);//转出钱
                int i =1/0;
                userDao.in(inName, money);//转入钱

            }
        };
    
        transactionTemplate.execute(action);
    }

}

运行测试方法:

第八章 事务管理
转账异常

查询你数据库:

第八章 事务管理

数据库没有发生改变,说明事务生效。

5. 声明式事务

Spring 声明式事务管理在底层采用了 AOP 技术,其最大的优点在于无须通过编程的方式管理事务,只需要在配置文件中进行相关的规则声明,就可以将事务规则应用到业务逻辑中。

Spring 实现声明式事务管理主要有两种方式:

  • 基于 XML 方式的声明式事务管理。

  • 通过 Annotation 注解方式的事务管理。

5.1 基于XML方式实现

(此案例直接在转账案例上进行修改)

第一步:在配置文件中配置aop和事务相关

业务类没有异常的情况下:

@Service("userService")
public class UserServiceImpl implements UserService {

    @Autowired
    UserDao userDao;

    @Override
    public void transfer(String outName, String inName, int money) {
                userDao.out(outName, money);//转出钱
                userDao.in(inName, money);//转入钱
    }
}

执行测试方法后,查询数据库:

第八章 事务管理

在没有异常的情况下,转账成功。

若在业务类中加入异常:

package com.lee.spring.service.impl;

@Service("userService")
public class UserServiceImpl implements UserService {

    @Autowired
![转账案例数据库初始数据.png](https://upload-images.jianshu.io/upload_images/21013181-4fc19313f769e007.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
    UserDao userDao;

    @Override
    public void transfer(String outName, String inName, int money) {
        userDao.out(outName, money);// 转出钱
        int i = 1 / 0;
        userDao.in(inName, money);// 转入钱
    }
}

执行测试方法就会发生异常,如下:

第八章 事务管理

查看数据库,发现转账失败:

第八章 事务管理

以上结果,说明事务生效。

5.3 基于Annotation注解方式实现

使用 Annotation 的方式非常简单,只需要在项目中做两件事,具体如下。

① 在 Spring 容器中注册驱动,代码如下所示:

② 在需要使用事务的业务类或者方法中添加注解 @Transactional,并配置 @Transactional 的参数。关于 @Transactional 的参数如图所示。

第八章 事务管理
Transactional注解

下面通过修改《转账案例》中代码讲解如何使用 Annotation 注解的方式实现 Spring 声明式事务管理。

第一步:注册驱动

修改 Spring 配置文件 applicationContext.xml:

上述代码中可以看出,与原来的配置文件相比,这里只修改了事务管理器部分,新添加并注册了事务管理器的驱动。

第二步:添加 @Transactional 注解

修改 UserServiceImpl,在文件中添加 @Transactional 注解及参数

package com.lee.spring.service.impl;

@Service("userService")
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT, readOnly = false)
public class UserServiceImpl implements UserService {

    @Autowired
    UserDao userDao;

    @Override
    public void transfer(String outName, String inName, int money) {
        userDao.out(outName, money);// 转出钱
        int i = 1 / 0;
        userDao.in(inName, money);// 转入钱
    }
}

需要注意的是,在使用 @Transactional 注解时,参数之间用“,”进行分隔。

使用 JUnit 测试再次运行 testTransfer() 方法时,控制台同样会输出异常信息,这说明使用基于 Annotation 注解的方式同样实现了 Spring 的声明式事务管理。如果注释掉模拟异常的代码进行测试,则转账操作可以正常完成。

开发中,经常使用基于XML方法的声明式事务。

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:qvyue@qq.com 进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。