银行转账案例分析事务问题
银行转账案例增删改查搭建:#
坐标引入:

<?xml version="1.0" encoding="UTF-8"?> <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/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.yyj</groupId> <artifactId>day03_eesy_01account</artifactId> <version>1.0-SNAPSHOT</version> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>6</source> <target>6</target> </configuration> </plugin> </plugins> </build> <packaging>jar</packaging> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.0.2.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.0.2.RELEASE</version> </dependency> <dependency> <groupId>commons-dbutils</groupId> <artifactId>commons-dbutils</artifactId> <version>1.4</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.6</version> </dependency> <dependency> <groupId>c3p0</groupId> <artifactId>c3p0</artifactId> <version>0.9.1.2</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> </dependencies> </project>
数据库连接工具类:

package com.yyj.utils; import javax.sql.DataSource; import java.sql.Connection; /** * 连接的工具类,它用于从数据源中获取一个连接,并且实现和线程的绑定 */ public class ConnectionUtils { 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(); } }
实体类:

package com.yyj.domain; import java.io.Serializable; /** * 账户的实体类 */ public class Account implements Serializable { private Integer id; private String name; private Float money; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Float getMoney() { return money; } public void setMoney(Float money) { this.money = money; } @Override public String toString() { return "Account{" + "id=" + id + ", name='" + name + '\'' + ", money=" + money + '}'; } }
Dao层接口:

package com.yyj.dao; import com.yyj.domain.Account; import java.util.List; /** * 账户的持久层接口 */ public interface IAccountDao { /** * 查询所有 * @return */ List<Account> findAllAccount(); /** * 查询一个 * @return */ Account findAccountById(Integer accountId); /** * 保存 * @param account */ void saveAccount(Account account); /** * 更新 * @param account */ void updateAccount(Account account); /** * 删除 * @param acccountId */ void deleteAccount(Integer acccountId); /** * 根据名称查询账户 * @param accountName * @return 如果有唯一的一个结果就返回,如果没有结果就返回null * 如果结果集超过一个就抛异常 */ Account findAccountByName(String accountName); }
Dao层实现类:

