spring使用事物的几种方法总结(转载)

转自:https://www.jb51.net/article/140158.htm#_lab2_1_0

前言

本文主要记录下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
@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
<? xml version = "1.0" encoding = "UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
< 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
@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());
  }
}

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

---------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());
}

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

---------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的锁机制,另外人的转账应该是立马到的(因为收款人账号没有被锁住),且金额不应该有问题

输出结果如下:

---------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成功,当然最终的金额也得一致

输出结果如下

---------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一样,

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
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
@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());
  }
}

输出

---------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
@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());
}

输出为

---------before----------
id: 1 money = 10010
id: 2 money = 49990
转账异常!!!
---------after----------
id: 1 money = 10010
id: 2 money = 49990

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

---------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时,输出

---------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
<!-- 首先添加命名空间 -->
xsi:schemaLocation="...
 
<!--对应的事物通知和切面配置-->
< 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的注入方式简单点

正常输出

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

status=1 出现异常时,输出

---------before----------
id: 1 money = 10010
id: 2 money = 49990
转账异常!!!
---------after----------
id: 1 money = 10010
id: 2 money = 49990

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

---------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 的输出,与前面预期一致

---------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
@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
<!--编程式事物-->
< 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. 参考

文档

Spring事务管理的四种方式

源码

 

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对脚本之家的支持。

猜你喜欢

转载自www.cnblogs.com/libin2015/p/12556218.html