Java Web基础入门第六十二讲 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.liayun.domain包下创建一个封装数据的实体——Account.java,对应数据库中的account表。Account类的具体代码如下:

package cn.liayun.domain;

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.liayun.dao包下创建AccountDao类,该类用于处理银行业务中的转账问题。
在这里插入图片描述
我们在应用程序中加入了DBCP连接池,还有关于JdbcUtils类该怎么写,可以参考我的笔记《Java Web基础入门第六十一讲 Apache DBUtils框架的学习》
上面AccountDao类的transfer方法虽然可以处理转账业务,并且保证了在同一个事务中进行,但是AccountDao的这个transfer方法是处理两个用户之间的转账业务的,已经涉及到具体的业务操作了,转账业务应该在业务层中做,不应该出现在Dao层。在实际开发中,Dao层的职责应该只涉及到基本的CRUD,不应涉及具体的业务操作,所以在开发中Dao层出现这样的业务处理方法是一种不好的设计。
在编写AccountDao类具体代码的过程中,我们一定要注意下面两点:

  • 假设要把2条sql语句作为一个整体执行,那么就不能像下面这样写:
    在这里插入图片描述
    如果你给其连接池,等会你在调用runner对象的方法做转账的时候,在连接发完sql语句之后,就会将连接给关了,你就没办法把2条sql语句作为一个整体执行了,所以这时就不能给其一个连接池;
  • 从aaa账户向bbb账户转100元,像上面那样写违背了三层架构设计思想。在实际开发里面,AccountDao类只提供增删改查的方法,所有的业务逻辑都在Service层里面做。

在业务层(BusinessService)处理事务

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

package cn.liayun.dao;

import java.sql.Connection;
import java.sql.SQLException;

import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;

import cn.liayun.domain.Account;
import cn.liayun.utils.JdbcUtils;

public class AccountDao {
	
	// 接收Service层传递过来的Connection对象
	private Connection conn;
	
	public AccountDao(Connection conn) {
		this.conn = conn;
	}
	
	public AccountDao() {
		super();
		// TODO Auto-generated constructor stub
	}

	// 在实际开发里面,转账应该这样写
	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 runner.query(conn, sql, id, new BeanHandler<Account>(Account.class));
		} catch (SQLException e) {
			throw new RuntimeException(e);
		}
	}
	
}

接着在cn.liayun.service包下创建一个BusinessService类,用于在业务逻辑层(BusinessService)中处理转账业务。BusinessService类的具体代码如下:
在这里插入图片描述
程序经过这样改造之后就比刚才好多了,AccountDao只负责CRUD,里面没有具体的业务处理方法了,职责就单一了,而BusinessService则负责具体的业务逻辑和事务的处理,需要操作数据库时,就调用AccountDao类提供的CRUD方法操作数据库。但是,在实际开发里面,向上面那样写同样不优雅,最优雅的办法有:

  1. 使用Spring进行事务管理;
  2. 使用ThreadLocal这个类来进行事务管理。

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

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

package cn.liayun.utils;

import java.io.InputStream;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;

import javax.sql.DataSource;

import org.apache.commons.dbcp.BasicDataSourceFactory;

public class JdbcUtils {
	
	private static DataSource ds;
	
	/*
	 * 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 {
//		return ds.getConnection();
		
		try {
			//首先得到当前线程上绑定的连接
			Connection conn = tl.get();
			
			if (conn == null) {//代表当前线程上没有绑定连接
				conn = ds.getConnection();
				tl.set(conn);
			}
			
			return conn;
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}
	
	public static void startTransaction() {
		try {
			//首先得到当前线程上绑定的连接,并开启事务
			Connection conn = tl.get();
			
			if (conn == null) {//代表当前线程上没有绑定连接
				conn = ds.getConnection();//从数据库连接池里面获取一个连接
				tl.set(conn);
			}
			
			conn.setAutoCommit(false);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}
	
	public static void commitTransaction() {
		try {
			//首先得到当前线程上绑定的连接
			Connection conn = tl.get();
			if (conn != null) {//代表当前线程上绑定了连接,当前线程上有连接才提交事务,当前线程没有连接就不用提交
				conn.commit();
			}
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}
	
	//关闭连接
	public static void closeConnection() {
		try {
			//得到当前线程上绑定的连接,并关闭该连接
			Connection conn = tl.get();
			if (conn != null) {
				conn.close();
			}
		} catch (Exception e) {
			throw new RuntimeException(e);
		} finally {
			/*
             * 关闭连接之后,即还给数据库连接池了,还要从ThreadLocal容器里面移除掉这个连接。
             * 
             * 如果不移除,会有什么问题?
             * 有一个线程来执行了转账,ThreadLocal类的Map集合里面就有一个连接了,
             * 第二个线程又来,ThreadLocal类的Map集合里面又有一个连接,
             * 第三个线程又来,ThreadLocal类的Map集合里面又有一个连接,
             * 而ThreadLocal又是静态的,即整个应用程序周期范围内都存在,那么这个容器就会越来越大,最后将导致数据溢出。
             * 所以静态的东西要慎用!!!
             */
			tl.remove();//千万要注意,解除当前线程上绑定的连接(从ThreadLocal容器中移除掉对应当前线程上的连接)
		}
	}
}

在数据库连接工具类JdbcUtils中,我们一定要注意关闭连接的代码。如果我们这样写:
在这里插入图片描述
整个应用程序会有很大的缺陷。我们一定要在关闭连接之后(即还给数据库连接池了),还要记得从ThreadLocal容器里面移除掉这个连接。如果不移除,会有什么问题呢?有一个线程来执行了转账,ThreadLocal类的Map集合里面就有了一个连接,第二个线程又来,ThreadLocal类的Map集合里面又会有一个连接,第三个线程又来,ThreadLocal类的Map集合里面又有一个连接…,而ThreadLocal又是静态的,即整个应用程序周期范围内都存在,那么这个容器就会越来越大,最后导致数据溢出。记住静态的东西要慎用!!!所以关闭连接的正确代码应该为:
在这里插入图片描述
紧接着,对AccountDao类进行改造,数据库连接对象不再需要Service层传递过来,而是直接从JdbcUtils提供的getConnection方法去获取,改造后的AccountDao类如下:

package cn.liayun.dao;

import java.sql.Connection;
import java.sql.SQLException;

import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;

import cn.liayun.domain.Account;
import cn.liayun.utils.JdbcUtils;

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 runner.query(JdbcUtils.getConnection(), sql, id, new BeanHandler<Account>(Account.class));
		} catch (SQLException e) {
			throw new RuntimeException(e);
		}
	}
	
}

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

package cn.liayun.service;

import java.sql.Connection;
import java.sql.SQLException;

import org.junit.Test;

import cn.liayun.dbutils.demo.AccountDao;
import cn.liayun.domain.Account;
import cn.liayun.utils.JdbcUtils;

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层的业务代码放到一个事务里面,那怎么做呢?解决方法是:使用事务过滤器,那么一次请求范围内的所有操作都将在一个事务里面了。如下图:
在这里插入图片描述
不急,我们以后会详细讲解事务过滤器的!!!

  • 5
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李阿昀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值