一、声明式事务概述

1、事务回顾

① 事务和 SQL 语句的关系

一个事务中包含多条 SQL 语句。

images

② 事务的 ACID 属性

  • 原子性:一个事务中包含的多个数据库操作,缺一不可。
  • 一致性:事务执行之前,整个数据库中的所有数据处于“一致”状态(正确状态);事务执行之后,数据仍然处于“一致”状态。为了做到这一点,我们会让事务中的所有操作,要么全部成功一起提交,要么在任何一个操作失败后整体回滚。
  • 持久性:事务一旦提交,那么就永久保存到数据库中。
  • 隔离性:多个事务可以并发执行,参照隔离级别,决定它们相互之间是否会有彼此干扰。

③ 事务执行过程中的并发问题

images

  • 脏读:
    • 初始状态:数据库中 age 字段数据的值是 20
    • T1 把 age 修改为了 30
    • T2 读取了 age 现在的值:30
    • T1 回滚了自己的操作,age 恢复为了原来的 20
    • 此时 T2 读取到的 30 就是一个不存在的“脏”的数据
  • 不可重复读:
    • T1 第一次读取 age 是 20
    • T2 修改 age 为 30 并提交事务,此时 age 确定修改为了 30
    • T1 第二次读取 age 得到的是 30
  • 幻读:
    • T1 第一次执行 count(*)返回 500
    • T2 执行了 insert 操作
    • T1 第二次执行 count(*)返回 501,感觉像是出现了幻觉

④ 事务的隔离级别

  • 读未提交:存在脏读、不可重复读、幻读这些所有问题
  • 读已提交:能够解决脏读问题,不可重复读、幻读问题还存在
  • 可重复读:能够解决脏读、不可重复读问题,幻读问题还存在
  • 串行化:锁定整个表,让对整个表的操作全部排队串行执行。能解决所有并发问题,安全性最好,但是性能极差

2、IOC 容器和后面这些内容的关系

在 Spring 中干活,所有的东西肯定都必须在 IOC 容器中。如果某个对象不在 IOC 容器中,那么它就不归 Spring 管。

images

3、编程式事务

事务的相关操作完全由开发人员通过编码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
try{

// 开启事务(关闭事务的自动提交)
connection.setAutoCommit(false);

// 访问数据库的目标操作
empDao.updateXxx();

// 提交事务
connection.commit();

}catch(Exception e){

// 回滚事务
connection.rollBack();

}finally{

// 释放数据库连接
connection.close();

}

4、声明式事务

事务的控制交给 Spring 框架来管理,开发人员只需要在 Spring 框架的配置文件中声明你需要的功能即可。Spring 框架底层基于 AOP 实现了声明式事务。

images

二、声明式事务具体操作

1、搭建环境

① 导入 jar 包

com.springsource.net.sf.cglib-2.2.0.jar
com.springsource.org.aopalliance-1.0.0.jar
com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar
commons-logging-1.1.3.jar
druid-1.1.9.jar
hamcrest-core-1.3.jar
junit-4.12.jar
mysql-connector-java-5.1.37-bin.jar
spring-aop-4.0.0.RELEASE.jar
spring-aspects-4.0.0.RELEASE.jar
spring-beans-4.0.0.RELEASE.jar
spring-context-4.0.0.RELEASE.jar
spring-core-4.0.0.RELEASE.jar
spring-expression-4.0.0.RELEASE.jar
spring-jdbc-4.0.0.RELEASE.jar
spring-orm-4.0.0.RELEASE.jar
spring-test-4.0.0.RELEASE.jar
spring-tx-4.0.0.RELEASE.jar

② 准备配置文件

[1]Spring 的配置文件 applicationContext.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- 配置自动扫描的包 -->
<context:component-scan base-package="com.atguigu.tx.component"/>

<!-- 引入外部属性文件 -->
<!-- 引入context名称空间 -->
<!-- 使用context:property-placeholder引入外部属性文件 -->
<!-- 在location属性中指定外部属性文件的路径 -->
<!-- classpath:表示从类路径的根目录下开始查找,后面写外部属性文件以类路径根目录为基准的相对路径 -->
<context:property-placeholder location="classpath:jdbc.properties"/>

<!-- 配置数据源,引用外部属性文件中保存的数据 -->
<bean class="com.alibaba.druid.pool.DruidDataSource" id="druidDataSource">
<property name="url" value="${wechat.dev.url}"/>
<property name="driverClassName" value="${wechat.dev.driver}"/>
<property name="username" value="${wechat.dev.username}"/>
<property name="password" value="${wechat.dev.password}"/>
</bean>

<!-- 配置JdbcTemplate -->
<bean class="org.springframework.jdbc.core.JdbcTemplate" id="jdbcTemplate">
<!-- 给JdbcTemplate装配数据源,便于它根据数据源获取数据库连接 -->
<property name="dataSource" ref="druidDataSource"/>
</bean>

[2]jdbc.properties

1
2
3
4
wechat.dev.driver=com.mysql.jdbc.Driver
wechat.dev.url=jdbc:mysql://localhost:3306/mybatis0223
wechat.dev.username=root
wechat.dev.password=root

③ 其他组件

images

