ThreadLocal实践

来源:互联网 发布:科禄格风机选型软件 编辑:程序博客网 时间:2024/05/05 06:42

无聊的话

不管是用过Hibernate还是Spring的程序员,又或者作为一名面试官,经常会提及Hibernate等框架在操作数据库时是如何做到多线程不混乱,

并且能自动提交、回滚,不需要程序员做这些麻烦的操作,极大的解放了程序员的工作量,且保持代码简洁优雅一般大家都会说,

用ThreadLocal来保存当前线程使用的SqlSession就OK了。确实是这样,但很少看到有人仔细去分析,或者自己尝试去实现这一过程

这几天一直在想实现一个简单版本的这种组件,纯粹为了实践下ThreadLocal的巧妙之处,于是有了这篇文章

 

关于设计的一些想法

(1)一个应用程序允许存在多个数据源,即一个或多个DataSource

(2)每个DataSource由一个数据连接工厂来处理,数据连接工厂即SqlSessionFactory

(3)正在运行的每个线程,可以操作多个数据源,那就要求实现多个数据源的一起提交与回滚

(4)针对该线程里面的每个数据源,只能分配一个SqlSession

(5)针对每个SqlSession,只能分配一个数据库连接,即一个Connection

这里面关键点就是通过【同一个线程】串联起来的,如果做到每个线程里面的每个数据源,都只有一个Sqlsession,下面看一个典型应用

 

