事务管理在三层架构中应用以及使用ThreadLocal再次重构

  本篇将详细讲解如何正确地在实际开发中编写事务处理操作,以及在事务处理的过程中使用ThreadLocal的方法。

  在前面两篇博客中已经详细地介绍和学习了DbUtils这个Apache的工具类,那么在本篇中将全部使用DbUtils来编写我们的代码,简化操作嘛,由于本篇主要讲解事务,因此如果不懂事务,可以先看之前的博客《使用JDBC进行数据库的事务操作(1)》和《使用JDBC进行数据库的事务操作(2)》。

  在博客《使用JDBC进行数据库的事务操作(2)》中我们已经学习了使用JDBC来操作事务,那么这篇博客又有什么不同呢。答案在于在之前写的博客只是对JDBC操作事务进行了简单的演示,但不适用于实际开发,不适用的原因并不是技术的问题,而是规范的问题,因此本篇将一步步优雅地演示如何在开发中使用事务。

  本篇的前提,由于之前《使用JDBC进行数据库的事务操作(2)》比较早,未使用一些第三方jar包工具,因此这里重新创建这篇博客中的工程,在本篇中将使用DBCP连接池和DbUtils数据库工具类。而数据库和表都和之前的博客一样:

  创建数据库和表,另外再添加两条数据:

    create database jdbcdemo;

    use jdbcdemo;

    create table account(
    id int primary key auto_increment,
    name varchar(40),
    money double
    );

    insert into account(name,money) values('a',1000);
    insert into account(name,money) values('b',1000);

导入我们需要的第三方jar包:

  

例1

  那么这里我们基本可以像《使用JDBC进行数据库的事务操作(2)》中的例1 一样进行事务处理的操作,我们对其进行使用DbUtils的改写:

 1 public class AccountDao {
 2     // A向B转账100元
 3     public void transfer() throws SQLException {
 4         Connection conn = null;
 5         try{
 6             conn = JdbcUtils.getDataSource().getConnection();
 7             conn.setAutoCommit(false);  //开启事务
 8             
 9             QueryRunner runner = new QueryRunner();
10             String sql1="update account set money=money-100 where name='a'";
11             runner.update(conn,sql1);
12             
13             String sql2="update account set money=money+100 where name='b'";
14             runner.update(conn,sql2);
15             
16             conn.commit();    //提交事务        
17         }finally{
18             if(conn!=null) {
19                 conn.close();
20             }
21         }
22     }
23 }
View Code

  这就是我们之前所学的使用JDBC来操作事务,虽然小小地利用DbUtils改造了下,但这个方法还是可以执行的,我们说过本篇对于事务的重新认识不是在于技术,而是在于规范。上面的代码确实可以执行,执行结果也没问题,问题在于不规范。

  例1不规范的地方在于dao层,也就是数据访问层,只能是增删改查(CRUD),上面的代码明显是业务逻辑的操作,因此应该在dao层对该事务的数据只能有CRUD,而业务逻辑放在service层。

例2

  现在对例1的代码根据三层架构进行分离,首先在dao层 要根据数据库中account表生成一个Account这种JavaBean,这里就省略了。

  接着在AccountDao中不能像刚才那样直接进行事务处理,而是只能有CRUD,这个例子中只用到了更新和查找,因此这里只写这两个方法:

 1 public class AccountDao {
 2     private Connection conn;
 3 
 4     public AccountDao() {
 5         super();
 6     }
 7     
 8     public AccountDao(Connection conn) {
 9         this.conn = conn; //指定使用的连接
10     }
11 
12     public void update(Account a) throws SQLException {
13         QueryRunner runner = new QueryRunner();  //必须使用无参的构造器
14         String sql = "update account set money=? where id=?";
15         Object[] params={a.getMoney(),a.getId()};
16         runner.update(this.conn, sql, params);
17     }
18     
19     public Account find(int id) throws SQLException {
20         QueryRunner runner = new QueryRunner();  //必须使用无参的构造器
21         String sql = "select * from account where id=?";
22         Account a = runner.query(this.conn, sql, new BeanHandler<>(Account.class),id);
23         return a;
24     }
25 }
View Code

  在service中要想调用上面这个dao,在创建AccountDao对象时必须接收一个Connection对象,这样能在事务中调用不同的dao中的方法都能使用同一个连接。只有保证使用的是同一个连接,才能保证所有方法调用的操作都是在同一个事务中。

  接下来才是到service层进行我们的业务逻辑处理,就是将A转账给B:

 1 public class BussinessService {
 2     /*
 3      * @sourceId 源客户id
 4      * @targetId 目标客户id
 5      * @transferMoney 交易金额
 6      */
 7     public void transfer(int sourceId,int targetId,double transferMoney) throws SQLException {
 8         Connection conn = null;
 9         try{
10             conn = JdbcUtils.getDataSource().getConnection();
11             conn.setAutoCommit(false); //开启事务
12             
13             AccountDao dao = new AccountDao(conn);    
14             Account a = dao.find(sourceId);
15             Account b = dao.find(targetId);
16             //业务处理
17             a.setMoney(a.getMoney()-transferMoney);
18             b.setMoney(b.getMoney()+transferMoney);
19             dao.update(a);
20             dao.update(b);
21             
22             conn.commit();  //提交事务
23             
24         }finally{
25             if(conn!=null) {
26                 conn.close(); //将连接返还连接池中
27             }
28         }
29     }
30 }
View Code

  上面就是我们处理转账的业务逻辑代码,虽然我们按照三层架构的思想将转账变得好像“复杂”的样子,但是却能让我们更好的进行业务处理。另外因为我们的连接是从连接池中获取的,因此即使调用close方法也是将连接返还给连接池中。上面transfer方法里的代码虽然简单,但是逻辑流程是在是很棒,值得好好品尝!

 

  但还不够优雅!也许你觉得上面的代码已经很不错了,但实际开发还是欠了一点,最优雅的方式要么使用Spring框架,也可以使用ThreadLocal类。因为还没学Spring,所以本篇以ThreadLocal来重构上面的业务操作。既然要使用ThreadLocal,那么就先简单的介绍下ThreadLocal:

  

