Spring 事务管理

Spring 的事务管理,事务在日常开发中非常重要,它可以对数据库中的一些异常进行回滚,这样就可以保证数据的一致性。

事务的四个特性:

  • 原子性(Atomicity):事务是一个原子操作,由一系列动作组成。事务的原子性确保动作要么全部完成,要么完全不起作用。
  • 一致性(Consistency):一旦事务完成(不管成功还是失败),系统必须确保它所建模的业务处于一致的状态,而不会是部分完成部分失败。在现实中的数据不应该被破坏。
  • 隔离性(Isolation):可能有许多事务会同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。
  • 持久性(Durability):一旦事务完成,无论发生什么系统错误,它的结果都不应该受到影响,这样就能从任何系统崩溃中恢复过来。通常情况下,事务的结果被写到持久化存储器中。

知识点

  • Spring 编程式事务管理
  • Spring 申明式事务管理

Spring 事务

Spring 中有两种事务管理的方式,一种是编程式事务管理,另一种是声明式事务管理。

  • 编程式事务管理:所谓编程式事务指的是通过编码方式实现事务,允许用户在代码中精确定义事务的边界。即类似于 JDBC 编程实现事务管理。管理使用 TransactionTemplate 或者直接使用底层的 PlatformTransactionManager。对于编程式事务管理,spring 推荐使用 TransactionTemplate。
  • 声明式事务管理:管理建立在 AOP 之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明(或通过基于@Transactional 注解的方式),便可以将事务规则应用到业务逻辑中。

接下我们将通过一个转账的案例来分别讲解这两种事务管理方式。

Spring 编程式事务管理

数据库准备

1
2
3
4
5
6
7
8
create database transaction;
use transaction
create table account(
id int,
username varchar(20),
money int);

insert into account values(1,'Tom',10000),(2,'Marry',10000);

新建项目

1
mvn archetype:generate -DgroupId = com.learn.tx -DartifactId = springTansaction -DarchetypeArtifactId = maven-archetype-quickstart

修改 pom.xml 文件,添加 Spring 的依赖:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
<project xmlns = "http://maven.apache.org/POM/4.0.0" xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation = "http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.learn.tx</groupId>
<artifactId>springTransaction</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>

<name>springTransaction</name>
<url>http://maven.apache.org</url>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<spring.version>5.1.1.RELEASE</spring.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${spring.version}</version>
</dependency>


<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${spring.version}</version>
</dependency>


<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

首先创建包 com.learn.tx.dao,创建 AccountDao.java,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.learn.tx.dao;

public interface AccountDao {
/**
* 汇款
* @param outer 汇款人
* @param money 汇款金额
*/
public void out(String outer,int money);

/**
* 收款
* @param inner 收款人
* @param money 收款金额
*/
public void in(String inner,int money);

}

在再这个包下创建 AccountDaoImpl.java,代码如下:

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
package com.learn.tx.dao;

import org.springframework.jdbc.core.support.JdbcDaoSupport;

import com.learn.tx.dao.AccountDao;

public class AccountDaoImpl extends JdbcDaoSupport implements AccountDao {

/**
* 根据用户名减少账户金额
*/
@Override
public void out(String outer, int money) {
this.getJdbcTemplate().update("update account set money = money - ? where username = ?",money,outer);
}

/**
* 根据用户名增加账户金额
*/
@Override
public void in(String inner, int money) {
this.getJdbcTemplate().update("update account set money = money + ? where username = ?",money,inner);
}

}

创建包 com.learn.tx.service,创建 AccountService.java,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.learn.tx.service;

public interface AccountService {

/**
* 转账
* @param outer 汇款人
* @param inner 收款人
* @param money 交易金额
*/
public void transfer(String outer,String inner,int money);

}

再在这个包下创建 AccountServiceImpl.java,代码如下:

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
33
package com.learn.tx.service;

import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;

import com.learn.tx.dao.AccountDao;
import com.learn.tx.service.AccountService;

public class AccountServiceImpl implements AccountService{

private AccountDao accountDao;
private TransactionTemplate transactionTemplate;

public void setTransactionTemplate(TransactionTemplate transactionTemplate) {
this.transactionTemplate = transactionTemplate;
}
public void setAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}
@Override
public void transfer(final String outer,final String inner,final int money) {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus arg0) {
accountDao.out(outer, money);
int i = 1/0;
accountDao.in(inner, money);
}
});
}

}

我们先在 src/main/ 下新建一个 Folder,命名为 resources,现在可以开始创建 Spring Bean 配置文件,创建文件 SpringBeans.xml,配置 bean 如下。文件位于 src/main/resources 下。

编辑 SpringBeans.xml 文件如下:

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
33
34
35
36
37
38
<?xml version = "1.0" encoding = "UTF-8"?>
<beans xmlns = "http://www.springframework.org/schema/beans"
xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
xmlns:context = "http://www.springframework.org/schema/context"
xmlns:aop = "http://www.springframework.org/schema/aop"
xsi:schemaLocation = "http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">

<bean id = "dataSource" class = "org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name = "driverClassName" value = "com.mysql.jdbc.Driver"/>
<property name = "url" value = "jdbc:mysql://localhost/transaction"/>
<property name = "username" value = "root"/>
<property name = "password" value = ""/>
</bean>

<bean id = "accountDao" class = "com.learn.tx.dao.AccountDaoImpl">
<property name = "dataSource" ref = "dataSource"></property>
</bean>

