Spring声明式事务管理转账Demo

一、关于


记录使用Spring声明式事务管理完成的转帐Demo全过程

环境:Mac 、 IDEA 、 Maven 、 MySQL 、 c3p0连接池

内容:一、基本的数据库连接配置;二、在XML定义事务;三、定义注解驱动事务;四、初步的事务原理探究

具体代码见:https://github.com/GuoJohnny/SpringPracticeDemo

二、基本数据库连接配置及接口与实现


2.1 关于Jar包

要使用Spring JDBC操作Mysql数据库,并配置c3p0连接池需要一下jar包(使用Maven中的XML文件来体现):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.23</version>
</dependency>
<!--数据库连接池-->
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.1</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.1.4.RELEASE</version>
</dependency>

2.2 关于接口及实现

有关Dao与Service层代码如下:

2.2.1 Class Dao and Impl

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

/**
* Created by huangguoxin on 16/1/24.
*/

public interface AccountDao {

/**
*
* @param out 转出账号
* @param money 转出金额
*/

public void outMoney(String out,Double money);

/**
*
* @param in 转入账号
* @param money 转入金额
*/

public void inMoney(String in,Double money);
}

package com.hgx.impl;

import com.hgx.dao.AccountDao;
import org.springframework.jdbc.core.support.JdbcDaoSupport;

/**
* Created by huangguoxin on 16/1/24.
*/


/**
* 接口实现类
*/

public class AccountDaoImpl extends JdbcDaoSupport implements AccountDao {

public void outMoney(String out, Double money) {
String sql = "update account set money = money - ? where name = ?";
this.getJdbcTemplate().update(sql,money,out);
}

public void inMoney(String in, Double money) {
String sql = "update account set money = money + ? where name = ?";
this.getJdbcTemplate().update(sql,money,in);
}
}

2.2.2 Class Service and Impl

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

/**
* Created by huangguoxin on 16/1/24.
*/


/**
* Service层接口
*/

public interface AccountService {

/**
*
* @param out 转出账号
* @param in 转入账号
* @param money 金额
*/

public void transfer(String out,String in,Double money);
}

package com.hgx.serviceimpl;

import com.hgx.dao.AccountDao;
import com.hgx.service.AccountService;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

/**
* Created by huangguoxin on 16/1/24.
*/



public class AccountServiceImpl implements AccountService {

//注入转账Dao
private AccountDao accountDao;


public void setAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}

/**
*
* @param out 转出账号
* @param in 转入账号
* @param money 金额
*/


public void transfer(String out, String in, Double money) {
accountDao.outMoney(out,money);
// int i = 1/0; //人为设置异常
accountDao.inMoney(in,money);

}
}

2.2.3 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
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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
">




<!--事务管理Demo-->
<!--引入外部配置文件-->
<context:property-placeholder location="classpath:jdbc.properties"/>


<!--配置c3p0链接池-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
<property name="driverClass" value="${jdbc.driverClass}"/>
<property name="jdbcUrl" value="${jdbc.url}"/>
<property name="user" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>

<!--链接池中保留的最小连接数-->
<property name="minPoolSize" value="1"/>
<!--链接池中保留的最大链接数 Default:15-->
<property name="maxPoolSize" value="15"/>
<!--初始化时获取的链接数,取值在min到max之间-->
<property name="initialPoolSize" value="3"/>
<!--最大空闲时间,60秒内未使用则连接被丢弃。若为0则永不丢弃。Default: 0 -->
<property name="maxIdleTime" value="60" />
<!--当连接池中的连接耗尽的时候c3p0一次同时获取的连接数。Default: 3 -->
<property name="acquireIncrement" value="3" />
<!--JDBC的标准参数,用以控制数据源内加载的PreparedStatements数量。但由于预缓存的statements

属于单个connection而不是整个连接池。所以设置这个参数需要考虑到多方面的因素。

如果maxStatements与maxStatementsPerConnection均为0,则缓存被关闭。Default: 0-->


<property name="maxStatements" value="0" />

<!--每60秒检查所有连接池中的空闲连接。Default: 0 -->

<property name="idleConnectionTestPeriod" value="60" />

<!--定义在从数据库获取新连接失败后重复尝试的次数。Default: 30 -->

<property name="acquireRetryAttempts" value="30" />

<!--获取连接失败将会引起所有等待连接池来获取连接的线程抛出异常。但是数据源仍有效

保留,并在下次调用getConnection()的时候继续尝试获取连接。如果设为true,那么在尝试

获取连接失败后该数据源将申明已断开并永久关闭。Default: false-->


<property name="breakAfterAcquireFailure" value="true" />

<!--因性能消耗大请只在需要的时候使用它。如果设为true那么在每个connection提交的