而ThreadLocal的方法也只有如下几个:

  

  简单来说ThreadLocal就相当于是一个容器,是一个线程的容器,只有在这个线程的开始到结束的过程中才能从这个容器中取该容器中的数据。ThreadLocal也可以看成是一个Map集合,而在这个Map集合中,Key就是当前线程,而value则是通过ThreadLocal的set方法保存的数据,当我们在该线程的处理过程中需要取出数据时通过get方法,其实是以当前线程为Key关键字到Map集合取出对应的数据。ThreadLocal在框架中经常被用到。

  一个简单的ThreadLocal的demo如下:

1 public static void main(String[] args){
2     
3         ThreadLocal<String> thread = new ThreadLocal<>();
4         thread.set("Long live SD");
5         
6         String str = thread.get();
7         System.out.println(str);
8     }
View Code

  因为都是在主线程中,所以这个ThreadLocal对象能存储的数据就是在主线程中能取出的。

 

  那么我们回到上面讲解的事务的例子,我们可以看到从service层到dao层,所演示的都只在一个线程内,transfer方法中只有一个线程在处理事务或者访问dao层进行数据库操作,没有并发的情况,因此我们可以将Connection对象存在这个线程的容器里,在这个线程处理的流程中需要的时候取出。

  在这个例子中,在获取连接,开启事务,dao层的增删改查操作,提交事务,关闭连接都需要从ThreadLocal中获取连接,像关闭连接还必须将连接对象从ThreadLocal容器中移除这样更重要的步骤,下图简单的反映上面的例子在处理的线程的流程:

  

