事务问题
# 70.事务问题
在讲解 AOP 之前,我们先看看我们之前的案例中有什么问题,然后再引出 AOP
# 环境准备
为了方便演示,我们在之前 demo5 的分支上继续开发;
# 添加一个转账功能
我们添加一个转账的功能。首先在接口层 IAccountService
添加一个 transfer 方法:
/**
* 转账
* @param sourceName 转出账户名称
* @param targetName 转入账户名称
* @param money 转账金额
*/
void transfer(String sourceName, String targetName, Float money);
2
3
4
5
6
7
然后我们需要在 dao 接口 IAccountDao
增加一个根据名称查找账户的方法:
/**
* 根据名称查询账户
* @param accountName
* @return 如果有唯一的结果就返回,如果没有结果就返回 null
* 如果结果集超过一个就抛异常
*/
Account findAccountByName(String accountName);
2
3
4
5
6
7
然后我们在 AccountDaoImpl
实现这个方法:
@Override
public Account findAccountByName(String accountName) {
try {
List<Account> accounts = runner.query("select * from account where name = ? ", new BeanListHandler<Account>(Account.class), accountName);
if (accounts == null || accounts.size() == 0) {
return null;
}
if (accounts.size() > 1) {
throw new RuntimeException("结果集不唯一,数据有问题");
}
return accounts.get(0);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
然后我们在 AccountServiceImpl
实现类里实现这个方法。步骤如下:
- 根据名称查询转出账户
- 根据名称查询转入账户
- 转出账户减钱
- 转入账户加钱
- 更新转出账户
- 更新转入账户
@Override
public void transfer(String sourceName, String targetName, Float money) {
// 1. 根据名称查询转出账户
Account source = accountDao.findAccountByName(sourceName);
// 2. 根据名称查询转入账户
Account target = accountDao.findAccountByName(targetName);
// 3. 转出账户减钱
source.setMoney(source.getMoney() - money);
// 4. 转入账户加钱
target.setMoney(target.getMoney() + money);
// 5. 更新转出账户
accountDao.updateAccount(source);
// 6. 更新转入账户
accountDao.updateAccount(target);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 新建测试类
我们测试下这个方法,用 aaa 账户给 bbb 账户转 100 块钱:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")
public class AccountServiceTest {
@Autowired
private IAccountService as;
@Test
public void testTransfer(){
as.transfer("aaa","bbb",100f);
}
}
2
3
4
5
6
7
8
9
10
11
12
运行前,两个整合都是 1000 块:
id name money
1 aaa 1000
2 bbb 1000
3 ccc 1000
2
3
4
运行后,成功转账:
id name money
1 aaa 900
2 bbb 1100
3 ccc 1000
2
3
4
# 如果有异常.....
如果在转账的过程中,发生了异常,怎么办呢?我们可以测试下,自己创造一个异常:
@Override
public void transfer(String sourceName, String targetName, Float money) {
// 1. 根据名称查询转出账户
Account source = accountDao.findAccountByName(sourceName);
// 2. 根据名称查询转入账户
Account target = accountDao.findAccountByName(targetName);
// 3. 转出账户减钱
source.setMoney(source.getMoney() - money);
// 4. 转入账户加钱
target.setMoney(target.getMoney() + money);
// 5. 更新转出账户
accountDao.updateAccount(source);
int i = 1/0;
// 6. 更新转入账户
accountDao.updateAccount(target);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
此时运行结果,确实抛出了异常;现在关键的问题是:数据库里的金额不对,aaa 账户的 100 块钱消失了!
java.lang.ArithmeticException: / by zero
id name money
1 aaa 800
2 bbb 1100
3 ccc 1000
2
3
4
5
6
7
# 问题分析
上述问题的产生,是因为我们没有使用开启事务,因此会自动提交,不会回滚。
在转账事务,获取了多次连接对象:查询 a 账户一次,查询 b 账户一次,更新 a 账户一次,更新 b 账户一次,发送了多次请求,每次请求成功后就会自动提交。如果中途出错,则后面的代码不会执行并提交,但之前的已经提交了
所以,转账事务里的操作,应该都是同一个 connection 操作,在业务层控制事务。我们可以使用 ThreadLocal 对象把 Connection 和当前线程绑定,从而使个线程中只有一个能控制事务的对象。
# connection 工具类
我们可以写个工具类,用来获取 connection,并新增一个 dataSource 属性和 set 方法用于注入
package com.peterjxl.utils;
import javax.sql.DataSource;
import java.sql.Connection;
/**
* 连接的工具类,它用于从数据源中获取一个连接,并且实现和线程的绑定
*/
public class ConnectonUtils {
private ThreadLocal<Connection> tl = new ThreadLocal<Connection>();
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
/**
* 获取当前线程上的连接
* @return
*/
public Connection getThreadConnection() {
try {
// 1. 先从 ThreadLocal 上获取
Connection conn = tl.get();
// 2. 判断当前线程上是否有连接
if (conn == null) {
// 3. 从数据源中获取一个连接,并且存入 ThreadLocal 中
conn = dataSource.getConnection();
tl.set(conn);
}
// 4. 返回当前线程上的连接
return conn;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 把连接和线程解绑
*/
public void removeConnection() {
tl.remove();
}
}
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
# 事务管理工具类
再写一个工具类,管理事务:
package com.peterjxl.utils;
/**
* 和事务管理相关的工具类,它包含了开启事务,提交事务,回滚事务和释放连接
*/
public class TransactionManager {
private ConnectonUtils connectonUtils;
public void setConnectonUtils(ConnectonUtils connectonUtils) {
this.connectonUtils = connectonUtils;
}
/**
* 开启事务
*/
public void beginTransaction() {
try {
connectonUtils.getThreadConnection().setAutoCommit(false);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 提交事务
*/
public void commit() {
try {
connectonUtils.getThreadConnection().commit();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 回滚事务
*/
public void rollback() {
try {
connectonUtils.getThreadConnection().rollback();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 释放连接
*/
public void release() {
try {
connectonUtils.getThreadConnection().close(); // 还回连接池中
connectonUtils.removeConnection();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
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
注意 Tomcat 等 Web 服务器,也会有线程池技术,当不再使用这个线程时,需要将 connection 移除;否则下次再获取这个线程时,还是能判断出是该线程有 connection 的,所以我们需要解绑。
# 改造 service 实现类
现在我们可以给 service 实现类加上 TransactionManager
成员变量和 set 方法,用来注入;
然后给每个方法,都加上事务控制,由于方法有很多,我们就不全部贴出来了:
@Override
public List<Account> findAllAccount() {
try {
// 1. 开启事务
txManager.beginTransaction();
// 2. 执行操作
List<Account> accounts = accountDao.findAllAccount();
//3. 提交事务
txManager.commit();
//4. 返回结果
return accounts;
}catch (Exception e) {
// 5. 回滚操作
txManager.rollback();
throw new RuntimeException(e);
}finally {
// 6. 释放连接
txManager.release();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
也就是每个方法中,都有重复的代码块:开启事务、提交事务、返回结果、异常处理等代码。
# 改造 dao 实现类
由于需要使用当前线程上的连接,因此我们需要引入 ConnectionUtils
的依赖,并且在 Queryrunner 中传入当前线程:
@Repository("accountDao")
public class AccountDaoImpl implements IAccountDao {
private QueryRunner runner;
private ConnectionUtils connectionUtils;
public void setRunner(QueryRunner runner) {
this.runner = runner;
}
public void setConnectionUtils(ConnectionUtils connectionUtils) {
this.connectionUtils = connectionUtils;
}
@Override
public List<Account> findAllAccount() {
try {
return runner.query(connectionUtils.getThreadConnection(), "select * from account", new BeanListHandler<Account>(Account.class));
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
// .........
}
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
也就是说,每个方法的 query 方法里,都要传入 connectionUtils.getThreadConnection()
这个对象
# 配置 IoC
接下来就是配置注入了
- 给 service 层 注入事务管理工具类
- 给 dao 层 注入
connectionUtils
工具类 QueryRunner
不再需要dataSource
注入,而是connectionUtils
需要注入dataSource
- 给事务管理工具类注入
connectionUtils
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 配置Service -->
<bean id="accountService" class="com.peterjxl.service.impl.AccountServiceImpl">
<!-- 注入dao -->
<property name="accountDao" ref="accountDao"/>
<property name="txManager" ref="txManager"/>
</bean>
<!--配置Dao对象-->
<bean id="accountDao" class="com.peterjxl.dao.impl.AccountDaoImpl">
<!-- 注入QueryRunner -->
<property name="runner" ref="runner"/>
<!-- 注入ConnectionUtils -->
<property name="connectionUtils" ref="connectionUtils"/>
</bean>
<!--配置QueryRunner-->
<bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"/>
<!-- 配置数据源 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!--连接数据库的必备信息-->
<property name="driverClass" value="com.mysql.cj.jdbc.Driver"/>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/learnSpring"/>
<property name="user" value="learnSpringUser"/>
<property name="password" value="learnSpringPassword"/>
</bean>
<!-- 配置Connection的工具类 ConnectionUtils -->
<bean id="connectionUtils" class="com.peterjxl.utils.ConnectionUtils">
<!-- 注入数据源-->
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 配置事务管理器-->
<bean id="txManager" class="com.peterjxl.utils.TransactionManager">
<!-- 注入ConnectionUtils -->
<property name="connectionUtils" ref="connectionUtils"/>
</bean>
</beans>
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
# 测试
此时我们再次测试转账,可以看到即使发生了异常,也能正常回滚,也就是成功使用了事务!
# 总结
虽然我们已经实现了事务,但目前项目中仍存在不少问题:
- 配置非常麻烦,很多依赖注入,引入了事务管理工具类和 connection 工具类
- 不仅仅引入了类之间的依赖,还有方法之间的依赖, 例如事务工具类有个方法名改了,所有 service 类都得跟着改
- 有很多的重复代码
有什么解决办法吗?有的,使用代理,增强我们的方法!下一篇博客就会回顾代理模式,其实之前已经讲过一些基本的概念并演示了,例如:JavaWeb-Filter 案例 (opens new window)
# 源码
本项目已将源码上传到 GitHub (opens new window) 和 Gitee (opens new window) 上。并且创建了分支 demo9,读者可以通过切换分支来查看本文的示例代码。