package com.yli.sql.connection;import java.util.HashMap;import java.util.Map;import com.yli.sql.session.SqlSession;public class Test {public static void main(String[] args) {// 创建Dao和User的实例,通常这由Spring的IOC来完成// 并且Dao和User在整个应用里面是单例,即singleton模式的UserDao userDao = new UserDao();UserService userService = new UserService();userService.setUserDao(userDao);// 调用service的业务方法:通常在service层加事务控制// 使用注解或者xml配置的方式:匹配粒度可以到方法名,也可以到类名等等// 但是不需要程序员手动打开、关闭连接,也不需要手动控制事务Map<String, Object> params = new HashMap<String, Object>();params.put("userId", 1001);userService.modifyUser(params);}}class UserService {private UserDao userDao;public void modifyUser(Map<String, Object> params){// ===>DB1userDao.insertUser(params);// ===>DB2userDao.updateUser(params);// another service methodthis.updateUser(params);}public void updateUser(Map<String, Object> params){userDao.updateUser(params);}public void setUserDao(UserDao userDao) {this.userDao = userDao;}}class UserDao {// 操作数据库1private SqlSession session1;// 操作数据库2private SqlSession session2;public void insertUser(Map<String, Object> params){String sql = "insert ...";session1.insert(sql, params);}public void updateUser(Map<String, Object> params){String sql = "update ...";session2.insert(sql, params);}}


 

那么怎么实现自动打开关闭连接、自动提交回滚事务呢,并且是涉及到多个数据源的

我的想法比较简单,在业务方法执行前,使用 Connection.setAutoCommit(false) ;成功执行后,使用 Connection.commit() 提交;执行过程中遇到异常时,使用 Connection.rollback() 回滚

那么这样一说,大家很可能就想到了使用Spring的AOP来实现,比如凡是调用 save* , update* , insert* , delete* , execute* 这种方法的

就在业务方法执行前、执行中、执行后 这3个阶段来包裹一段代码,实现自动提交与回滚

确实是这样的,不过既然是自己实现,那就不用Spring的AOP了,JDK原生的动态代理  invocationhandler 就能很好的做到

不过JDK的动态代理要求业务类必须定义接口,这不太方便,那就使用 cglib 来实现,它可以为每一个类生成动态代理,不强制要求有接口(原理就是为每个类生成一个子类)

 

但是我们看看上面的代码,即 userService.modifyUser(params),其实每一个业务方法里面

肯定会调用其他业务方法,或者调用其他业务类的方法,比如 employeeService.save() 方法。

如果就简单粗暴的【为每个业务类生成代理类,并为每个匹配切点的方法包裹一段代码】,那就是这种结构的代码

class ProxyService implements MethodInterceptor {@Overridepublic Object intercept(Object obj, Method method, Object[] args,MethodProxy proxy) throws Throwable {// 暂且不说这个 session 是如何得到的,但是前文已经提过// 由session来获取连接 Connection // 设置要程序提交事务session.getConnection().setAutoCommit(false);try{Object result = proxy.invokeSuper(obj, args);}catch(SQLException e) {e.printStackTrace();session.getConnection().rollback();}session.getConnection().commit();return null;}}


分析下前面提到的典型应用 userService.modifyUser(params),由于该方法又调用了 userService.updateUser()方法

那么按照上述代理类的执行过程就是这种:

===>modifyUser()

===>setAutoCommit()

===>proxy.invokeSuper(obj, args):进入了 真正的 modifyUser  方法

      ===>updateUser()  :执行 userDao.insertUser(params)  和  userDao.updateUser(params)

      ===>setAutoCommit()

      ===>proxy.invokeSuper(obj, args)

      ===>commit()

===>commit()

很明显这是有问题的,出现了多次setAutoCommit和多次 commit,还能想到在异常那段也有多次rollback

特别是多次 commit 就导致整个方法调用不是事务同步的。。。因为内层方法已经提交了,外层基本出错

还怎么回滚掉内层的(不要多想Spring事务管理的传播属性,我没那么强大)

 

那么怎么处理呢,我的想法是把当前线程的每次方法调用,用一个栈记录下来

一进入某个方法,就push当前方法名;一调用完毕,就pop当前方法名。因为整个线程是把所有内层方法串联起来的,所以很好办了。

那么只要判断当前方法栈里面有多少个方法就行了:第一个入口方法处调用 setAutoCommit,在第一个入口方法调用结束处调用 commit 就行了

那这个方法栈,是当前线程独有的局部变量,怎么得到呢,就是ThreadLocal来保存了

那就来看整个实现过程好了,不想啰嗦了。。。一张图+代码说明

 

 

1.创建connection包,来定义一些类,处理Connection

比如创建包:com.yli.sql.connection

 

package com.yli.sql.connection;import java.io.PrintWriter;import java.sql.Connection;import java.sql.DriverManager;import java.sql.SQLException;import java.sql.SQLFeatureNotSupportedException;import java.util.logging.Logger;import javax.sql.DataSource;/** * 每一个Connection都应该有DataSource来提供 * DataSource充当连接池,比如C3P0或者DBCP都是非常优秀的连接池 * 此处不想这么费事了。。。我就每次new一个Connection好了 * 所以我这就实现getConnection方法 * @author yli * */public class MyDataSource implements DataSource {private String dirver = "com.mysql.jdbc.Driver";private String url = "jdbc:mysql://localhost:3306/test";private String user = "root";private String password = "gzu_imis";public MyDataSource(String driver, String url, String user, String password) {this.dirver = driver;this.url = url;this.user = user;this.password = password;}@Overridepublic Connection getConnection() throws SQLException {try {Class.forName(this.dirver);return DriverManager.getConnection(this.url, this.user, this.password);} catch (ClassNotFoundException e) {e.printStackTrace();} catch (SQLException e) {e.printStackTrace();}return null;}@Overridepublic Connection getConnection(String username, String password)throws SQLException {// TODO Auto-generated method stubreturn null;}@Overridepublic PrintWriter getLogWriter() throws SQLException {// TODO Auto-generated method stubreturn null;}@Overridepublic void setLogWriter(PrintWriter out) throws SQLException {// TODO Auto-generated method stub}@Overridepublic void setLoginTimeout(int seconds) throws SQLException {// TODO Auto-generated method stub}@Overridepublic int getLoginTimeout() throws SQLException {// TODO Auto-generated method stubreturn 0;}@Overridepublic Logger getParentLogger() throws SQLFeatureNotSupportedException {// TODO Auto-generated method stubreturn null;}@Overridepublic <T> T unwrap(Class<T> iface) throws SQLException {// TODO Auto-generated method stubreturn null;}@Overridepublic boolean isWrapperFor(Class<?> iface) throws SQLException {// TODO Auto-generated method stubreturn false;}}


 

2.创建session包,来定义一些类,处理Session

比如创建包:com.yli.sql.session

 

package com.yli.sql.session;import java.sql.Connection;import java.util.List;import java.util.Map;/** * 定义SqlSession接口 * 暴露给程序员的方法,就是一些基本的query、update等等 * 当然要提供获取Connection的方法 * @author yli * */public interface SqlSession {Connection getConnection();boolean execute(String sql, Map<String, Object> params);int insert(String sql, Map<String, Object> params);int update(String sql, Map<String, Object> params);int delete(String sql, Map<String, Object> params);Map<String, Object> selectForOne(String sql, Map<String, Object> params);List<Map<String, Object>> selectForList(String sql, Map<String, Object> params);}

 

 

package com.yli.sql.session;import java.sql.PreparedStatement;import java.sql.SQLException;import java.util.ArrayList;import java.util.List;import java.util.Map;import java.util.regex.Matcher;import java.util.regex.Pattern;/** * 定义一个简单的sql参数处理工具类 * 比如动态配置sql语句:select * from user where userId=:userId * 就是使用 :userId 来处理动态参数 * 这个工具类很简单,不能用于生产系统 * 看过Spring处理Sql动态参数的童鞋都明白,其实蛮复杂的一个过程 *  * @author yli */public class SqlParam {/** * 将 select * from user where userId=:userId 处理成 *   select * from user where userId=? * 因为 ? 才是PreParedStatement能处理的 * 使用 :userId 完全是为了方便开发者,并且方便程序动态处理所有参数 */    private String       prePareSql;    /**     * 动态sql语句要传递进来的参数     */    private List<Object> paramList;    public SqlParam(String prePareSql, List<Object> paramList) {        this.prePareSql = prePareSql;        this.paramList = paramList;    }        public void setPreParams(PreparedStatement ps){        if(null == ps || null == paramList) {            return;        }        for (int i = 0; i < paramList.size(); i++) {            try {                ps.setObject(i + 1, paramList.get(i));            } catch (SQLException e) {                e.printStackTrace();            }        }    }    /**     * 用正则表达式匹配 :userId 这种结构的动态sql语句     * 比较简单的一个处理,有很多bug的,别在意。。。     * @param sql     * @param params     * @return     */    public static SqlParam prePareSqlParam(String sql, Map<String, Object> params) {        String regx = ":[a-zA-Z0-9_]+";        Pattern pattern = Pattern.compile(regx);        Matcher matcher = pattern.matcher(sql);        List<Object> paramList = new ArrayList<Object>();        String sqlParam;        while (matcher.find()) {            sqlParam = sql.substring(matcher.start(), matcher.end());            paramList.add(params.get(sqlParam.substring(1)));        }        if (paramList.isEmpty()) {            System.out.println("==========>没有任何递参数进来:" + sql);        } else {            sql = matcher.replaceAll("?");        }        return new SqlParam(sql, paramList);    }    public String getPrePareSql() {        return prePareSql;    }    public List<Object> getParamList() {        return paramList;    }}


package com.yli.sql.session;import java.sql.SQLException;import java.util.HashMap;import java.util.Map;import javax.sql.DataSource;/** * 定义一个SqlSessionFactory工厂类 * 该工厂提供openSession方法:从当前线程获取Session * 更重要的是定义一个localMap,用来保存当心线程使用到的session * 前文说过:每个线程可以操作多个数据源的,每个数据源要用一个SqlSession * 所有用了一个Map来保存 *  * @author yli * */public class SqlSessionFactory {private DataSource dataSource;private static ThreadLocal<Map<SqlSessionFactory, SqlSession>> localFactory = new ThreadLocal<Map<SqlSessionFactory, SqlSession>>();public void setDataSource(DataSource dataSource) {this.dataSource = dataSource;}public DataSource getDataSource() {return this.dataSource;}/** * 根据当前线程使用的SqlSessionFactory来判断应该使用哪个数据源 * 在创建对应的SqlSession即可 * 每次创建的SqlSession保存到Map中:可以就是当前要使用的SqlSessionFactory * @return */public SqlSession openSession() {Map<SqlSessionFactory, SqlSession> factory = localFactory.get();SqlSession session = null;if (null == factory) {try {factory = new HashMap<SqlSessionFactory, SqlSession>();session = new DefaultSqlSession(this);factory.put(this, session);localFactory.set(factory);} catch (SQLException e) {e.printStackTrace();}} else {if(null == factory.get(this)) {try {session = new DefaultSqlSession(this);factory.put(this, session);localFactory.set(factory);} catch (SQLException e) {e.printStackTrace();}}}return factory.get(this);}// 如果使用这个方式来保存SqlSession,那么当前线程只能使用一个SqlSession了// 对应要操作多个数据源的应用程序来说,就不适用了// private static ThreadLocal<SqlSession> localSession = new ThreadLocal<SqlSession>();/* 我想这段代码是很多人在分析Hibernate或者Spring时会提到的 * 但有没有像过同一线程操作对个数据源的情况了,你要亲自试试才知道 * 当然Hibernate没我说的这么简单。。。是有很多人想简单了public SqlSession openSession1() {SqlSession session = localSession.get();if(null == session) {try {session = new DefaultSqlSession(this);} catch (SQLException e) {e.printStackTrace();}localSession.set(session);}return session;}*/}
package com.yli.sql.session;/** * 在定义个工具类,一般来说由程序员自己实现或者扩展它 * 因为要使用多个SqlSessionFactory对应多个数据源 * 所以此处定义两个来测试,如果你有更多的数据源,就再定义了 *  * 所以这个类是客户端自己定义或者扩展的 * 客户端的Dao继承自它,就可以使用SqlSession了 * @author yli * */public class SessionDaoSupport {private SqlSessionFactory factory1;private SqlSessionFactory factory2;public void setFactory1(SqlSessionFactory factory1) {this.factory1 = factory1;}public void setFactory2(SqlSessionFactory factory2) {this.factory2 = factory2;}public SqlSession getSession1(){return this.factory1.openSession();}public SqlSession getSession2(){return this.factory2.openSession();}}


 

3.创建动态代理的包,来自动处理事务

比如创建包:com.yli.sql.transaction

package com.yli.sql.transaction;/** * 简单点,我们定义一个匹配方法的切点 *  * @author liyu * */public class MethodAdvice {private String[] matches;public MethodAdvice(String[] matches) {this.matches = matches;}public String[] getMatches(){return this.matches;}}


 

package com.yli.sql.transaction;import java.sql.SQLException;import java.util.ArrayList;import java.util.List;import java.util.Stack;import com.yli.sql.session.SqlSessionFactory;/** * 定义一个事务管理类 * 该类关联到应用程序中所有SqlSessionFactory * 那么就可以获取到当前线程里面的 SqlSession * 获取到SqlSession就能获取Connectin * 获取到Connection就能自动提交、回滚了。。。 * 不过这很简单也很粗暴的实现啊 *  * @author yli * */public class TransactionManager {private MethodAdvice advice;private List<SqlSessionFactory> factorys = new ArrayList<SqlSessionFactory>();private static ThreadLocal<Stack<String>> methodStack = new ThreadLocal<Stack<String>>();public void setFactorys(List<SqlSessionFactory> factorys,MethodAdvice advice) {this.factorys = factorys;this.advice = advice;}/** * 开始事务处理 * @param methodName */public void beginTransaction(String methodName) {// 获取当前执行线程的方法栈Stack<String> methodStack = this.getMethodStack();// 如果该方法匹配拦截点,则压入当前方法栈if (isMatchMethod(methodName)) {methodStack.push(methodName);// 如果当前栈只有1个方法,说明在第一个方法入口处if (methodStack.size() == 1) {System.out.println(methodName+ "【开始】设置:conn.setAutoCommit(false)");try {for (SqlSessionFactory factory : factorys) {factory.openSession().getConnection().setAutoCommit(false);}} catch (SQLException e) {e.printStackTrace();// 回滚rollBack();}}}}/** * 自定提交事务。。。也不是很好 * @param methodName */public void endTransaction(String methodName) {// 获取当前执行线程的方法栈Stack<String> methodStack = this.getMethodStack();// 如果该方法匹配拦截点,则从栈中移除当前方法if (isMatchMethod(methodName)) {methodStack.pop();// 如果栈中没有方法了:说明方法链已经全部执行完if (methodStack.isEmpty()) {try {for (SqlSessionFactory factory : factorys) {factory.openSession().getConnection().commit();}} catch (SQLException e) {e.printStackTrace();// 回滚rollBack();}System.out.println(methodName + "【结束】设置:conn.commit()");}}}// 自动回滚:将当前所有工程获取到的Sqlsession全部回滚掉// 不是很好,有些方法可能只使用了一个factory来创建SqlSessionpublic void rollBack() {for (SqlSessionFactory factory : factorys) {try {factory.openSession().getConnection().rollback();} catch (SQLException e) {e.printStackTrace();}}}    // 检查当前方法是否被匹配到了private boolean isMatchMethod(String methodName) {for (String match : advice.getMatches()) {if (methodName.toLowerCase().startsWith(match)) {return true;}}return false;}// 获取当前线程对应的方法栈private Stack<String> getMethodStack() {Stack<String> currentStack = methodStack.get();if (null == currentStack) {currentStack = new Stack<String>();methodStack.set(currentStack);}return currentStack;}}
package com.yli.sql.transaction;import java.lang.reflect.Method;import net.sf.cglib.proxy.MethodInterceptor;import net.sf.cglib.proxy.MethodProxy;/** * 前文提到使用JDK的动态代理来搞自动提交和回滚 * 这里用了 cglib 为每个类生成动态代理 * @author yli * */public class MethodTransaction implements MethodInterceptor {    // 使用事务管理器来管理实务private TransactionManager manager;public MethodTransaction(TransactionManager manager) {this.manager = manager;}@Overridepublic Object intercept(Object object, Method method, Object[] args,MethodProxy proxy) throws Throwable {String methodName = method.getName();System.out.println("=============>当前准备执行方法:" + methodName);// 开启事务manager.beginTransaction(methodName);// 执行目标方法Object result = null;try {result = proxy.invokeSuper(object, args);// 提交事务manager.endTransaction(methodName);} catch (Throwable e) {System.out.println("=============>当前方法:" + methodName + "全部回滚");manager.rollBack();}// 返回方法执行结果return result;}}


 

好了,截止到这里,我模拟的简单自动提交、回滚就这样了,当然还是有很多bug哈

那么来测试下,我没有用Spring 的IOC,就自己模拟一个IOC好了,真的是最简单的模拟。。。

4.测试

package com.yli.sql.test;import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map;import javax.sql.DataSource;import net.sf.cglib.proxy.Enhancer;import com.yli.sql.connection.MyDataSource;import com.yli.sql.session.SqlSessionFactory;import com.yli.sql.transaction.MethodAdvice;import com.yli.sql.transaction.MethodTransaction;import com.yli.sql.transaction.TransactionManager;/** * 定义一个Map * 把所有Bean创建好,并创建好依赖关系 * 当然DataSource要创建多个 * 那么SqlSessionFactory也是多个 * 并定义一个事务管理器、一个方法匹配器 * 然后为业务类定义代理类。。。 * 真正的Spring IOC 大家都搞过啊,就是这么干的 *  * @author yli * */public class BeanFactory {public static Map<String, Object> beanMap = new HashMap<String, Object>();public static void initBean() {// 数据源-1:test数据库String dirver = "com.mysql.jdbc.Driver";String url = "jdbc:mysql://localhost:3306/test";String user = "root";String password = "123456";DataSource da1 = new MyDataSource(dirver, url, user, password);// 数据源-2:test1数据库url = "jdbc:mysql://localhost:3306/test1";DataSource da2 = new MyDataSource(dirver, url, user, password);// Session工厂-1SqlSessionFactory factory1 = new SqlSessionFactory();factory1.setDataSource(da1);// Session工厂-2SqlSessionFactory factory2 = new SqlSessionFactory();factory2.setDataSource(da2);// DaoSupportUserDao userDao = new UserDao();userDao.setFactory1(factory1);userDao.setFactory2(factory2);// 定义哪些方法需要处理事务String[] methods = new String[] { "save", "update", "insert", "delete","execute", "modify" };MethodAdvice advice = new MethodAdvice(methods);// 事务管理类:将所有session工厂注入TransactionManager manager = new TransactionManager();List<SqlSessionFactory> factorys = new ArrayList<SqlSessionFactory>();factorys.add(factory1);factorys.add(factory2);manager.setFactorys(factorys, advice);// 业务Bean:主要针对Service层,哪些方法需要自动处理事务UserService userService = new UserService();userService.setUserDao(userDao);// 对于要求开启事务的[类==>方法列表]:生成代理类UserService userServiceProxy = (UserService) getProxyBean(UserService.class, manager);userServiceProxy.setUserDao(userDao);// 将所有Bean加载到BeanMapbeanMap.put("da1", da1);beanMap.put("da2", da2);beanMap.put("factory1", factory1);beanMap.put("factory2", factory2);beanMap.put("userService", userServiceProxy); // 注意此处的UserService设置为代理类beanMap.put("userService$", userService); // 原生的类则使用[相同名称+$]定位:Spring 也这么干}public static Object getProxyBean(Class<?> classz, TransactionManager manager) {Enhancer enhancer = new Enhancer();// 如果不能传递class进来,那就传className进来: Class.forName(className)// 也能根据类名获取class对象 ,比如Spring的bean配置文件,肯定是要配置类的全路径的enhancer.setSuperclass(classz);enhancer.setCallback(new MethodTransaction(manager));return enhancer.create();}}



再定义一个Dao和Servei吧

package com.yli.sql.test;import java.util.Map;import com.yli.sql.session.SessionDaoSupport;public class UserDao extends SessionDaoSupport{public boolean inserUserToDB1(Map<String, Object> params){String sql = "insert into user(name,sex) values(:name,:sex)";return getSession1().insert(sql, params) > 0;}public boolean inserUserToDB2(Map<String, Object> params){String sql = "insert into user(name,sex) values(:name,:sex)";return getSession2().insert(sql, params) > 0;}public boolean updateUserToDB1(Map<String, Object> params){String sql = "update user set name=:name where id=:Id";return getSession1().update(sql, params) > 0;}public boolean updateUserToDB2(Map<String, Object> params){String sql = "update user set name=:name where id=:Id";return getSession2().update(sql, params) > 0;}public boolean deleteUserToDB1(Map<String, Object> params){String sql = "delete from user where id=:Id";return getSession1().delete(sql, params) > 0;}public boolean deleteUserToDB2(Map<String, Object> params){String sql = "delete from user where id=:Id";return getSession2().delete(sql, params) > 0;}}


 

package com.yli.sql.test;import java.util.HashMap;import java.util.Map;public class UserService {private UserDao userDao;public void setUserDao(UserDao userDao) {this.userDao = userDao;}public void modifyUser() {Map<String, Object> params = new HashMap<String, Object>();// params.put("Id", 1);params.put("name", "hello");params.put("sex", "0");// insert => DB1this.userDao.inserUserToDB1(params);// 测试程序出错了回滚// System.out.println(1/0);// insert => DB2this.userDao.inserUserToDB2(params);// 有调用其他业务方法,也可以是其他业务类的方法insertUser();}public void insertUser() {Map<String, Object> params = new HashMap<String, Object>();params.put("name", "yli");params.put("sex", "1");// insert => DB1this.userDao.inserUserToDB1(params);// 测试程序出错了回滚// System.out.println(1 / 0);}}


最后来一个Main方法测试了

package com.yli.sql.test;public class MainTest {public static void main(String[] args) {BeanFactory.initBean();UserService user = (UserService)BeanFactory.beanMap.get("userService");user.modifyUser();}}


测试表明一起提交或者某个出错就一起回滚了。。。

好吧,只是简单地应用,有人浏览到这里,看看就行了

 对了我是用的mysql,在公司用db2也试了,哎,肯定是相同的结果

这个例子就依赖了 mysql5.5 和 cglib2.2的 jar包

 

 

 

 

0 0
原创粉丝点击