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 的接口实现如下:
其中,使用最广泛的是 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():获取事务是否只读。
在上述五个方法的描述中,事务的传播行为是指在同一个方法中,不同操作前后所使用的事务。
属性名称 | 值 | 描 述 |
---|---|---|
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
- 创建 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 的参数如图所示。
下面通过修改《转账案例》中代码讲解如何使用 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方法的声明式事务。