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层的业务代码放到一个事务里面,那怎么做呢?解决方法是:使用事务过滤器,那么一次请求范围内的所有操作都将在一个事务里面了。如下图:

不急,我们以后会详细讲解事务过滤器的!!!

时间: 2024-08-06 15:58:06

JDBC应用中的事务管理的相关文章

[JavaEE - JPA] 3. Spring Framework中的事务管理

前文讨论了事务划分(Transaction Demarcation)在EJB中是如何实现的,本文继续介绍在Spring Framework中是如何完成事务划分的. 我们已经知道了当采用Container事务类型的时候,事务划分主要有以下两种方案(参考这里): 使用JTA接口在应用中编码完成显式划分 在容器的帮助下完成自动划分 在使用JavaEE的EJB规范时,这两种方案分别被实现为BMT以及CMT,关于BMT和CMT在上一篇文章中有比较详尽的讨论(参考这里). 那么对于Spring Framew

Spring中的事务管理

一.Spring事务管理用到的三个接口 a. PlatformTransactionManager 事务管理器 b. TransactionDefinition 事务定义信息(隔离.传播.超时.只读) c. TransactionStatus 事务具体的运行状态 二.Spring为不同的持久化框架提供了不同的PlatformTransactionManager接口实现 事务 说明 org.springframework.jdbc.datasource.DataSourceTransactionM

JavaEE中的事务管理——事务划界

前面博文中大致介绍了一下事务,其实在企业应用服务器中事务是在不同的级别上存在的.比较简单的事务是最底层的事务,就是位于资源级别的事务管理.假设数据最终要存储在一个关系型数据库中,那么最底层的事务就是位于这里.我们把这种事务称之为资源本地事务(resource-localtransaction)在不用容器的大部分情况下开发人员要面对的事务都属于这里(其他的事务面对不到是因为水平不够!).理解了数据的事务之后来解决资源本地事务就易如反掌了.如果使用了企业应用服务器那么要处理的大多数事务就是JavaT

Spring Boot中的事务管理

原文  http://blog.didispace.com/springboottransactional/ 什么是事务? 我们在开发企业应用时,对于业务人员的一个操作实际是对数据读写的多步操作的结合.由于数据操作在顺序执行的过程中,任何一步操作都有可能发生异常,异常会导致后续操作无法完成,此时由于业务逻辑并未正确的完成,之前成功操作数据的并不可靠,需要在这种情况下进行回退. 事务的作用就是为了保证用户的每一个操作都是可靠的,事务中的每一步操作都必须成功执行,只要有发生异常就回退到事务开始未进行

企业分布式微服务云SpringCloud SpringBoot mybatis (十七)Spring Boot中的事务管理

快速入门 在Spring Boot中,当我们使用了spring-boot-starter-jdbc或spring-boot-starter-data-jpa依赖的时候,框架会自动默认分别注入DataSourceTransactionManager或JpaTransactionManager.所以我们不需要任何额外配置就可以用@Transactional注解进行事务的使用. 我们以之前实现的<用spring-data-jpa访问数据库>的示例Chapter3-2-2作为基础工程进行事务的使用常识

JavaEE中的事务管理——事务概述(1)

今天打算说一说事务管理,读者可能了解也有可能不了解,其实很简单(大牛请自行绕过).本来想引用个成语的啥的来描述事务的特点,但是搜肠刮肚也没有发现合适的,于是就找了下面几组成语来描述事务性.其实在官方文档中对于事务的描述也是分四个方面来说的.这里算是用自己的理解解释一下罢了. 第一对词语是:"开弓没有回头箭"和"前功尽弃"(功亏一篑?功败垂成?) 这一对词语结合起来看就是事务的原子性,就是我们平时说的要做就做完,要不做就一点儿也不要做.其中前面的做完就是说明这个事务已

django中的事务管理

在讲解之前首先来了解一下数据库中的事务. 什么是数据库中的事务? 热心网友回答: (1):事务(Transaction)是并发控制的单位,是用户定义的一个操作序列.这些操作要么都做,要么都不做,是一个不可分割的工作单位.通过事务,SQL Server能将逻辑相关的一组操作绑定在一起,以便服务器保持数据的完整性. (2):事务通常是以BEGIN TRANSACTION开始,以COMMIT或ROLLBACK结束. COMMIT表示提交,即提交事务的所有操作.具体地说就是将事务中所有对数据库的更新写回

Java数据库连接--JDBC调用存储过程,事务管理和高级应用

相关链接:Jdbc调用存储过程 一.JDBC常用的API深入详解及存储过程的调用 1.存储过程的介绍 我们常用的操作数据库语言SQL语句在执行的时候要先进行编译,然后执行,而存储过程是在大型数据库系统中,一组为了完成特定功能的SQL语句集,存储在数据库中,经过第一次编译后再次调用不需要再次编译,用户通过制定存储过程的名字并给出参数(如果该存储过程带有参数) 来执行它.存储过程是数据库中 的一个重要对象,任何一个设计良好的数据库应用程序都应该用到存储过程. 一个存储过程是一个可编程的函数,它在数据

Java中的事务——JDBC事务和JTA事务

本文来介绍一下J2EE中和事务相关的内容,在阅读本文之前,希望读者对分布式有一定的了解. Java事务的类型有三种:JDBC事务.JTA(Java Transaction API)事务.容器事务. 常见的容器事务如Spring事务,容器事务主要是J2EE应用服务器提供的,容器事务大多是基于JTA完成,这是一个基于JNDI的,相当复杂的API实现.所以本文暂不讨论容器事务.本文主要介绍J2EE开发中两个比较基本的事务:JDBC事务和JTA事务. JDBC事务 JDBC事务,就是在Java中用来控制