首页 > 代码库 > JDBC应用中的事务管理

JDBC应用中的事务管理

在开发中,对数据库的多个表或者对一个表中的多条数据执行更新操作时要保证对多个更新操作要么同时成功,要么都不成功,这就涉及到对多个更新操作的事务管理问题了。比如银行业务中的转账问题,A用户向B用户转账100元,假设A用户和B用户的钱都存储在Account表,那么A用户向B用户转账时就涉及到同时更新Account表中的A用户的钱和B用户的钱,用SQL来表示就是:

update account set money=money-100 where name=‘A‘;
update account set money=money+100 where name=‘B‘;

我们以银行业务中的转账问题来讲解JDBC开发中的事务管理,首先编写测试用的SQL脚本,如下:

/* 创建数据库 */
create database day18;

use day18;

/* 创建账户表 */
create table account 
(
    id int primary key auto_increment,
    name varchar(40),
    money float
) character set utf8 collate utf8_general_ci;

/* 插入测试数据 */
insert into account(name,money) values(‘aaa‘,1000);
insert into account(name,money) values(‘bbb‘,1000);
insert into account(name,money) values(‘ccc‘,1000); 

在数据访问层(Dao)中处理事务

在cn.itcast.domain包下创建一个封装数据的实体——Account.java,对应数据库中的account表。Account类的具体代码如下:

public class Account {
    private int id;
    private String name;
    private double money;
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public double getMoney() {
        return money;
    }
    public void setMoney(double money) {
        this.money = money;
    }

}

对于这样的同时更新一个表中的多条数据的操作,那么必须保证要么同时成功,要么都不成功,所以需要保证这两个update操作在同一个事务中进行。在开发中,我们可能会在AccountDao里写一个转账处理方法。现在在cn.itcast.dao包下创建AccountDao类,该类用于处理银行业务中的转账问题。

public class AccountDao {

    // 从aaa账户向bbb账户转100元,像下面这样写违背了三层架构设计思想,在实际开发里面,AccountDao只提供增删改查的方法,所有的业务逻辑都在service里面做
    public void transfer() throws SQLException {
        /*
         * 现在把2条sql语句作为一个整体执行,这时就不能这样写:
         * QueryRunner runner = new QueryRunner(JdbcUtils.getDataSource());
         * 如果你给其连接池,等会你在调用runner的方法做转账的时候,在连接发完sql语句之后,就将连接给关了,
         * 你就没办法把2条sql语句作为一个整体执行,这时就不能给其一个连接池。
         */
        Connection conn = null;

        try {
            conn = JdbcUtils.getConnection();
            conn.setAutoCommit(false); // 开启事务

            QueryRunner runner = new QueryRunner();
            String sql1 = "update account set money=money-100 where name=‘aaa‘";
            runner.update(conn, sql1);

            String sql2 = "update account set money=money+100 where name=‘bbb‘";
            runner.update(conn, sql2);

            conn.commit(); // 提交事务
        } finally {
            if (conn != null) {
                conn.close();
            }
        }
    }

}

我们在应用程序中加入了dbcp连接池,还有关于JdbcUtils类怎么写,可以参考我的笔记Apache的DBUtils框架学习——使用DBUtils完成数据库的CRUD。
上面AccountDao的这个transfer方法虽然可以处理转账业务,并且保证了在同一个事务中进行,但是AccountDao的这个transfer方法是处理两个用户之间的转账业务的,已经涉及到具体的业务操作,应该在业务层中做,不应该出现在Dao层的。在开发中,Dao层的职责应该只涉及到基本的CRUD,不涉及具体的业务操作,所以在开发中Dao层出现这样的业务处理方法是一种不好的设计。
总结编写以上代码的过程中,我们一定要注意2点

  • 现在把2条sql语句作为一个整体执行,这时就不能这样写:

    QueryRunner runner = new QueryRunner(JdbcUtils.getDataSource());

    如果你给其连接池,等会你在调用runner的方法做转账的时候,在连接发完sql语句之后,就会将连接给关了,你就没办法把2条sql语句作为一个整体执行,所以这时就不能给其一个连接池。

  • 从aaa账户向bbb账户转100元,像上面这样写违背了三层架构设计思想。在实际开发里面,AccountDao只提供增删改查的方法,所有的业务逻辑都在service里面做。

在业务层(BusinessService)处理事务

由于上述AccountDao存在具体的业务处理方法,导致AccountDao的职责不够单一,下面我们对AccountDao进行改造,让AccountDao的职责只是做CRUD操作,将事务的处理挪到业务层(BusinessService),改造后的AccountDao如下:

public class AccountDao {

    // 接收service层传递过来的Connection对象
    private Connection conn;

    public AccountDao(Connection conn) {
        this.conn = conn;
    }

    public AccountDao() {

    }