④junit 测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(value = {"classpath:applicationContext.xml"})
public class TxTest {

@Autowired
private DataSource dataSource;

@Autowired
private EmpService empService;

@Test
public void testConnection() throws SQLException {
Connection connection = dataSource.getConnection();
System.out.println("connection = " + connection);
}

2、加入基于注解的声明式事务(三步)

① 配置事务管理器

images

1
2
3
4
5
6
<!-- 配置事务管理器的bean -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

<!-- 给事务管理器装配数据源 -->
<property name="dataSource" ref="dataSource"/>
</bean>

② 开启声明式事务的注解驱动

images

注意:导入名称空间的时候,不要使用了错误的名称空间。

1
2
3
4
<!-- 开启声明式事务的注解驱动 -->
<!-- 在transaction-manager属性中指定前面配置的事务管理器的bean的id -->
<!-- transaction-manager属性的默认值是transactionManager,如果正好前面bean的id就是这个默认值,那么transaction-manager属性可以省略不配 -->
<tx:annotation-driven transaction-manager="transactionManager"/>

③ 在需要事务的方法上使用注解@Transactional

3、准备测试场景并测试声明式事务

images

①EmpDao 接口

1
2
3
4
5
6
7
package com.atguigu.tx.component.dao.api;

public interface EmpDao {

void updateEmpName(Integer empId, String empName);

}

②EmpDaoImpl 实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Repository
public class EmpDaoImpl implements EmpDao {

@Autowired
private JdbcTemplate jdbcTemplate;

@Override
public void updateEmpName(Integer empId, String empName) {
if (empId == 8) {
throw new RuntimeException("抛出异常!!!");
}

String sql = "update t_emp set emp_name=? where emp_id=?";

jdbcTemplate.update(sql, empName, empId);
}
}

③EmpService 接口

1
2
3
4
5
6
7
8
package com.atguigu.tx.component.service.api;

public interface EmpService {

void updateTwice();

}

④EmpService 方法

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
32
@Service

// @Transactional注解加在类上表示对类中的每一个方法都使用事务
// @Transactional注解加在类上之后,注解中设置的属性也会被应用到具体每一个方法上
// 类上设置只读,延续到增删改方法上会抛出java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed异常
@Transactional(readOnly = true)
public class EmpServiceImpl implements EmpService {

@Autowired
private EmpDao empDao;

@Override
// 在方法上应用的@Transactional注解属性设置会覆盖类上注解中的属性
@Transactional(readOnly = false)
public void updateTwice() {

// 1.准备第一次修改所需的数据
Integer empId = 7;
String empName = "AAA";

// 2.执行第一次修改
empDao.updateEmpName(empId, empName);

// 3.准备第二次修改所需的数据
empId = 8;
empName = "BBB";

// 4.执行第二次修改
empDao.updateEmpName(empId, empName);

}
}

⑤junit 测试

1
2
3
4
@Test
public void testTx() {
empService.updateTwice();
}

4、查看事务管理器源码

org.springframework.jdbc.datasource.DataSourceTransactionManager

① 提交操作

1
2
3
4
5
6
7
8
9
10
11
12
13
protected void doCommit(DefaultTransactionStatus status) {
DataSourceTransactionManager.DataSourceTransactionObject txObject = (DataSourceTransactionManager.DataSourceTransactionObject)status.getTransaction();
Connection con = txObject.getConnectionHolder().getConnection();
if (status.isDebug()) {
this.logger.debug("Committing JDBC transaction on Connection [" + con + "]");
}

try {
con.commit();
} catch (SQLException var5) {
throw new TransactionSystemException("Could not commit JDBC transaction", var5);
}
}

② 回滚操作

1
2
3
4
5
6
7
8
9
10
11
12
13
protected void doRollback(DefaultTransactionStatus status) {
DataSourceTransactionManager.DataSourceTransactionObject txObject = (DataSourceTransactionManager.DataSourceTransactionObject)status.getTransaction();
Connection con = txObject.getConnectionHolder().getConnection();
if (status.isDebug()) {
this.logger.debug("Rolling back JDBC transaction on Connection [" + con + "]");
}

try {
con.rollback();
} catch (SQLException var5) {
throw new TransactionSystemException("Could not roll back JDBC transaction", var5);
}
}

5、@Transactional 注解写在类上

① 写法

1
2
3
@Service
@Transactional
public class EmpServiceImpl implements EmpService {

② 效果

@Transactional 注解写在类上之后相当于给类中的每一个方法都加了这个注解。包括注解中设置的属性,也会一起被作用到类中的方法。

③ 特殊情况

如果类上@Transactional 注解设置的属性,对具体的某个方法来说不合适,那么就可以在具体的这个方法上再声明一个@Transactional 注解,设置自己需要的属性。此处遵循就近原则,离方法近的设置会覆盖离得远的设置。

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
32
@Service
@Transactional(readOnly = true)
public class EmpServiceImpl implements EmpService {

@Autowired
private EmpDao empDao;

@Override
@Transactional(readOnly = false)
public void updateTwice() {

// 1.准备第一次修改所需的数据
Integer empId = 5;
String empName = "AAA~~~";

// 2.执行第一次修改
empDao.updateEmpName(empId, empName);

// 3.准备第二次修改所需的数据
empId = 6;
empName = "BBB~~~";

// 4.执行第二次修改
empDao.updateEmpName(empId, empName);

}

@Override
public List<Employee> getAll() {
return empDao.selectAll();
}
}

在上面的例子中,类上的@Transactional(readOnly = true)注解对 getAll()方法有效,对 updateTwice()方法无效。

updateTwice()方法应用的是它自己的@Transactional(readOnly = false)。