浅谈 ThreadLocal 在持久化的作用

125 阅读3分钟

浅谈 ThreadLocal 在持久化的作用

补充知识可以先看看这一篇: MVC开发规范的常见问题

ThreadLocal 是什么?

ThreadLocal 底层就是用 Map 来实现的 只不过他的 key 是 Thread 类型,value 是 Object 以便存放元素
而在持久化的作用,是让事务跨 Dao、service 层获取连接对象

持久化的什么场景需要用到 ThreadLocal ?

当在 JavaWeb 的 Servlet 开发时,使用 MVC 规范的三层架构时,在需要有事务控制的业务需要使用到 ThreadLocal 的帮助

我们知道,事务一般是在 Service层中开启 / 关闭的。然而,Connection 对象并不在 Service层开启,而是在 Dao 中开启 但这就存在一个问题了:那就是如果有多条 SQL 语句执行的话,就不能确保原子性了,事务就不能保证


持久化时容易出现的问题

未使用 ThreadLocal 的 DBUtils

public class DBUtils {
    private static Connection conn = null;
    static {
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
​
    /**
     * 获取连接
     */
    public static Connection getconnection() {
        try {
            conn = DriverManager.getConnection(URL,USERNAME,PASSWORD);
        } catch (SQLException throwables) { }
        return conn;
    }
    
    /**
     * 释放资源
     */
    public static void close(){
        if (conn != null) {
            try {
                conn.close();
            } catch (SQLException throwables) {  }
        }
    }
}
​

Dao 是这样子定义的

public Integer insert(Enterprise enterprise, String[] invregnums, String[] regcaps, String[] scales) throws SQLException {
        String sql = "insert into t_enterprise values(?,?,?,?,?,?,?,?,?,?,?)";
        ps = DBUtil.createPrepareStatement(sql); // 注意一下这里
}

Service 是这样的

在获取 Connection 连接的时候还没出现问题,问题出在了 Service 的事务控制上

/**
 * 在这里,query,insert 是两条独立的语句了,并没有原子性(即使已经开启了事务,但Connection不是同一个,所以不起作用!)
 */
public Integer insert(Enterprise enterprise, String[] invregnums, String[] regcaps, String[] scales) throws SQLException {
        try {
            DBUtils.beginTransaction(conn); // 在这里有问题
            count = enterpriseDao.query(); // 查询
            result = enterpriseDao.insert(enterprise,invregnums,regcaps,scales); // insert
            DBUtils.commitTransaction(conn);
        } catch (SQLException throwables) {
                DBUtils.rollbackTransaction(conn);
        }finally {
            DBUtils.endTransaction(conn); 
            DBUtils.close(); 
        }
        return count;
}

问题所在

于是我们发现了问题:service 中的 Connection 和 Dao 中的 Connection 不是同一个对象 而都是分别开启了不同的连接对象,所以不能保证事务的成成功执行!!! Connection 在一条 SQL 语句执行完毕之后就已经关闭了,但我们要在事务完成之后才能将 Connection 关闭才是正确的做法


解决办法一(无 ThreadLocal)

Connection 作为参数从 Service 传入到 Dao,然后在 DBUtils 上写好方法重载(带 Connection 的形参)

// Service中
enterpriseDao.query("sql", connection);
​
// Dao中
public Integer insert(String sql, Connection conn) {
        ps = DBUtil.createPrepareStatement(sql, conn); 
}
​
// DBUtils中
public static PreparedStatement createPrepareStatement(String sql, Connection conn) {
        this.conn = conn;
        PreparedStatement ps = conn.prepareStatement(sql);
        return ps;
}

缺点

虽然解决了事务控制的问题,保证了使用同一个 Connection 对象 但方法的形参变多了,导致Service传参、Dao接口、DaoImpl、DBUtils 的方法上都要多出来一个 Connection 这样子代码就很累赘,而且耦合度增加了


解决方法二(使用 ThreadLocal)

DBUtils 定义

public class DBUtils {
    private static ThreadLocal threadLocal = new ThreadLocal();
    private static Connection conn = null;
    static {
        Class.forName("com.mysql.cj.jdbc.Driver");
    }
    
    /**
     * 获取连接
     */
    public static Connection getConnection() {
        conn = (Connection) threadLocal.get(); // 首先看 ThreadLocal 中有没有 conn
        if (conn == null){ // 如果没有就新建
            try {
                conn = DriverManager.getConnection(URL,USERNAME,PASSWORD);
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }
            threadLocal.set(conn);
        }
        return conn; // 如果有就直接返回
    }
    
    /**
     *获取 PreparedStatement
     */
    public static PreparedStatement createPrepareStatement(String sql) throws SQLException {
        conn = getConnection();
        PreparedStatement ps = conn.prepareStatement(sql);
        return ps;
    }
    
    /**
     * 释放资源
     */
    public static void close(){
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }
        }
        if (conn != null) {
            try {
                threadLocal.remove(); // 当前线程一定要和连接对象解除绑定关系!!!
                conn.close();
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }
        }
    }
​
    public static void close(ResultSet rs){
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }
        }
        close();
    }
}
​

这样子就保证了一个线程对应一个 Connection 对象


Dao 方法

ps = DBUtil.createPrepareStatement(sql);

Service 层调用

DBUtils.beginTransaction(conn); // 开启事务
count = enterpriseDao.query(); 
result = enterpriseDao.insert(enterprise,invregnums,regcaps,scales); 
DBUtils.commitTransaction(conn); // 提交事务// 提交、回滚、释放Connection 代码略...

这样子就显得干净很多了,代码结构没那么混乱


ThreadLocal 实现原理

import java.util.HashMap;
import java.util.Map;
​
/**
 * ThreadLocal 作用是让事务跨 Dao、service 层获取连接对象
 * 因为在 service 层中控制事务,必须保证 service 方法执行的连接对象和 dao 方法中的 Connection 是同一个
 * Author:  chenjunjia
 * Date:    2021/11/13 18:37
 * WeChat:  China_JoJo_
 * Blog:    https://juejin.cn/user/1856417285289304/posts
 * Github:  https://github.com/chenjjiaa
 */
public class ThreadLocal<T> {
​
    Map<Thread,T> threadLocalMap = new HashMap();
​
    public void set(T obj){
        threadLocalMap.put(Thread.currentThread(),obj);
    }
​
    public T get(){
        return threadLocalMap.get(Thread.currentThread());
    }
​
    public void remove(){
        threadLocalMap.remove(Thread.currentThread());
    }
}



end……