Spring学习之事务的使用姿势

文章目录
  1. Spring + mybatis + mysql 使用事务的几种姿势
    1. I. 前提
      1. case1:两张表的的事务支持情况
      2. case2:单表的事务支持
    2. II. 实例演示
      1. 1. 硬编码方式
        1. a. 实现
        2. b. 测试用例
        3. c. 小结
      2. 2. 基于TransactionProxyFactoryBean方式
        1. a. 实现
        2. b. 测试
        3. c. 小结
      3. 3. xml使用方式
        1. a. 实现
        2. b. 测试
      4. 4. 注解方式
        1. a. 实现
        2. b. 测试case
    3. III. 小结
    4. IV. 其他
      1. 1. 参考
      2. 2. 个人博客: 一灰灰Blog
      3. 3. 声明
      4. 4. 扫描关注

Spring + mybatis + mysql 使用事务的几种姿势

主要记录下spring是如何支持事务的,以及在Spring结合mybatis时,可以怎么简单的实现数据库的事务功能

I. 前提

case1:两张表的的事务支持情况

首先准备两张表,一个user表,一个story表,结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
CREATE TABLE `user` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
`pwd` varchar(26) NOT NULL DEFAULT '' COMMENT '密码',
`isDeleted` tinyint(1) NOT NULL DEFAULT '0',
`created` varchar(13) NOT NULL DEFAULT '0',
`updated` varchar(13) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `story` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`userId` int(20) unsigned NOT NULL DEFAULT '0' COMMENT '作者的userID',
`name` varchar(20) NOT NULL DEFAULT '' COMMENT '作者名',
`title` varchar(26) NOT NULL DEFAULT '' COMMENT '密码',
`story` text COMMENT '故事内容',
`isDeleted` tinyint(1) NOT NULL DEFAULT '0',
`created` varchar(13) NOT NULL DEFAULT '0',
`updated` varchar(13) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `userId` (`userId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

我们的事务场景在于用户修改name时,要求两张表的name都需要一起修改,不允许出现不一致的情况

case2:单表的事务支持

转账,一个用户减钱,另一个用户加钱

1
2
3
4
5
6
7
8
9
10
CREATE TABLE `money` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
`money` int(26) NOT NULL DEFAULT '0' COMMENT '钱',
`isDeleted` tinyint(1) NOT NULL DEFAULT '0',
`created` varchar(13) NOT NULL DEFAULT '0',
`updated` varchar(13) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

相比上面那个case,这个更加简单了,下面的实例则主要根据这个进行说明,至于case1,则留待扩展里面进行

首先是实现对应的dao和entity

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
@Data
public class MoneyEntity implements Serializable {

private static final long serialVersionUID = -7074788842783160025L;

private int id;

private String name;

private int money;

private int isDeleted;

private int created;

private int updated;
}


public interface MoneyDao {
MoneyEntity queryMoney(@Param("id") int userId);

// 加钱,负数时表示减钱
int incrementMoney(@Param("id") int userId, @Param("addMoney") int addMoney);
}

对应的mapper文件为

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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.git.hui.demo.mybatis.mapper.MoneyDao">

<sql id="moneyEntity">
id, `name`, `money`, `isDeleted`, `created`, `updated`
</sql>


<select id="queryMoney" resultType="com.git.hui.demo.mybatis.entity.MoneyEntity">
select
<include refid="moneyEntity"/>
from money
where id=#{id}

</select>

<update id="incrementMoney">
update money
set money=money + #{addMoney}
where id=#{id}
</update>
</mapper>

对应的mybatis连接数据源的相关配置

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
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<value>classpath*:jdbc.properties</value>
</property>
</bean>


<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<property name="driverClassName" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>

<property name="filters" value="stat"/>

<property name="maxActive" value="20"/>
<property name="initialSize" value="1"/>
<property name="maxWait" value="60000"/>
<property name="minIdle" value="1"/>

<property name="timeBetweenEvictionRunsMillis" value="60000"/>
<property name="minEvictableIdleTimeMillis" value="300000"/>

<property name="validationQuery" value="SELECT 'x'"/>
<property name="testWhileIdle" value="true"/>
<property name="testOnBorrow" value="false"/>
<property name="testOnReturn" value="false"/>

<property name="poolPreparedStatements" value="true"/>
<property name="maxPoolPreparedStatementPerConnectionSize" value="50"/>
</bean>


<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<!-- 指定mapper文件 -->
<property name="mapperLocations" value="classpath*:mapper/*.xml"/>
</bean>


<!-- 指定扫描dao -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.git.hui.demo.mybatis"/>
</bean>

II. 实例演示

通过网上查询,Spring事务管理总共有四种方式,下面逐一进行演示,每种方式是怎么玩的,然后看实际项目中应该如何抉择

1. 硬编码方式

编程式事务管理,既通过TransactionTemplate来实现多个db操作的事务管理

a. 实现

那么,我们的转账case可以如下实现

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
@Repository
public class CodeDemo1 {

@Autowired
private MoneyDao moneyDao;


@Autowired
private TransactionTemplate transactionTemplate;


/**
* 转账
*
* @param inUserId
* @param outUserId
* @param payMoney
* @param status 0 表示正常转账, 1 表示内部抛出一个异常, 2 表示新开一个线程,修改inUserId的钱 +200, 3 表示新开一个线程,修改outUserId的钱 + 200
*/
public void transfor(final int inUserId, final int outUserId, final int payMoney, final int status) {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
MoneyEntity entity = moneyDao.queryMoney(outUserId);
if (entity.getMoney() > payMoney) { // 可以转账

// 先减钱
moneyDao.incrementMoney(outUserId, -payMoney);


testCase(inUserId, outUserId, status);

// 再加钱
moneyDao.incrementMoney(inUserId, payMoney);
System.out.println("转账完成! now: " + System.currentTimeMillis());
}
}
});
}


// 下面都是测试用例相关
private void testCase(final int inUserId, final int outUserId, final int status) {
if (status == 1) {
throw new IllegalArgumentException("转账异常!!!");
} else if(status == 2) {
addMoney(inUserId);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else if (status == 3) {
addMoney(outUserId);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}


public void addMoney(final int userId) {
System.out.printf("内部加钱: " + System.currentTimeMillis());
new Thread(new Runnable() {
public void run() {
moneyDao.incrementMoney(userId, 200);
System.out.println(" sub modify success! now: " + System.currentTimeMillis());
}
}).start();
}
}

主要看上面的transfor方法,内部通过 transactionTemplate 来实现事务的封装,内部有三个db操作,一个查询,两个更新,具体分析后面说明

上面的代码比较简单了,唯一需要关注的就是transactionTemplate这个bean如何定义的,xml文件中与前面重复的就不贴了,直接贴上关键代码, 一个是根据DataSource创建的TransactionManager,一个则是根据TransactionManager创建的TransactionTemplate

1
2
3
4
5
6
7
8
<!--编程式事务-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>

<bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
<property name="transactionManager" ref="transactionManager"/>
</bean>

b. 测试用例

正常演示情况, 演示没有任何异常,不考虑并发的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath*:spring/service.xml", "classpath*:test-datasource1.xml"})
public class CodeDemo1Test {
@Autowired
private CodeDemo1 codeDemo1;

@Autowired
private MoneyDao moneyDao;

@Test
public void testTransfor() {

System.out.println("---------before----------");
System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney());
System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney());


codeDemo1.transfor(1, 2, 10, 0);

System.out.println("---------after----------");
System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney());
System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney());
}
}

输出如下,两个账号的钱都没有问题

1
2
3
4
5
6
7
---------before----------
id: 1 money = 10000
id: 2 money = 50000
转账完成! now: 1526130394266
---------after----------
id: 1 money = 10010
id: 2 money = 49990

转账过程中出现异常,特别是转账方钱已扣,收款方还没收到钱时,也就是case中的status为1的场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 内部抛异常的情况
@Test
public void testTransforException() {

System.out.println("---------before----------");
System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney());
System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney());


try {
codeDemo1.transfor(1, 2, 10, 1);
} catch (Exception e) {
e.printStackTrace();
}

System.out.println("---------after----------");
System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney());
System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney());
}

对此,我们希望把转账方的钱还回去, 输出如下,发现两个的钱都没有变化

1
2
3
4
5
6
7
8
---------before----------
id: 1 money = 10010
id: 2 money = 49990
---------after----------
id: 1 money = 10010
java.lang.IllegalArgumentException: 转账异常!!!
... // 省略异常信息
id: 2 money = 49990

当status为2,表示在转账人钱已扣,收款人钱没收到之间,又有人给收款人转了200,此时根据mysql的锁机制,另外人的转账应该是立马到的(因为收款人账号没有被锁住),且金额不应该有问题

输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
---------before----------
id: 1 money = 10010
id: 2 money = 49990
## 右边是注释: 转账过程中,另外存钱立马到账,没有被锁住
内部加钱: 1526130827480
sub modify success! now: 1526130827500
## 存钱结束
转账完成! now: 1526130830488
---------after----------
id: 1 money = 10220
id: 2 money = 49980

当status为3, 表示在转账人钱已扣,收款人钱没收到之间,又有人给转账人转了200,这时因为转账人的记录以及被加了写锁,因此只能等待转账的事务提交之后,才有可能+200成功,当然最终的金额也得一致

输出结果如下

1
2
3
4
5
6
7
8
9
10
11
---------before----------
id: 1 money = 10220
id: 2 money = 49980
## 右边是注释:内部存钱了,但没有马上成功
## 直到转账完成后,才立马存成功,注意两个时间戳
内部加钱: 1526131101046
转账完成! now: 1526131104051
sub modify success! now: 1526131104053
---------after----------
id: 1 money = 10230
id: 2 money = 50170

c. 小结

至此,编程式事务已经实例演示ok,从上面的过程,给人的感觉就和直接写事务相关的sql一样,

1
2
3
4
5
6
start transaction;

-- 这中间就是 TransactionTemplate#execute 方法内部的逻辑
-- 也就是需要事务管理的一组sql

commit;

2. 基于TransactionProxyFactoryBean方式

接下来的三个就是声明式事务管理,这种用得也比较少,因为需要每个事务管理类,添加一个TransactionProxyFactoryBean

a. 实现

除了将 TransactionTemplate 干掉,并将内部的sql逻辑移除之外,对比前面的,发现基本上没有太多差别

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
public class FactoryBeanDemo2 {

@Autowired
private MoneyDao moneyDao;

/**
* 转账
*
* @param inUserId
* @param outUserId
* @param payMoney
* @param status 0 表示正常转账, 1 表示内部抛出一个异常, 2 表示新开一个线程,修改inUserId的钱 +200, 3 表示新开一个线程,修改outUserId的钱 + 200
*/
public void transfor(final int inUserId, final int outUserId, final int payMoney, final int status) {

MoneyEntity entity = moneyDao.queryMoney(outUserId);
if (entity.getMoney() > payMoney) { // 可以转账

// 先减钱
moneyDao.incrementMoney(outUserId, -payMoney);


testCase(inUserId, outUserId, status);


// 再加钱
moneyDao.incrementMoney(inUserId, payMoney);
System.out.println("转账完成! now: " + System.currentTimeMillis());
}


}


private void testCase(final int inUserId, final int outUserId, final int status) {
if (status == 1) {
throw new IllegalArgumentException("转账异常!!!");
} else if (status == 2) {
addMoney(inUserId);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else if (status == 3) {
addMoney(outUserId);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}


public void addMoney(final int userId) {
System.out.println("内部加钱: " + System.currentTimeMillis());
new Thread(new Runnable() {
public void run() {
moneyDao.incrementMoney(userId, 200);
System.out.println("sub modify success! now: " + System.currentTimeMillis());
}
}).start();
}
}

重点来了,主要是需要配置一个 TransactionProxyBeanFactory,我们知道BeanFactory就是我们自己来创建Bean的一种手段,相关的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
<!--编程式事务-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>

<bean id="factoryBeanDemo2" class="com.git.hui.demo.mybatis.repository.transaction.FactoryBeanDemo2"/>

<!-- 配置业务层的代理 -->
<bean id="factoryBeanDemoProxy" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<!-- 配置目标对象 -->
<property name="target" ref="factoryBeanDemo2" />
<!-- 注入事务管理器 -->
<property name="transactionManager" ref="transactionManager"/>
<!-- 注入事务的属性 -->
<property name="transactionAttributes">
<props>
<!--
prop的格式:
* PROPAGATION :事务的传播行为
* ISOTATION :事务的隔离级别
* readOnly :只读
* -EXCEPTION :发生哪些异常回滚事务
* +EXCEPTION :发生哪些异常不回滚事务
-->
<!-- 这个key对应的就是目标类中的方法-->
<prop key="transfor">PROPAGATION_REQUIRED</prop>
<!-- <prop key="transfer">PROPAGATION_REQUIRED,readOnly</prop> -->
<!-- <prop key="transfer">PROPAGATION_REQUIRED,+java.lang.ArithmeticException</prop> -->
</props>
</property>
</bean>

通过上面的配置,大致可以了解到这个通过TransactionProxyFactoryBean就是创建了一个FactoryBeanDemo2的代理类,这个代理类内部封装好事务相关的逻辑,可以看做是前面编程式的一种简单通用抽象

b. 测试

测试代码与前面基本相同,唯一的区别就是我们使用的应该是上面BeanFactory生成的Bean,而不是直接使用FactoryBeanDemo2

正常演示case:

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
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath*:spring/service.xml", "classpath*:test-datasource2.xml"})
public class FactoryBeanDemo1Test {

@Resource(name = "factoryBeanDemoProxy")
private FactoryBeanDemo2 factoryBeanDemo2;

@Autowired
private MoneyDao moneyDao;


@Test
public void testTransfor() {

System.out.println("---------before----------");
System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney());
System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney());


factoryBeanDemo2.transfor(1, 2, 10, 0);

System.out.println("---------after----------");
System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney());
System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney());
}
}

输出

1
2
3
4
5
6
7
---------before----------
id: 1 money = 10000
id: 2 money = 50000
转账完成! now: 1526132058886
---------after----------
id: 1 money = 10010
id: 2 money = 49990

status为1,内部异常的情况下,我们希望钱也不会有问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void testTransforException() {

System.out.println("---------before----------");
System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney());
System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney());


try {
factoryBeanDemo2.transfor(1, 2, 10, 1);
} catch (Exception e) {
System.out.println(e.getMessage());;
}

System.out.println("---------after----------");
System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney());
System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney());
}

输出为

1
2
3
4
5
6
7
---------before----------
id: 1 money = 10010
id: 2 money = 49990
转账异常!!!
---------after----------
id: 1 money = 10010
id: 2 money = 49990

status为2 时,分析结果与上面应该相同,输出如下

1
2
3
4
5
6
7
8
9
---------before----------
id: 1 money = 10010
id: 2 money = 49950
内部加钱: 1526133325376
sub modify success! now: 1526133325387
转账完成! now: 1526133328381
---------after----------
id: 1 money = 10220
id: 2 money = 49940

status为3时,输出

1
2
3
4
5
6
7
8
9
---------before----------
id: 1 money = 10220
id: 2 money = 49940
内部加钱: 1526133373466
转账完成! now: 1526133376476
sub modify success! now: 1526133376480
---------after----------
id: 1 money = 10230
id: 2 money = 50130

c. 小结

TransactionProxyFactoryBean 的思路就是利用代理模式来实现事务管理,生成一个代理类,拦截目标方法,将一组sql的操作封装到事务中进行;相比较于硬编码,无侵入,而且支持灵活的配置方式

缺点也显而易见,每个都要进行配置,比较繁琐

3. xml使用方式

Spring有两大特点,IoC和AOP,对于事务这种情况而言,我们可不可以使用AOP来做呢?

对于需要开启事务的方法,拦截掉,执行前开始事务,执行完毕之后提交事务,出现异常时回滚

这样一看,感觉还是蛮有希望的,而下面两种姿势正是这么玩的,因此需要加上aspect的依赖

1
2
3
4
5
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</dependency>

a. 实现

java类与第二种完全一致,变动的只有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
<!-- 首先添加命名空间 -->
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="...
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd"



<!--对应的事务通知和切面配置-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!--
propagation :事务传播行为
isolation :事务的隔离级别
read-only :只读
rollback-for:发生哪些异常回滚
no-rollback-for :发生哪些异常不回滚
timeout :过期信息
-->
<tx:method name="transfor" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>



<!-- 配置切面 -->
<aop:config>
<!-- 配置切入点 -->
<aop:pointcut expression="execution(* com.git.hui.demo.mybatis.repository.transaction.XmlDemo3.*(..))" id="pointcut1"/>
<!-- 配置切面 -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="pointcut1"/>
</aop:config>

观察上面的配置,再想想第二种方式,思路都差不多了,但是这种方式明显更加通用,通过切面和切点,可以减少大量的配置

b. 测试

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
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath*:spring/service.xml", "classpath*:test-datasource3.xml"})
public class XmlBeanTest {
@Autowired
private XmlDemo3 xmlDemo;

@Autowired
private MoneyDao moneyDao;


@Test
public void testTransfor() {

System.out.println("---------before----------");
System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney());
System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney());


xmlDemo.transfor(1, 2, 10, 0);

System.out.println("---------after----------");
System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney());
System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney());
}
}

这个测试起来,和一般的写法就没啥两样了,比第二种的FactoryBean的注入方式简单点

正常输出

1
2
3
4
5
6
7
---------before----------
id: 1 money = 10000
id: 2 money = 50000
转账完成! now: 1526135301273
---------after----------
id: 1 money = 10010
id: 2 money = 49990

status=1 出现异常时,输出

1
2
3
4
5
6
7
---------before----------
id: 1 money = 10010
id: 2 money = 49990
转账异常!!!
---------after----------
id: 1 money = 10010
id: 2 money = 49990

status=2 转账过程中,又存钱的场景,输出,与前面预期一致

1
2
3
4
5
6
7
8
9
---------before----------
id: 1 money = 10010
id: 2 money = 49990
内部加钱: 1526135438403
sub modify success! now: 1526135438421
转账完成! now: 1526135441410
---------after----------
id: 1 money = 10220
id: 2 money = 49980

status=3 的输出,与前面预期一致

1
2
3
4
5
6
7
8
9
---------before----------
id: 1 money = 10220
id: 2 money = 49980
内部加钱: 1526135464341
转账完成! now: 1526135467349
sub modify success! now: 1526135467352
---------after----------
id: 1 money = 10230
id: 2 money = 50170

4. 注解方式

这个就是消灭xml,用注解来做的方式,就是将前面xml中的配置用 @Transactional注解替换

a. 实现

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
@Repository
public class AnnoDemo4 {

@Autowired
private MoneyDao moneyDao;


/**
* 转账
*
* @param inUserId
* @param outUserId
* @param payMoney
* @param status 0 表示正常转账, 1 表示内部抛出一个异常, 2 表示新开一个线程,修改inUserId的钱 +200, 3 表示新开一个线程,修改outUserId的钱 + 200
*
*
* Transactional注解中的的属性 propagation :事务的传播行为 isolation :事务的隔离级别 readOnly :只读
* rollbackFor :发生哪些异常回滚 noRollbackFor :发生哪些异常不回滚
* rollbackForClassName 根据异常类名回滚
*/
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT, readOnly = false)
public void transfor(final int inUserId, final int outUserId, final int payMoney, final int status) {

MoneyEntity entity = moneyDao.queryMoney(outUserId);
if (entity.getMoney() > payMoney) { // 可以转账

// 先减钱
moneyDao.incrementMoney(outUserId, -payMoney);


testCase(inUserId, outUserId, status);


// 再加钱
moneyDao.incrementMoney(inUserId, payMoney);
System.out.println("转账完成! now: " + System.currentTimeMillis());
}


}


private void testCase(final int inUserId, final int outUserId, final int status) {
if (status == 1) {
throw new IllegalArgumentException("转账异常!!!");
} else if (status == 2) {
addMoney(inUserId);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else if (status == 3) {
addMoney(outUserId);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}


private void addMoney(final int userId) {
System.out.println("内部加钱: " + System.currentTimeMillis());
new Thread(new Runnable() {
public void run() {
moneyDao.incrementMoney(userId, 200);
System.out.println("sub modify success! now: " + System.currentTimeMillis());
}
}).start();
}

}

因此需要在xml中配置,开启事务注解

1
2
3
4
5
6
7
8

<!--编程式事务-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>


<tx:annotation-driven transaction-manager="transactionManager"/>

这样一看,就更加清晰了,实际项目中,xml和注解方式也是用得最多的场景了

b. 测试case

和第三种测试case完全相同, 输出结果也一样,直接省略

III. 小结

上面说了Spring中四种使用事务的姿势,其中硬编码方式可能是最好理解的,就相当于将我们写sql中,使用事务的方式直接翻译成对应的java代码了;而FactoryBean方式相当于特殊情况特殊对待,为每个事务来一个代理类来增强事务功能;后面的两个则原理差不多都是利用事务通知(AOP)来实现,定义切点及相关信息

编程式:

  • 注入 TransactionTemplate
  • 将利用事务的逻辑封装到 transactionTemplate#execute方法内

代理BeanFactory:

  • 利用 TransactionProxyFactoryBean 为事务相关类生成代理
  • 使用方通过FactoryBean获取代理类,作为使用的Bean

xml配置:

  • 利用 tx标签 + aop方式来实现
  • <tx:advice> 标签定义事务通知,内部可有较多的配置信息
  • <aop:config> 配置切点,切面

注解方式:

  • 在开启事务的方法or类上添加 @Transactional 注解即可
  • 开启事务注解 <tx:annotation-driven transaction-manager="transactionManager"/>

IV. 其他

1. 参考

文档

源码

2. 个人博客: 一灰灰Blog

基于hexo + github pages搭建的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

3. 声明

尽信书则不如,已上内容,纯属一家之言,因本人能力一般,见识有限,如发现bug或者有更好的建议,随时欢迎批评指正

4. 扫描关注

QrCode

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×