例3

  那么下面我们就开始使用ThreadLocal来重构上面的转账事务。

  根据三层架构,我们在service层和dao层都需要用到Connection对象,那么ThreadLocal我们可以设为static静态的,因为静态的在类一加载就已经存在,并且保持到虚拟机关闭,可以说是整个应用的运行期间都存在而不会销毁,但是也说明了将一个容器设为静态后一定要记得清除无用的数据,否则会越来越臃肿导致内存溢出

  这次我们重新编写该项目中的数据库工具类JdbcUtils,加入ThreadLocal对象和处理事务的常用方法:

 1 public class JdbcUtils {
 2 
 3     private static DataSource ds;
 4     private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
 5     static {
 6         InputStream in = JdbcUtils.class.getClassLoader().getResourceAsStream(
 7                 "dbcpconfig.properties");
 8         Properties prop = new Properties();
 9         try {
10             prop.load(in);
11             ds = BasicDataSourceFactory.createDataSource(prop);
12 
13         } catch (Exception e) {
14             throw new ExceptionInInitializerError();
15         }
16 
17     }
18 
19     public static DataSource getDataSource() {
20         return ds;
21     }
22 
23     public static Connection getConnection() throws SQLException {
24         Connection conn = threadLocal.get();
25         if (conn == null) {
26             conn = ds.getConnection();
27             threadLocal.set(conn); // 将获取的连接同时存入ThreadLocal容器中
28         }
29         return conn;
30     }
31 
32     public static void startTransaction() throws SQLException {
33         Connection conn = threadLocal.get();
34         if (conn == null) {
35             conn = getConnection();
36         }
37         conn.setAutoCommit(false); // 开启事务
38     }
39 
40     public static void commitTransation() throws SQLException {
41         Connection conn = threadLocal.get();
42         if (conn != null) {
43             conn.commit(); // 提交事务
44         }
45     }
46 
47     public static void closeConnection() throws SQLException {
48         try {
49             Connection conn = threadLocal.get();
50             if (conn != null) {
51                 conn.close();
52             }
53         } finally {
54             threadLocal.remove(); // 将连接返回给连接池中一定记住同时从ThreadLocal中移除
55         }
56     }
57 }
View Code

  在这个工具类JdbcUtils中,获取连接是从ThreadLocal中获取,如果是第一次获取连接则从连接池中获取同时存入ThreadLocal以便后面线程处理需要再取出,接着就是开启事务、提交事务、和将连接返回给连接池同时从ThreadLocal移除,因为这三个方法都需要从ThreadLocal中获取Connection对象并且都在一个线程中会使用到,因此都从ThreadLocal中获取连接对象。另外一定要记住,如果将ThreadLocal容器设为了静态,当向ThreadLocal中存入数据时,一定要在同一线程内调用ThreadLocal的remove方法移除数据,否则将会存储越来越多导致内存溢出

  现在在dao层中我们事务会用到的方法就不需要像例2一样在创建dao对象时传入Connection参数了,可以直接从JdbcUtils中,其实是从ThreadLocal中获取连接:

 1 public class AccountDao {
 2     
 3     public void update(Account a) throws SQLException {
 4         QueryRunner runner = new QueryRunner();
 5         String sql = "update account set money=? where id=?";
 6         Object[] params = {a.getMoney(),a.getId()};
 7         //从JdbcUtils中,其实也是从ThreadLocal中获取连接
 8         runner.update(JdbcUtils.getConnection(), sql, params);
 9     }
10     
11     public Account find(int id) throws SQLException {
12         QueryRunner runner = new QueryRunner();
13         String sql = "select * from account where id=?";
14         //从JdbcUtils中,其实也是从ThreadLocal中获取连接
15         Account a = runner.query(JdbcUtils.getConnection(), sql, new BeanHandler<>(Account.class), id);
16         return a;
17     }
18 }
View Code

  我们已经完成了dao层的一点修改,其实就是将连接的获取方式从通过构造器传递变为从线程容器中获取,本质都是一样的,那么在service层我们就可以通过dao层和工具类JdbcUtils中新写的方法进行业务处理了:

 1 public class BussinessService {
 2     
 3     public void transfer(int sourceId,int targetId,int transferMoney) throws SQLException  {
 4         Connection conn = null;
 5         try{
 6             //获取连接,第一次获取的话同时也将连接存入ThreadLocal中
 7             conn = JdbcUtils.getConnection();
 8             JdbcUtils.startTransaction();  //开启事务
 9             
10             AccountDao dao = new AccountDao();
11             Account a = dao.find(sourceId);
12             Account b = dao.find(targetId);
13             a.setMoney(a.getMoney()-transferMoney);
14             b.setMoney(b.getMoney()+transferMoney);
15             dao.update(a);
16             dao.update(b);
17             
18             JdbcUtils.commitTransation(); //提交事务
19         }finally{
20             if(conn!= null){
21                 JdbcUtils.closeConnection(); //将连接返回给连接池中一定记住同时从ThreadLocal中移除
22             }
23         }
24     }
25 }
View Code

  例3和例2 的业务处理其实差不多是一样的,只是获取连接Connection对象的方式不同,例3不再是通过构造器层层的传递所需的参数,而是通过容器存取,这点其实和之前的博客所介绍过的JNDI容器是类似的。

  使用ThreadLocal即使是多线程的情况也是线程安全的,不同线程以其线程名作为Map集合的关键字将各自的Connection对象存入ThreadLocal对象中,因此无论是调用get方法还是remove方法都是将各自线程保留的数据取出或移除,而不会影响集合中别的数据。

 

最后补充一点。

  在实际开发时可能会遇到更复杂的情况,在web层Servlet将请求依次交给多个service层的实现类进行业务处理,但是每个service的实现类可能在方法后面就会提交事务,无法做到多个service类的方法进行同一个事务操作。如果想将多个service层进行同一个事务操作,则可以使用Filter过滤器进行处理。使用Filter可以先将请求拦下,这时开启事务,后面由各个service进行事务操作后再回到过滤器Filter进行事务提交即可完成多service的事务操作。

 

 

 

                 

posted @ 2016-03-13 22:09  fjdingsd  阅读(927)  评论(0编辑  收藏  举报