时候都将校验其有效性。建议使用idleConnectionTestPeriod或automaticTestTable

等方法来提升连接测试的性能。Default: false -->


<property name="testConnectionOnCheckout" value="false" />

</bean>



<bean id="accountService" class="com.hgx.serviceimpl.AccountServiceImpl">
<property name="accountDao" ref="accountDao"/>
</bean>

<bean id="accountDao" class="com.hgx.impl.AccountDaoImpl">
<property name="dataSource" ref="dataSource"/>
<!--为dao提供jdbc模版-->
</bean>

</beans>

2.2.4 Account SQL

1
2
3
4
5
6
7
8
9
CREATE TABLE `account` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL,
`money` double DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

INSERT INTO `account` VALUES ('1', 'aaa', '1000');
INSERT INTO `account` VALUES ('2', 'bbb', '1000');
INSERT INTO `account` VALUES ('3', 'ccc', '1000');

2.2.5 Test

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.hgx.springtest;

import com.hgx.service.AccountService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import javax.annotation.Resource;

/**
* Created by huangguoxin on 16/1/24.
*/

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class AccountTest {

@Resource(name = "accountService")
private AccountService accountService;

@Test
public void AccountDemo(){
accountService.transfer("aaa","bbb",200d);
}
}

运行测试类

即可看见结果,如果在service中添加异常,只减少不增加。


三、在XML中定义事务

3.1 前言

在早期版本Spring中,声明事务需要装配一个名为TransactoinProxyFactoryBean的特殊的bean,倒是非常冗长的xml配置,Spring3.0之后提供了一个tx配置命名空间,极大简化Spring中的声明式事务。
这个tx命名空间提供了一些新的XML配置元素,其中最值得注意。

1
2
3
4
5
6
7
8
9
10
11
12
13
<!--事务代理工厂bean已经被淘汰,现在Spring提供一个tx配置命名空间,简化声明式事务,即事务的通知-->
<tx:advice id="txAdvice" transaction-manager="transactionManager" >
<tx:attributes>
<tx:method name="transfer" propagation="REQUIRED"/>
</tx:attributes>

</tx:advice>

<!--同样需要配置切面-->
<aop:config>
<aop:pointcut id="pointcut" expression="execution(* com.hgx.service.AccountService.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="pointcut"/>
</aop:config>

3.2 事务配置项详细说明

3.2.1 事务5个方面

通过元素的属性来指定,即在标签中的的参数

传播行为 含义
isolation 指定事务的管理隔离级别
propagation 指定事务的传播规则
raed-only 指定事务为只读
回滚规则:rollback-for;no-rollback-for rollback-for:指定哪儿检查型异常应当回滚,no-rollback-for:反之
timeout 对于长时间运行的事务定义超时时间

3.2.2 传播规则

传播规则定义了何时要创建一个事物或何时使用已有的事务

传播规则 含义
PROPAGATION_MANDATORY 支持当前事务,如果当前没有事务,就抛出异常
PROPAGATION_NESTED 表示如果当前已经存在一个事务,该方法讲会前套事务中运行。嵌套可以独立于当前事务进行单独得提交或回滚
PROPAGATION_NEVER 以非事务方式执行,如果当前存在事务,则抛出异常
PROPAGATION_NOT_SUPPORTED 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起
PROPAGATION_REQUIRED 支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择
PROPAGATION_REQUIRES_NEW 新建事务,如果当前存在事务,把当前事务挂起
PROPAGATION_SUPPORTS 支持当前事务,如果当前没有事务,就以非事务方式执行

3.2.2.1 传播规则示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ServiceA {   


void methodA() {
ServiceB.methodB();
}

}

ServiceB {


void methodB() {
}

}

3.2.2.2 传播规则分析

我们这里一个个分析吧
1: PROPAGATION_REQUIRED
加入当前正要执行的事务不在另外一个事务里,那么就起一个新的事务,比如说,ServiceBmethodB的事务级别定义为PROPAGATION_REQUIRED, 那么由于执行ServiceA.methodA的时候,ServiceA.methodA已经起了事务,这时调用ServiceB.methodB,ServiceB.methodB看到自己已经运行在ServiceA.methodA的事务内部,就不再起新的事务。而假如ServiceA.methodA运行的时候发现自己没有在事务中,他就会为自己分配一个事务。
这样,在ServiceA.methodA或者在ServiceB.methodB内的任何地方出现异常,事务都会被回滚。即使ServiceB.methodB的事务已经被提交,但是ServiceA.methodA在接下来fail要回滚,ServiceB.methodB也要回滚