<bean id = "accountService" class = "com.learn.tx.service.AccountServiceImpl">
<property name = "accountDao" ref = "accountDao"></property>
<property name = "transactionTemplate" ref = "transactionTemplate"></property>
</bean>

<!-- 创建模板 -->
<bean id = "transactionTemplate" class = "org.springframework.transaction.support.TransactionTemplate">
<property name = "transactionManager" ref = "txManager"></property>
</bean>

<!-- 配置事务管理器,管理器需要事务,事务从 Connection 获得,连接从连接池DataSource获得 -->
<bean id = "txManager" class = "org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name = "dataSource" ref = "dataSource"></property>
</bean>
</beans>

最后创建 App.java,在包路径 com.learn.tx 下,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.learn.tx;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.learn.tx.service.AccountService;

public class App {
private static ApplicationContext context;

public static void main(String[] args) {
context = new ClassPathXmlApplicationContext("SpringBeans.xml");

AccountService account = (AccountService) context.getBean("accountService");
// Tom 向 Marry 转账1000
account.transfer("Tom", "Marry", 1000);
}
}

运行:

1
2
mvn compile
mvn exec:java -Dexec.mainClass = "com.learn.tx.App"

程序发生异常,此时数据库会回滚,所以没有数据的变化,查看数据库:

现在让我们修改 AccountServiceImpl.java 中的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.learn.tx.service;

import com.learn.tx.dao.AccountDao;
import com.learn.tx.service.AccountService;

public class AccountServiceImpl implements AccountService{

private AccountDao accountDao;

public void setAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}
@Override
public void transfer(String outer, String inner, int money) {
accountDao.out(outer, money);
int i = 1/0;
accountDao.in(inner, money);
}

}

再修改 SpringBeans.xml 中的代码:

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
<?xml version = "1.0" encoding = "UTF-8"?>
<beans xmlns = "http://www.springframework.org/schema/beans"
xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
xmlns:context = "http://www.springframework.org/schema/context"
xmlns:aop = "http://www.springframework.org/schema/aop"
xsi:schemaLocation = "http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">

<bean id = "dataSource" class = "org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name = "driverClassName" value = "com.mysql.jdbc.Driver"/>
<property name = "url" value = "jdbc:mysql://localhost/transaction"/>
<property name = "username" value = "root"/>
<property name = "password" value = ""/>
</bean>

<bean id = "accountDao" class = "com.learn.tx.dao.AccountDaoImpl">
<property name = "dataSource" ref = "dataSource"></property>
</bean>

<bean id = "accountService" class = "com.learn.tx.service.AccountServiceImpl">
<property name = "accountDao" ref = "accountDao"></property>
</bean>
</beans>

运行

1
2
mvn compile
mvn exec:java -Dexec.mainClass = "com.learn.tx.App"

程序发生异常,由于没有加入事务,就算发生异常,还是会更新数据库的数据

Spring 申明式事务管理

首先修改 SpringBeans.xml 中的代码:

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
33
34
35
36
37
38
39
40
41
<?xml version = "1.0" encoding = "UTF-8"?>
<beans xmlns = "http://www.springframework.org/schema/beans"
xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
xmlns:context = "http://www.springframework.org/schema/context"
xmlns:aop = "http://www.springframework.org/schema/aop"
xmlns:tx = "http://www.springframework.org/schema/tx"
xsi:schemaLocation = "http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">

<bean id = "dataSource" class = "org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name = "driverClassName" value = "com.mysql.jdbc.Driver"/>
<property name = "url" value = "jdbc:mysql://localhost/transaction"/>
<property name = "username" value = "root"/>
<property name = "password" value = ""/>
</bean>

<bean id = "accountDao" class = "com.learn.tx.dao.AccountDaoImpl">
<property name = "dataSource" ref = "dataSource"></property>
</bean>

<bean id = "accountService" class = "com.learn.tx.service.AccountServiceImpl">
<property name = "accountDao" ref = "accountDao"></property>
</bean>

<!-- 1 事务管理器 -->
<bean id = "txManager" class = "org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name = "dataSource" ref = "dataSource"></property>
</bean>
<!-- 2 将管理器交予 spring
* transaction-manager 配置事务管理器
* proxy-target-class
true : 底层强制使用 cglib 代理
-->
<tx:annotation-driven transaction-manager = "txManager" proxy-target-class = "true"/>
</beans>

然后修改 AccountServiceImpl.java 中的代码

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
package com.learn.tx.service;

import javax.annotation.Resource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import com.learn.tx.dao.AccountDao;
import com.learn.tx.service.AccountService;

@Transactional(propagation = Propagation.REQUIRED , isolation = Isolation.DEFAULT)
@Service("accountService")
public class AccountServiceImpl implements AccountService{
@Resource(name = "accountDao")
private AccountDao accountDao;

public void setAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}
@Override
public void transfer(String outer, String inner, int money) {
accountDao.out(outer, money);
// int i = 1/0;
accountDao.in(inner, money);
}

}

先将 AccountServiceImpl.java 中的 int i = 1/0 注释掉,以验证代码的正确性,然后运行:

1
2
mvn compile
mvn exec:java -Dexec.mainClass = "com.learn.tx.App"

查看数据库:
数据发生了变化,说明在没有异常的时候代码是正确的,然后再将 int i = 1/0 取消注释,运行

1
2
mvn compile
mvn exec:java -Dexec.mainClass = "com.learn.tx.App"

查看数据库:
由于添加了事务,发生异常过后会回滚,所以数据没有变化。