package com.yyj.dao.impl; import com.yyj.dao.IAccountDao; import com.yyj.domain.Account; import com.yyj.utils.ConnectionUtils; import org.apache.commons.dbutils.QueryRunner; import org.apache.commons.dbutils.handlers.BeanHandler; import org.apache.commons.dbutils.handlers.BeanListHandler; import java.util.List; /** * 账户的持久层实现类 */ 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 (Exception e) { throw new RuntimeException(e); } } @Override public Account findAccountById(Integer accountId) { try { return runner.query(connectionUtils.getThreadConnection(), "select * from account where id = ? ", new BeanHandler<Account>(Account.class), accountId); } catch (Exception e) { throw new RuntimeException(e); } } @Override public void saveAccount(Account account) { try { runner.update(connectionUtils.getThreadConnection(), "insert into account(name,money)values(?,?)", account.getName(), account.getMoney()); } catch (Exception e) { throw new RuntimeException(e); } } @Override public void updateAccount(Account account) { try { runner.update(connectionUtils.getThreadConnection(), "update account set name=?,money=? where id=?", account.getName(), account.getMoney(), account.getId()); } catch (Exception e) { throw new RuntimeException(e); } } @Override public void deleteAccount(Integer accountId) { try { runner.update(connectionUtils.getThreadConnection(), "delete from account where id=?", accountId); } catch (Exception e) { throw new RuntimeException(e); } } @Override public Account findAccountByName(String accountName) { try { List<Account> accounts = runner.query(connectionUtils.getThreadConnection(), "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 (Exception e) { throw new RuntimeException(e); } } }
业务层接口:

package com.yyj.service; import com.yyj.domain.Account; import java.util.List; /** * 账户的业务层接口 */ public interface IAccountService { /** * 查询所有 * @return */ List<Account> findAllAccount(); /** * 查询一个 * @return */ Account findAccountById(Integer accountId); /** * 保存 * @param account */ void saveAccount(Account account); /** * 更新 * @param account */ void updateAccount(Account account); /** * 删除 * @param acccountId */ void deleteAccount(Integer acccountId); /** * 转账 * @param sourceName 转出账户名称 * @param targetName 转入账户名称 * @param money 转账金额 */ void transfer(String sourceName,String targetName,Float money); //void test();//它只是连接点,但不是切入点,因为没有被增强 }
业务层实现类:

package com.yyj.service.impl; import com.yyj.dao.IAccountDao; import com.yyj.domain.Account; import com.yyj.service.IAccountService; import java.util.List; /** * 账户的业务层实现类 * * 事务控制应该都是在业务层 */ public class AccountServiceImpl implements IAccountService{ private IAccountDao accountDao; public void setAccountDao(IAccountDao accountDao) { this.accountDao = accountDao; } @Override public List<Account> findAllAccount() { return accountDao.findAllAccount(); } @Override public Account findAccountById(Integer accountId) { return accountDao.findAccountById(accountId); } @Override public void saveAccount(Account account) { accountDao.saveAccount(account); } @Override public void updateAccount(Account account) { accountDao.updateAccount(account); } @Override public void deleteAccount(Integer accountId) { accountDao.deleteAccount(accountId); } @Override public void transfer(String sourceName, String targetName, Float money) { System.out.println("transfer...."); //2.1根据名称查询转出账户 Account source = accountDao.findAccountByName(sourceName); //2.2根据名称查询转入账户 Account target = accountDao.findAccountByName(targetName); //2.3转出账户减钱 source.setMoney(source.getMoney()-money); //2.4转入账户加钱 target.setMoney(target.getMoney()+money); //2.5更新转出账户 accountDao.updateAccount(source); // int i=1/0; //2.6更新转入账户 accountDao.updateAccount(target); } }
配置文件bean.xml:

<?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="proxyAccountService" factory-bean="beanFactory" factory-method="getAccountService"></bean> <!--配置beanfactory--> <bean id="beanFactory" class="com.yyj.factory.BeanFactory"> <!-- 注入service --> <property name="accountService" ref="accountService"></property> <!-- 注入事务管理器 --> <property name="txManager" ref="txManager"></property> </bean> <!-- 配置Service --> <bean id="accountService" class="com.yyj.service.impl.AccountServiceImpl"> <!-- 注入dao --> <property name="accountDao" ref="accountDao"></property> </bean> <!--配置Dao对象--> <bean id="accountDao" class="com.yyj.dao.impl.AccountDaoImpl"> <!-- 注入QueryRunner --> <property name="runner" ref="runner"></property> <!-- 注入ConnectionUtils --> <property name="connectionUtils" ref="connectionUtils"></property> </bean> <!--配置QueryRunner--> <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"></bean> <!-- 配置数据源 --> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <!--连接数据库的必备信息--> <property name="driverClass" value="com.mysql.jdbc.Driver"></property> <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/eesy"></property> <property name="user" value="root"></property> <property name="password" value="123"></property> </bean> <!-- 配置Connection的工具类 ConnectionUtils --> <bean id="connectionUtils" class="com.yyj.utils.ConnectionUtils"> <!-- 注入数据源--> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 配置事务管理器--> <bean id="txManager" class="com.yyj.utils.TransactionManager"> <!-- 注入ConnectionUtils --> <property name="connectionUtils" ref="connectionUtils"></property> </bean> </beans>
测试类:

package com.yyj.test; import com.yyj.service.IAccountService; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; /** * 使用Junit单元测试:测试我们的配置 */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = "classpath:bean.xml") public class AccountServiceTest { @Autowired @Qualifier("proxyAccountService") private IAccountService as; @Test public void testTransfer(){ as.transfer("aaa","bbb",100f); } }
转账中存在的问题:#
事务被自动控制了。换言之,我们使用了 connection 对象的 setAutoCommit(true)此方式控制事务,如果我们每次都执行一条 sql 语句,没有问题,但是如果业务方法一次要执行多条 sql语句,这种方式就无法实现功能了。
当我们执行转账中添加int i = 1/0; 时,由于执行有异常,转账失败。但是因为我们是每次执行持久层方法都是独立事务,导致无法实现事务控制(不符合事务的一致性)
问题的解决方式:#
添加事务控制类:

package com.yyj.utils; /** * 和事务管理相关的工具类,它包含了,开启事务,提交事务,回滚事务和释放连接 */ public class TransactionManager { private ConnectionUtils connectionUtils; public void setConnectionUtils(ConnectionUtils connectionUtils) { this.connectionUtils = connectionUtils; } /** * 开启事务 */ public void beginTransaction() { try { connectionUtils.getThreadConnection().setAutoCommit(false); } catch (Exception e) { e.printStackTrace(); } } /** * 提交事务 */ public void commit() { try { connectionUtils.getThreadConnection().commit(); } catch (Exception e) { e.printStackTrace(); } } /** * 回滚事务 */ public void rollback() { try { connectionUtils.getThreadConnection().rollback(); } catch (Exception e) { e.printStackTrace(); } } /** * 释放连接 */ public void release() { try { connectionUtils.getThreadConnection().close();//还回连接池中 /** 将连接对象还回连接池之后,通过connectionUtils.getThreadConnection() * 还是可以获取一个连接对象只是连接已经关闭了会报错 * 所以关闭连接还回连接池中后需要将该线程也还回连接池中 **/ connectionUtils.removeConnection(); } catch (Exception e) { e.printStackTrace(); } } }
业务层进行事务控制:

package com.yyj.service.impl; import com.yyj.dao.IAccountDao; import com.yyj.domain.Account; import com.yyj.service.IAccountService; import com.yyj.utils.TransactionManager; import java.util.List; /** * 账户的业务层实现类 * <p> * 事务控制应该都是在业务层 */ public class AccountServiceImpl_OLD implements IAccountService { private IAccountDao accountDao; private TransactionManager txManager; public void setTxManager(TransactionManager txManager) { this.txManager = txManager; } public void setAccountDao(IAccountDao accountDao) { this.accountDao = accountDao; } @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(); } } @Override public Account findAccountById(Integer accountId) { try { //1.开启事务 txManager.beginTransaction(); //2.执行操作 Account account = accountDao.findAccountById(accountId); //3.提交事务 txManager.commit(); //4.返回结果 return account; } catch (Exception e) { //5.回滚操作 txManager.rollback(); throw new RuntimeException(e); } finally { //6.释放连接 txManager.release(); } } @Override public void saveAccount(Account account) { try { //1.开启事务 txManager.beginTransaction(); //2.执行操作 accountDao.saveAccount(account); //3.提交事务 txManager.commit(); } catch (Exception e) { //4.回滚操作 txManager.rollback(); } finally { //5.释放连接 txManager.release(); } } @Override public void updateAccount(Account account) { try { //1.开启事务 txManager.beginTransaction(); //2.执行操作 accountDao.updateAccount(account); //3.提交事务 txManager.commit(); } catch (Exception e) { //4.回滚操作 txManager.rollback(); } finally { //5.释放连接 txManager.release(); } } @Override public void deleteAccount(Integer acccountId) { try { //1.开启事务 txManager.beginTransaction(); //2.执行操作 accountDao.deleteAccount(acccountId); //3.提交事务 txManager.commit(); } catch (Exception e) { //4.回滚操作 txManager.rollback(); } finally { //5.释放连接 txManager.release(); } } @Override public void transfer(String sourceName, String targetName, Float money) { try { //1.开启事务 txManager.beginTransaction(); //2.执行操作 //2.1根据名称查询转出账户 Account source = accountDao.findAccountByName(sourceName); //2.2根据名称查询转入账户 Account target = accountDao.findAccountByName(targetName); //2.3转出账户减钱 source.setMoney(source.getMoney() - money); //2.4转入账户加钱 target.setMoney(target.getMoney() + money); //2.5更新转出账户 accountDao.updateAccount(source); int i = 1 / 0; //2.6更新转入账户 accountDao.updateAccount(target); //3.提交事务 txManager.commit(); } catch (Exception e) { //4.回滚操作 txManager.rollback(); e.printStackTrace(); } finally { //5.释放连接 txManager.release(); } } }
通过对业务层改造,已经可以实现事务控制了,但是由于我们添加了事务控制,也产生了一个新的问题:
业务层方法变得臃肿了,里面充斥着很多重复代码。并且业务层方法和事务控制方法耦合了。试想一下,如果我们此时提交,回滚,释放资源中任何一个方法名变更,都需要修改业务层的代码,况且这还只是一个业务层实现类,而实际的项目中这种业务层实现类可能有十几个甚至几十个。
思考:
这个问题能不能解决呢?
答案是肯定的,使用下一小节中提到的技术。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)