事务管理在三层架构中应用以及使用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的改写:
public class AccountDao {
// A向B转账100元
public void transfer() throws SQLException {
Connection conn = null;
try{
conn = JdbcUtils.getDataSource().getConnection();
conn.setAutoCommit(false); //开启事务 QueryRunner runner = new QueryRunner();
String sql1="update account set money=money-100 where name='a'";
runner.update(conn,sql1); String sql2="update account set money=money+100 where name='b'";
runner.update(conn,sql2); conn.commit(); //提交事务
}finally{
if(conn!=null) {
conn.close();
}
}
}
}
这就是我们之前所学的使用JDBC来操作事务,虽然小小地利用DbUtils改造了下,但这个方法还是可以执行的,我们说过本篇对于事务的重新认识不是在于技术,而是在于规范。上面的代码确实可以执行,执行结果也没问题,问题在于不规范。
例1不规范的地方在于dao层,也就是数据访问层,只能是增删改查(CRUD),上面的代码明显是业务逻辑的操作,因此应该在dao层对该事务的数据只能有CRUD,而业务逻辑放在service层。
例2:
现在对例1的代码根据三层架构进行分离,首先在dao层 要根据数据库中account表生成一个Account这种JavaBean,这里就省略了。
接着在AccountDao中不能像刚才那样直接进行事务处理,而是只能有CRUD,这个例子中只用到了更新和查找,因此这里只写这两个方法:
public class AccountDao {
private Connection conn; public AccountDao() {
super();
} public AccountDao(Connection conn) {
this.conn = conn; //指定使用的连接
} public void update(Account a) throws SQLException {
QueryRunner runner = new QueryRunner(); //必须使用无参的构造器
String sql = "update account set money=? where id=?";
Object[] params={a.getMoney(),a.getId()};
runner.update(this.conn, sql, params);
} public Account find(int id) throws SQLException {
QueryRunner runner = new QueryRunner(); //必须使用无参的构造器
String sql = "select * from account where id=?";
Account a = runner.query(this.conn, sql, new BeanHandler<>(Account.class),id);
return a;
}
}
在service中要想调用上面这个dao,在创建AccountDao对象时必须接收一个Connection对象,这样能在事务中调用不同的dao中的方法都能使用同一个连接。只有保证使用的是同一个连接,才能保证所有方法调用的操作都是在同一个事务中。
接下来才是到service层进行我们的业务逻辑处理,就是将A转账给B:
public class BussinessService {
/*
* @sourceId 源客户id
* @targetId 目标客户id
* @transferMoney 交易金额
*/
public void transfer(int sourceId,int targetId,double transferMoney) throws SQLException {
Connection conn = null;
try{
conn = JdbcUtils.getDataSource().getConnection();
conn.setAutoCommit(false); //开启事务 AccountDao dao = new AccountDao(conn);
Account a = dao.find(sourceId);
Account b = dao.find(targetId);
//业务处理
a.setMoney(a.getMoney()-transferMoney);
b.setMoney(b.getMoney()+transferMoney);
dao.update(a);
dao.update(b); conn.commit(); //提交事务 }finally{
if(conn!=null) {
conn.close(); //将连接返还连接池中
}
}
}
}
上面就是我们处理转账的业务逻辑代码,虽然我们按照三层架构的思想将转账变得好像“复杂”的样子,但是却能让我们更好的进行业务处理。另外因为我们的连接是从连接池中获取的,因此即使调用close方法也是将连接返还给连接池中。上面transfer方法里的代码虽然简单,但是逻辑流程是在是很棒,值得好好品尝!
但还不够优雅!也许你觉得上面的代码已经很不错了,但实际开发还是欠了一点,最优雅的方式要么使用Spring框架,也可以使用ThreadLocal类。因为还没学Spring,所以本篇以ThreadLocal来重构上面的业务操作。既然要使用ThreadLocal,那么就先简单的介绍下ThreadLocal:
而ThreadLocal的方法也只有如下几个:
简单来说ThreadLocal就相当于是一个容器,是一个线程的容器,只有在这个线程的开始到结束的过程中才能从这个容器中取该容器中的数据。ThreadLocal也可以看成是一个Map集合,而在这个Map集合中,Key就是当前线程,而value则是通过ThreadLocal的set方法保存的数据,当我们在该线程的处理过程中需要取出数据时通过get方法,其实是以当前线程为Key关键字到Map集合取出对应的数据。ThreadLocal在框架中经常被用到。
一个简单的ThreadLocal的demo如下:
public static void main(String[] args){ ThreadLocal<String> thread = new ThreadLocal<>();
thread.set("Long live SD"); String str = thread.get();
System.out.println(str);
}
因为都是在主线程中,所以这个ThreadLocal对象能存储的数据就是在主线程中能取出的。
那么我们回到上面讲解的事务的例子,我们可以看到从service层到dao层,所演示的都只在一个线程内,transfer方法中只有一个线程在处理事务或者访问dao层进行数据库操作,没有并发的情况,因此我们可以将Connection对象存在这个线程的容器里,在这个线程处理的流程中需要的时候取出。
在这个例子中,在获取连接,开启事务,dao层的增删改查操作,提交事务,关闭连接都需要从ThreadLocal中获取连接,像关闭连接还必须将连接对象从ThreadLocal容器中移除这样更重要的步骤,下图简单的反映上面的例子在处理的线程的流程:
例3:
那么下面我们就开始使用ThreadLocal来重构上面的转账事务。
根据三层架构,我们在service层和dao层都需要用到Connection对象,那么ThreadLocal我们可以设为static静态的,因为静态的在类一加载就已经存在,并且保持到虚拟机关闭,可以说是整个应用的运行期间都存在而不会销毁,但是也说明了将一个容器设为静态后一定要记得清除无用的数据,否则会越来越臃肿导致内存溢出。
这次我们重新编写该项目中的数据库工具类JdbcUtils,加入ThreadLocal对象和处理事务的常用方法:
public class JdbcUtils { private static DataSource ds;
private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
static {
InputStream in = JdbcUtils.class.getClassLoader().getResourceAsStream(
"dbcpconfig.properties");
Properties prop = new Properties();
try {
prop.load(in);
ds = BasicDataSourceFactory.createDataSource(prop); } catch (Exception e) {
throw new ExceptionInInitializerError();
} } public static DataSource getDataSource() {
return ds;
} public static Connection getConnection() throws SQLException {
Connection conn = threadLocal.get();
if (conn == null) {
conn = ds.getConnection();
threadLocal.set(conn); // 将获取的连接同时存入ThreadLocal容器中
}
return conn;
} public static void startTransaction() throws SQLException {
Connection conn = threadLocal.get();
if (conn == null) {
conn = getConnection();
}
conn.setAutoCommit(false); // 开启事务
} public static void commitTransation() throws SQLException {
Connection conn = threadLocal.get();
if (conn != null) {
conn.commit(); // 提交事务
}
} public static void closeConnection() throws SQLException {
try {
Connection conn = threadLocal.get();
if (conn != null) {
conn.close();
}
} finally {
threadLocal.remove(); // 将连接返回给连接池中一定记住同时从ThreadLocal中移除
}
}
}
在这个工具类JdbcUtils中,获取连接是从ThreadLocal中获取,如果是第一次获取连接则从连接池中获取同时存入ThreadLocal以便后面线程处理需要再取出,接着就是开启事务、提交事务、和将连接返回给连接池同时从ThreadLocal移除,因为这三个方法都需要从ThreadLocal中获取Connection对象并且都在一个线程中会使用到,因此都从ThreadLocal中获取连接对象。另外一定要记住,如果将ThreadLocal容器设为了静态,当向ThreadLocal中存入数据时,一定要在同一线程内调用ThreadLocal的remove方法移除数据,否则将会存储越来越多导致内存溢出。
现在在dao层中我们事务会用到的方法就不需要像例2一样在创建dao对象时传入Connection参数了,可以直接从JdbcUtils中,其实是从ThreadLocal中获取连接:
public class AccountDao { public void update(Account a) throws SQLException {
QueryRunner runner = new QueryRunner();
String sql = "update account set money=? where id=?";
Object[] params = {a.getMoney(),a.getId()};
//从JdbcUtils中,其实也是从ThreadLocal中获取连接
runner.update(JdbcUtils.getConnection(), sql, params);
} public Account find(int id) throws SQLException {
QueryRunner runner = new QueryRunner();
String sql = "select * from account where id=?";
//从JdbcUtils中,其实也是从ThreadLocal中获取连接
Account a = runner.query(JdbcUtils.getConnection(), sql, new BeanHandler<>(Account.class), id);
return a;
}
}
我们已经完成了dao层的一点修改,其实就是将连接的获取方式从通过构造器传递变为从线程容器中获取,本质都是一样的,那么在service层我们就可以通过dao层和工具类JdbcUtils中新写的方法进行业务处理了:
public class BussinessService { public void transfer(int sourceId,int targetId,int transferMoney) throws SQLException {
Connection conn = null;
try{
//获取连接,第一次获取的话同时也将连接存入ThreadLocal中
conn = JdbcUtils.getConnection();
JdbcUtils.startTransaction(); //开启事务 AccountDao dao = new AccountDao();
Account a = dao.find(sourceId);
Account b = dao.find(targetId);
a.setMoney(a.getMoney()-transferMoney);
b.setMoney(b.getMoney()+transferMoney);
dao.update(a);
dao.update(b); JdbcUtils.commitTransation(); //提交事务
}finally{
if(conn!= null){
JdbcUtils.closeConnection(); //将连接返回给连接池中一定记住同时从ThreadLocal中移除
}
}
}
}
例3和例2 的业务处理其实差不多是一样的,只是获取连接Connection对象的方式不同,例3不再是通过构造器层层的传递所需的参数,而是通过容器存取,这点其实和之前的博客所介绍过的JNDI容器是类似的。
使用ThreadLocal即使是多线程的情况也是线程安全的,不同线程以其线程名作为Map集合的关键字将各自的Connection对象存入ThreadLocal对象中,因此无论是调用get方法还是remove方法都是将各自线程保留的数据取出或移除,而不会影响集合中别的数据。
最后补充一点。
在实际开发时可能会遇到更复杂的情况,在web层Servlet将请求依次交给多个service层的实现类进行业务处理,但是每个service的实现类可能在方法后面就会提交事务,无法做到多个service类的方法进行同一个事务操作。如果想将多个service层进行同一个事务操作,则可以使用Filter过滤器进行处理。使用Filter可以先将请求拦下,这时开启事务,后面由各个service进行事务操作后再回到过滤器Filter进行事务提交即可完成多service的事务操作。
事务管理在三层架构中应用以及使用ThreadLocal再次重构的更多相关文章
- JDBC事务--软件开发三层架构--ThreadLocal
JDBC事务--软件开发三层架构--ThreadLocal 一.JDBC事务 1.概述: 事务是指逻辑上的一组操作!这一组操作,通常认为是一个整体,不可拆分! 特点:同生共死;事务内的这一组操作要么全 ...
- 三层架构中bll层把datatable转换为实体model的理解
看了很多人的项目,很多都是用到三层架构,其中BLL层中有一种将DataTable转换为实体的方法.一直没有明白为啥要这样做,今天特意去搜索了一下,如果没有答案我是准备提问,寻求解答了.还好找到一个相关 ...
- 谈谈三层架构中Model的作用
Model又叫实体类,这个东西,大家可能觉得不好分层.包括我以前在内,是这样理解的:UI<-->Model<-->BLL<-->Model<-->DAL ...
- 为什么三层架构中业务层(service)、持久层(dao)需要使用一个接口?
为什么三层架构中业务层(service).持久层(dao)需要使用一个接口? 如果没有接口那么我们在控制层使用业务层或业务层使用持久层时,必须要学习每个方法,若哪一天后者的方法名改变了则直接影响到前面 ...
- 【转】.NET 三层架构 中 DAL+IDAL+Model+BLL+Web
其实三层架构是一个程序最基本的 在.Net开发中通常是多层开发 比如说 BLL 就是business Logic laywer(业务逻辑层) 他只负责向数据提供者也就是DAL调用数据 然后传递给 客户 ...
- 关于C#三层架构中的“分页”功能
新手上路,请多指教! 今天将分页功能实现了,要特别感谢坐在前面的何同学的指点,不胜感谢!功能的实现采用了三层架构的方式实现该功能,简述如下: 界面: DAL层有两个方法:“当前所在页”和“总页数” 这 ...
- 怎么在三层架构中使用Quartz.Net开源项目(与数据库交互)
1.首先在项目中先创建一个控制台应用程序 2.然后右击项目中的[引用],可以[添加引用],也可以[管理NuGet程序包],作者使用的是[添加引用],添加本地应用.版本不同,所使用的方式不同.需要此版本 ...
- linq to sql 三层架构中使用CRUD操作
/// <summary> /// 数据层 /// </summary> public partial class GasBottles : IGasBottles { #re ...
- Spring Boot中的事务管理
原文 http://blog.didispace.com/springboottransactional/ 什么是事务? 我们在开发企业应用时,对于业务人员的一个操作实际是对数据读写的多步操作的结合 ...
随机推荐
- JavaScript螺纹的问题和答案
要求: JavaScript是单线程的,有任务队列.比方使用setTimeou(func,secs)来在secs毫秒后向任务队列加入func.可是,setTimeout后面跟一个死循环,那么死循环导致 ...
- php5.5 + apache2.4 安装配置
php5.5 做了大量的更新,在与apache搭配的时候如何选择也很有讲究,这里我们以64位 php5.6 和 Apache2.4为例介绍如何配置. 工具/原料 Win7/8 64位 php5.5.6 ...
- ThinkPHP - 博客获取列表信息
得到数据: Array ( [0] => Array ( [id] => 5 [name] => PHP [pid] => 0 [sort] => 1 [blog] =& ...
- WebView之2
首先需要添加权限: <uses-permission android:name="android.permission.INTERNET"/> MainActivity ...
- 一种用javascript实现的比较兼容的回到顶部demo + 阻止事件冒泡
回到页面顶部demo <!DOCTYPE html> <html lang="en"> <head> <meta charset=&quo ...
- (Problem 21)Amicable numbers
Let d(n) be defined as the sum of proper divisors of n (numbers less than n which divide evenly into ...
- Windows的公共控件窗口类列表
The following window class names are provided by the common control library: ANIMATE_CLASS Creates a ...
- x64栈结构
A function's prolog is responsible for allocating stack space for local variables, saved registers, ...
- 转:js包装DOM对象
我们在日常的应用中,使用Javascript大多数时间都是在用DOM ,以致于很多人都有一种看法就是DOM==JS,虽然这种看法是错误的,但是也可以说明DOM的重要性. 这就导致了我们在写JS的时候, ...
- 基于visual Studio2013解决算法导论之028散列表开放寻址
题目 散列表 解决代码及点评 #include <iostream> #include <time.h> using namespace std; template & ...