    // 在实际开发里面,转账应该这样写
    public void update(Account a) {
        try {
            QueryRunner runner = new QueryRunner();
            String sql = "update account set money=? where id=?";
            Object[] params = {a.getMoney(), a.getId()};
            // 使用service层传递过来的Connection对象操作数据库
            runner.update(conn, sql, params);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    public Account find(int id) {
        try {
            QueryRunner runner = new QueryRunner();
            String sql = "select * from account where id=?";
            // 使用service层传递过来的Connection对象操作数据库
            return (Account) runner.query(conn, sql, id, new BeanHandler(Account.class));
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

}

接着在cn.itcast.service包下创建一个类BusinessService,用于在业务逻辑层(BusinessService)中处理转账业务。BusinessService类的具体代码如下:

public class BusinessService {

    @Test
    public void test() throws SQLException {
        transfer1(1, 2, 100);
    }

    /*
     * 在实际开发里面,这样写同样不优雅,最优雅的办法有:
     * 1. 用spring进行事务管理
     * 2. 用ThreadLocal类进行事务管理
     */
    public void transfer1(int sourceid, int targetid, double money) throws SQLException {

        Connection conn = null;

        try {
            conn = JdbcUtils.getConnection();
            conn.setAutoCommit(false);

            AccountDao dao = new AccountDao(conn);

            Account a = dao.find(sourceid); // select
            Account b = dao.find(targetid); // select

            a.setMoney(a.getMoney() - money);
            b.setMoney(b.getMoney() + money);

            dao.update(a); // update

            // int x = 1/0;

            dao.update(b); // update

            conn.commit();
        } finally {
            if (conn != null) {
                conn.close();
            }
        }
    }
}

程序经过这样改造之后就比刚才好多了,AccountDao只负责CRUD,里面没有具体的业务处理方法了,职责就单一了,而BusinessService则负责具体的业务逻辑和事务的处理,需要操作数据库时,就调用AccountDao层提供的CRUD方法操作数据库。
但是,在实际开发里面,向上面这样写同样不优雅,最优雅的办法有:

  1. 用Spring进行事务管理。
  2. 用ThreadLocal类进行事务管理。

使用ThreadLocal类进行更加优雅的事务处理

上面的在BusinessService层这种处理事务的方式依然不够优雅,为了能够让事务处理更加优雅,我们使用ThreadLocal类进行改造。ThreadLocal是一个容器,向这个容器存储的对象,在当前线程范围内都可以取得出来,向ThreadLocal里面存东西就是向它里面的Map存东西的,然后ThreadLocal把这个Map挂到当前的线程底下,这样Map就只属于这个线程了
查看JDK API 1.6.0文档,发现ThreadLocal类有2个主要的方法:

  • public void set(T value)
    原理:ThreadLocal是一个容器,向ThreadLocal里面存东西就是向它里面的Map存东西。
    例如,有如下这样的代码:

    ThreadLocal<Connection> threadLocal = new ThreadLocal<Connection>();
    Connection conn = ......
    threadLocal.set(conn);

    threadLocal.set(conn);这句代码的意思就是:得到当前线程,以当前线程对象为关键字将数据库连接conn存放到Map集合中,即map.put(thread, conn);

  • public T get()
    原理:得到当前线程,以当前线程对象为关键字从Map集合中检索出前面绑定的Connection。
    例如,有如下这样的代码:

    ThreadLocal<Connection> threadLocal = new ThreadLocal<Connection>();
    Connection conn = threadLocal.get();

    Connection conn = threadLocal.get();这句代码的原理就是:

    Thread thread = Thread.currentThread();
    Connection conn = threadLocal.get(thread);

使用ThreadLocal类进行改造数据库连接工具类JdbcUtils,改造后的代码如下:

public class JdbcUtils {
    private static DataSource ds = null;

    // static特性:随着类加载而加载,这要这个类加载,JVM的内存里面就有一个ThreadLocal对象,并且这个ThreadLocal对象永远存在,除非JVM退出
    private static ThreadLocal<Connection> tl = new ThreadLocal<Connection>();

    static {
        try {
            Properties prop = new Properties();
            InputStream in = JdbcUtils.class.getClassLoader().getResourceAsStream("dbcpconfig.properties");
            prop.load(in);
            BasicDataSourceFactory factory = new BasicDataSourceFactory();
            ds = factory.createDataSource(prop);
        } catch (Exception e) {
            throw new ExceptionInInitializerError(e);
        }
    }

    public static DataSource getDataSource() {
        return ds;
    }

    public static Connection getConnection() throws SQLException {

        try {
            // 得到当前线程上绑定的连接
            Connection conn = tl.get();
            if (conn == null) { // 代表线程上没有绑定连接
                conn = ds.getConnection();
                tl.set(conn);
            }
            return conn;
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }

        // return ds.getConnection();
    }

    public static void startTransaction() {
        try {
            // 得到当前线程上绑定的连接,并开启事务
            Connection conn = tl.get();
            if (conn == null) { // 代表线程上没有绑定连接
                conn = ds.getConnection();
                tl.set(conn);
            }
            conn.setAutoCommit(false);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    public static void commitTransaction() {
        try {
            // 得到当前线程上绑定的连接,并提交事务
            Connection conn = tl.get();
            if (conn != null) { // 代表当前线程上绑定了连接,当前线程有连接才提交,当前线程没有连接就不用提交
                conn.commit();
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    // 关闭连接
    public static void closeConnection() {
        try {
            // 得到当前线程上绑定的连接,并关闭该连接
            Connection conn = tl.get();
            if (conn != null) { 
                conn.close();
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            /*
             * 关闭连接之后,即还给数据库连接池了,还要从ThreadLocal容器里面移除掉这个连接。
             * 
             * 如果不移除,会有什么问题?
             * 有一个线程来执行了转账,ThreadLocal的map集合里面就有一个连接,
             * 第二个线程又来,ThreadLocal的map集合里面又有一个连接,
             * 第三个线程又来,ThreadLocal的map集合里面又有一个连接,
             * 而ThreadLocal又是静态的,即整个应用程序周期范围内都存在,那这个容器就会越来越大,最后导致数据溢出。
             * 所以静态的东西要慎用!!!
             */
            tl.remove(); // 千万注意:解除当前线程上绑定的连接(从ThreadLocal容器中移除掉对应当前线程的连接)
        }
    }

}

注意数据库连接工具类JdbcUtils,我们一定要注意关闭连接的代码。如果我们这样写:

// 关闭连接
public static void closeConnection() {
    try {
        // 得到当前线程上绑定的连接,并关闭该连接
        Connection conn = tl.get();
        if (conn != null) { 
            conn.close();
        }
    } catch (SQLException e) {
        throw new RuntimeException(e);
    }
}

整个应用程序会有很大的缺陷。我们一定要在关闭连接之后(即还给数据库连接池了),还要记得从ThreadLocal容器里面移除掉这个连接。
如果不移除,会有什么问题?
答:有一个线程来执行了转账,ThreadLocal的map集合里面就有一个连接;第二个线程又来,ThreadLocal的map集合里面又有一个连接;第三个线程又来,ThreadLocal的map集合里面又有一个连接……,而ThreadLocal又是静态的,即整个应用程序周期范围内都存在,那这个容器就会越来越大,最后导致数据溢出。记住静态的东西要慎用!!!所以关闭连接的正确代码应该为:

// 关闭连接
public static void closeConnection() {
    try {
        // 得到当前线程上绑定的连接,并关闭该连接
        Connection conn = tl.get();
        if (conn != null) { 
            conn.close();
        }
    } catch (SQLException e) {
        throw new RuntimeException(e);
    } finally {
        tl.remove(); // 千万注意:解除当前线程上绑定的连接(从ThreadLocal容器中移除掉对应当前线程的连接)
    }
}

对AccountDao进行改造,数据库连接对象不再需要service层传递过来,而是直接从JdbcUtils提供的getConnection方法去获取,改造后的AccountDao如下:

public class AccountDao {

    // 在实际开发里面,转账应该这样写
    public void update(Account a) {
        try {
            QueryRunner runner = new QueryRunner();
            String sql = "update account set money=? where id=?";
            Object[] params = {a.getMoney(), a.getId()};
            runner.update(JdbcUtils.getConnection(), sql, params);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    public Account find(int id) {
        try {
            QueryRunner runner = new QueryRunner();
            String sql = "select * from account where id=?";
            return (Account) runner.query(JdbcUtils.getConnection(), sql, id, new BeanHandler(Account.class));
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

}

对BusinessService进行改造,service层不再需要传递数据库连接Connection给Dao层,改造后的BusinessService如下:

public class BusinessService {

    @Test
    public void test() throws SQLException {
        transfer2(1, 2, 100);
    }

    // 用上ThreadLocal类进行事务管理
    public void transfer2(int sourceid, int targetid, double money) throws SQLException {

        try {
            JdbcUtils.startTransaction(); // 当前线程上已经绑定了一个开启事务的连接
            AccountDao dao = new AccountDao();
            Account a = dao.find(sourceid); // select
            Account b = dao.find(targetid); // select

            a.setMoney(a.getMoney() - money);
            b.setMoney(b.getMoney() + money);

            dao.update(a); // update

            // int x = 1/0;

            dao.update(b); // update

            JdbcUtils.commitTransaction();
        } finally {
            JdbcUtils.closeConnection();
        }
    }

}

这样在service层对事务的处理看起来就更加优雅了。ThreadLocal类在开发中使用得是比较多的,程序运行中产生的数据要想在一个线程范围内共享,只需要把数据使用ThreadLocal进行存储即可
我们可以用图来表示,会更加利于理解:
技术分享
但是如果Servlet将请求转发给另一个Servlet,情况就大不一样了。参见下图:
技术分享
上面出现的问题又该怎么解决呢?我们只须把所有Service层的业务代码放到一个事务里面,那怎么做呢?解决方法是:使用事务过滤器,那么一次请求范围内的所有操作都将在一个事务里面了。如下图:
技术分享
不急,我们以后会详细讲解事务过滤器的!!!

<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    JDBC应用中的事务管理