本节内容

数据库读写中的事务管理,是我之前不怎么接触的部分,本节主要通过一个简单的转账的功能来理解一下事务管理。

代码展示

我们在之前项目的代码基础上,改写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方法,就发现转账即使在异常发生的情况下数据库里面的数据也是正常的。
只是在这里我们看到如果对所有的操作都开启事务管理,那么将有很多胶水代码,在接下来我们将学习如何移除这些胶水代码,使用更加又没的方法进行事务管理。