2: PROPAGATION_SUPPORTS
如果当前在事务中,即以事务的形式运行,如果当前不再一个事务中,那么就以非事务的形式运行这就跟平常用的普通非事务的代码只有一点点区别了。不理这个,因为我也没有觉得有什么区别
3: PROPAGATION_MANDATORY
必须在一个事务中运行。也就是说,他只能被一个父事务调用。否则,他就要抛出异常。
4: PROPAGATION_REQUIRES_NEW
这个就比较绕口了。 比如我们设计ServiceA.methodA的事务级别为PROPAGATION_REQUIRED,ServiceB.methodB的事务级别为PROPAGATION_REQUIRES_NEW,那么当执行到ServiceB.methodB的时候,ServiceA.methodA所在的事务就会挂起,ServiceB.methodB会起一个新的事务,等待ServiceB.methodB的事务完成以后,他才继续执行。他与PROPAGATION_REQUIRED 的事务区别在于事务的回滚程度了。因为ServiceB.methodB是新起一个事务,那么就是存在两个不同的事务。如果ServiceB.methodB已经提交,那么ServiceA.methodA失败回滚,ServiceB.methodB是不会回滚的。如果ServiceB.methodB失败回滚,如果他抛出的异常被ServiceA.methodA捕获,ServiceA.methodA事务仍然可能提交。
5: PROPAGATION_NOT_SUPPORTED
当前不支持事务。比如ServiceA.methodA的事务级别是PROPAGATION_REQUIRED ,而ServiceB.methodB的事务级别是PROPAGATION_NOT_SUPPORTED ,那么当执行到ServiceB.methodB时,ServiceA.methodA的事务挂起,而他以非事务的状态运行完,再继续ServiceA.methodA的事务。
6: PROPAGATION_NEVER
不能在事务中运行。假设ServiceA.methodA的事务级别是PROPAGATION_REQUIRED, 而ServiceB.methodB的事务级别是PROPAGATION_NEVER ,那么ServiceB.methodB就要抛出异常了。
7: PROPAGATION_NESTED
理解Nested的关键是savepoint。他与PROPAGATION_REQUIRES_NEW的区别是,PROPAGATION_REQUIRES_NEW另起一个事务,将会与他的父事务相互独立,而Nested的事务和他的父事务是相依的,他的提交是要等和他的父事务一块提交的。也就是说,如果父事务最后回滚,他也要回滚的。

而Nested事务的好处是他有一个savepoint。

1
2
3
4
5
6
7
8
9
10
11
12
13
ServiceA {   


void methodA() {
try {
//savepoint
ServiceB.methodB(); //PROPAGATION_NESTED 级别
} catch (SomeException) {
// 执行其他业务, 如 ServiceC.methodC();
}
}

}

也就是说ServiceB.methodB失败回滚,那么ServiceA.methodA也会回滚到savepoint点上,ServiceA.methodA可以选择另外一个分支,比如ServiceC.methodC,继续执行,来尝试完成自己的事务。但是这个事务并没有在EJB标准中定义。

3.2.3 隔离规则

隔离级别决定了一个事务会被其它并行的事务的影响程度(就是多个事务对相同数据完成各自操作中可能会导致脏读,不可重复读,幻读)

在理想情况下,事务之间是完全隔离的,从而防止这些问题出现,但是完全的隔离会通常涉及到数据库中的记录。侵占性的锁会造成性能上的问题。因此会有如下隔离级别。

隔离级别 含义
ISOLATION_DEFAULT 使用后端数据库默认的隔离级别
ISOLATION_READ_UNCOMMITED 允许读取尚未提交的数据,可能会引起脏读、不可重复读、幻读
ISOLATION_READ_COMMITED 允许读取并发事务已经提交的数据,可以避免脏读
ISOLATION_REPEATABLE_READ 对同一字段的多次读取的结果是一致的,除非数据是被本事务自己锁修改
ISOLATION_SERIALIZABLE 可以确保不发生脏读、不可重复读、幻读,但是是最慢的事务隔离级别

四、使用注解定义事务

使用注解,通常只需要一行XML,上文提到代码只需要做如改动下:

1
2
<!--注解驱动的事务-->
<tx:annotation-driven transaction-manager="transactionManager"/>
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
package com.hgx.serviceimpl;

import com.hgx.dao.AccountDao;
import com.hgx.service.AccountService;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

/**
* Created by huangguoxin on 16/1/24.
*/


@Transactional(propagation = Propagation.REQUIRED)
public class AccountServiceImpl implements AccountService {

//注入转账Dao

private AccountDao accountDao;


public void setAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}

/**
*
* @param out 转出账号
* @param in 转入账号
* @param money 金额
*/

@Transactional(propagation = Propagation.REQUIRED,readOnly = false)
public void transfer(String out, String in, Double money) {
accountDao.outMoney(out,money);
// int i = 1/0;
accountDao.inMoney(in,money);

}
}

五、初步的事务原理探究