本节内容
数据库读写中的事务管理,是我之前不怎么接触的部分,本节主要通过一个简单的转账的功能来理解一下事务管理。
代码展示
我们在之前项目的代码基础上,改写AccountService代码实现一个转账的功能。
首先在接口中添加transfer方法,然后在Impl进行实现,自此我们也可以知道为什么要分为Service层和Dao两个层,service中的方法具有业务属性,而Dao中只是对数据库里面对记录进行操作并不具有业务属性。
@Service(value="accountService") public class AccountServiceImpl implements IAccountService { @Resource(name = "accountDao") private IAccountDao accountDao;//在Dao里面实现数据库的配置以及相关的读写操作,通过一个QueryRunner queryRunner这个对象进行,在程序执行的时候会找到这个bean并使用其中的操作方法。 public void setAccountDao(IAccountDao accountDao) { this.accountDao = accountDao; } public void transfer(int idFrom, int idTo, float money){ //先查找到相关的账户 Account fromAccount = accountDao.findAccountById(idFrom); Account toAccount = accountDao.findAccountById(idTo); #更新金额 fromAccount.setMoney(fromAccount.getMoney()-money); toAccount.setMoney(toAccount.getMoney()+money); #更新相应的数据库 accountDao.updateAccount(fromAccount); accountDao.updateAccount(toAccount); } }
添加完了之后,我们进行运行:
public class Client { public static void main(String[] args){ AnnotationConfigApplicationContext ap = new AnnotationConfigApplicationContext(SpringConfigration.class); AccountServiceImpl ac = ap.getBean("accountService", AccountServiceImpl.class); ac.transfer(2,3,100); ap.close(); } }
可以看到数据库里面的数据得到了正常的更新。
mysql> select * from account; +----+------+-------+ | id | name | money | +----+------+-------+ | 2 | bbb | 900 | | 3 | ccc | 1100 | | 4 | eeee | 3333 | | 5 | ppp | 100 | | 6 | ppp | 100 | | 7 | ppp | 100 | +----+------+-------+
事务问题
但是这里面有一个问题,如果我把transfer中手动插入一个异常
public void transfer(int idFrom, int idTo, float money){ //先查找到相关的账户 Account fromAccount = accountDao.findAccountById(idFrom); Account toAccount = accountDao.findAccountById(idTo); #更新金额 fromAccount.setMoney(fromAccount.getMoney()-money); toAccount.setMoney(toAccount.getMoney()+money); #更新相应的数据库 accountDao.updateAccount(fromAccount); accountDao.updateAccount(toAccount); }
再次查询就发现,账户id为2的用户金额减少了,到那时id=3的用户由于发生了异常并没有得到更新,因此就出现了不一致的事务。
mysql> select * from account; +----+------+-------+ | id | name | money | +----+------+-------+ | 2 | bbb | 800 | | 3 | ccc | 1100 | | 4 | eeee | 3333 | | 5 | ppp | 100 | | 6 | ppp | 100 | | 7 | ppp | 100 | +----+------+-------+
为了对这个问题进行改善,我们需要将两个操作放在一个事务中进行,要失败同时失败,要成功那么也需要同时成功。
优化方法
对与这个问题我们知道我们配置对QueryRunner是prototype队列类型,因此在tranfer中会有不同对链接执行数据库对操作,而我们要做对就是要将这四个操作放在一次事务中进行执行,要么成功,要么失败。我们采用对做法是使用ThreadLocal来进行解决。
public void transfer(int idFrom, int idTo, float money){ //先查找到相关的账户 Account fromAccount = accountDao.findAccountById(idFrom); //会获取一个链接 Account toAccount = accountDao.findAccountById(idTo);//获取另外一个链接 #更新金额 fromAccount.setMoney(fromAccount.getMoney()-money); toAccount.setMoney(toAccount.getMoney()+money); #更新相应的数据库 accountDao.updateAccount(fromAccount);//获取再另外一个链接 accountDao.updateAccount(toAccount);//获取再再另外一个链接 }
使用ThreadLocal对数据库对链接进行管理,也就是说将链接和线程绑定,同一个线程用的一定是相同的链接,然后我们要自己写一个事务管理类,针对一个链接进行事务的开启,提交,回滚和释放。
我们需要新增两个类:ConnectionUtil和TxManager,然后修改AccountServiceImpl和AccountDaoImpl中的代码,因为以前QueryRunner会自动化获取链接,而本次我们希望它使用的链接对一个线程来说是固定,所以需要适配ConnectionUtil。
代码修改
package com.lujuan.util; import javax.sql.DataSource; import java.sql.Connection; /** * @项目: spring-learn-demo * @描述: 连接池管理,主要是把数据库对链接和线程进行绑定,保证同一个线程使用的是同一个链接 * @作者: lujuan03 * @创建日期: 2020-04-11 **/ public class ConnectionUtil { private ThreadLocal<Connection> tl = new ThreadLocal<Connection>(); public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; } private DataSource dataSource; public Connection getThreadConnection(){ try {//先尝试获取 Connection conn = tl.get(); if (conn == null) { conn = dataSource.getConnection(); tl.set(conn); } return conn; }catch (Exception e){ throw new RuntimeException(e); } } public void remove(){ tl.remove(); } }
然后是事务管理类:
package com.lujuan.util; /** * @项目: spring-learn-demo * @描述: 事务管理:通过connection进行的事务管理 * @作者: lujuan03 * @创建日期: 2020-04-11 **/ public class TxManager { private ConnectionUtil connectionUtil; public void setConnectionUtil(ConnectionUtil connectionUtil) { this.connectionUtil = connectionUtil; } //开启事务 public void beginTx(){ try{ this.connectionUtil.getThreadConnection().setAutoCommit(false); }catch (Exception e){ e.printStackTrace(); } } //提交事务 public void commitTx(){ try{ this.connectionUtil.getThreadConnection().commit(); }catch (Exception e){ e.printStackTrace(); } } //回滚 public void rollbackTx(){ try{ this.connectionUtil.getThreadConnection().rollback(); }catch (Exception e){ e.printStackTrace(); } } //释放链接 public void releaseTx(){ try{ this.connectionUtil.getThreadConnection().close(); this.connectionUtil.remove(); }catch (Exception e){ e.printStackTrace(); } } }
然后是我们service层代码和dao层代码的修改
@Repository("accountDao") public class AccountDaoImpl implements IAccountDao { @Resource(name = "queryRunner") private QueryRunner queryRunner;//这是commons.dbutils提供的 public void setQueryRunner(QueryRunner queryRunner) { this.queryRunner = queryRunner; } @Resource(name = "connectionUtil") private ConnectionUtil connectionUtil; public void setConnectionUtil(ConnectionUtil connectionUtil) { this.connectionUtil = connectionUtil; } public List<Account> findAllAccount() { List<Account> reV = null; try{ //查询之后顺便映射为实体类 reV = queryRunner.query(connectionUtil.getThreadConnection(),"select * from account", new BeanListHandler<Account>(Account.class)); }catch (Exception e){ throw new RuntimeException(e); } return reV; } public Account findAccountById(Integer id) { Account reV = null; try{ reV = queryRunner.query(connectionUtil.getThreadConnection(), "select * from account where id = ?", new BeanHandler<Account>(Account.class), id); }catch (Exception e){ throw new RuntimeException(e); } return reV; } public void addAccount(Account account) { try{ queryRunner.update(connectionUtil.getThreadConnection(), "insert into account(name, money) values (?, ?)", account.getName(), account.getMoney()); }catch (Exception e){ throw new RuntimeException(e); } } public void deleteAccountById(Integer id) { try{ queryRunner.update(connectionUtil.getThreadConnection(), "delete from account where id = ?", id); }catch (Exception e){ throw new RuntimeException(e); } } public void updateAccount(Account account) { try{ queryRunner.update(connectionUtil.getThreadConnection(), "update account set name= ?, money = ? where id = ?", account.getName(), account.getMoney(), account.getId()); }catch (Exception e){ throw new RuntimeException(e); } } }
service层
@Service(value="accountService") public class AccountServiceImpl implements IAccountService { @Resource(name = "accountDao") private IAccountDao accountDao; @Resource(name = "txManager") private TxManager txManager; public void setAccountDao(IAccountDao accountDao) { this.accountDao = accountDao; } public void transfer(int idFrom, int idTo, float money){ try{ //开启事务 txManager.beginTx(); //执行操作 Account fromAccount = accountDao.findAccountById(idFrom); Account toAccount = accountDao.findAccountById(idTo); fromAccount.setMoney(fromAccount.getMoney()-money); toAccount.setMoney(toAccount.getMoney()+money); accountDao.updateAccount(fromAccount); //int a = 1/0; accountDao.updateAccount(toAccount); //提交事务 txManager.commitTx(); }catch (Exception e){ txManager.rollbackTx(); }finally { txManager.releaseTx(); } } public List<Account> findAllAccount() { return accountDao.findAllAccount(); } public Account findAccountById(Integer id) { return accountDao.findAccountById(id); } public void addAccount(Account account) { accountDao.addAccount(account); } public void deleteAccountById(Integer id) { accountDao.deleteAccountById(id); } public void updateAccount(Account account) { accountDao.updateAccount(account); } public void saveAccount() { System.out.println("AccountServiceImpl: saveAccount"); } @PostConstruct public void init(){ System.out.println("init......"); } @PreDestroy public void destroy(){ System.out.println("destroy......"); } }
其余的改动就是要把txManager和connectionUtil这两个bean对象在xml文件中进行配置。然后我们执行tranfer方法,就发现转账即使在异常发生的情况下数据库里面的数据也是正常的。
只是在这里我们看到如果对所有的操作都开启事务管理,那么将有很多胶水代码,在接下来我们将学习如何移除这些胶水代码,使用更加又没的方法进行事务管理。