目錄
2.1 問題背景
2.2 方案1-修改接口傳參
四.其他使用場景
一.ThreadLocal介紹
我們知道,變量從作用域范圍進行分類,可以分為“全局變量”、“局部變量”兩種:
1.全局變量(global variable),比如類的靜態屬性(加static關鍵字),在類的整個生命周期都有效;
2.局部變量(local variable),比如在一個方法中定義的變量,作用域只是在當前方法內,方法執行完畢后,變量就銷毀(釋放)了;
使用全局變量,當多個線程同時修改靜態屬性,就容易出現並發問題,導致臟數據;而局部變量一般來說不會出現並發問題(在方法中開啟多線程並發修改局部變量,仍可能引起並發問題);
再看ThreadLocal,從名稱上就能知道,它可以用來保存局部變量,只不過這個“局部”是指“線程”作用域,也就是說,該變量在該線程的整個生命周期中有效。
二.使用場景1——數據庫事務問題
2.1問題背景
下面介紹示例,UserService調用UserDao刪除用戶信息,涉及到兩張表的操作,所以用到了數據庫事務:
數據庫封裝類DbUtils
public class DbUtils { // 使用C3P0連接池 private static ComboPooledDataSource dataSource = new ComboPooledDataSource("dev"); public static Connection getConnectionFromPool() throws SQLException { return dataSource.getConnection(); } // 省略其他方法..... }
UserService代碼如下:
public class UserService { private UserDao userDao; public void deleteUserInfo(Integer id, String operator) { Connection connection = null; try { // 從連接池中獲取一個連接 connection = DbUtils.getConnectionFromPool(); // 因為涉及事務操作,所以需要關閉自動提交 connection.setAutoCommit(false); // 事務涉及兩步操作,刪除用戶表,增加操作日志 userDao.deleteUserById(id); userDao.addOperateLog(id, operator); connection.commit(); } catch (SQLException e) { // 回滾操作 try { if (connection != null) { connection.rollback(); } } catch (SQLException ex) { } } finally { DbUtils.freeConnection(connection); } } }
下面是UserDao,省略了部分代碼:
package cn.ganlixin.dao; import cn.ganlixin.util.DbUtils; import java.sql.Connection; /** * @author ganlixin * @create 2020-06-12 */ public class UserDao { public void deleteUserById(Integer id) { // 從連接池中獲取一個數據連接 Connection connection = DbUtils.getConnectionFromPool(); // 利用獲取的數據庫連接,執行sql...........刪除用戶表的一條數據 // 歸還連接給連接池 DbUtils.freeConnection(connection); } public void addOperateLog(Integer id, String operator) { // 從連接池中獲取一個數據連接 Connection connection = DbUtils.getConnectionFromPool(); // 利用獲取的數據庫連接,執行sql...........插入一條記錄到操作日志表 // 歸還連接給連接池 DbUtils.freeConnection(connection); } }
上面的代碼乍一看,好像沒啥問題,但是仔細看,其實是存在問題的!!問題出在哪兒呢?就出在從數據庫連接池獲取連接哪個位置。
1.UserService會從數據庫連接池獲取一個連接,關閉該連接的自動提交;
2.UserService然后調用UserDao的兩個接口進行數據庫操作;
3.UserDao的兩個接口,都會從數據庫連接池獲取一個連接,然后執行sql;
注意,第1步和第3步獲得的連接不一定是同一個!!!!這才是關鍵。
如果UserService和UserDao獲取的數據庫連接不是同一個,那么UserService中關閉自動提交的數據庫連接,並不是UserDao接口中執行sql的數據庫連接,當userService中捕獲異常,即使執行rollback,userDao中的sql已經執行完了,並不會回滾,所以數據已經出現不一致!!!
2.2方案1-修改接口傳參
上面的例子中,因為UserService和UserDao獲取的連接不是同一個,所以並不能保證事務原子性;那么只要能夠解決這個問題,就可以保證了
可以修改userDao中的代碼,不要每次在UserDao中從數據庫連接池獲取連接,而是增加一個參數,該參數就是數據庫連接,有UserService傳入,這樣就能保證UserService和UserDao使用同一個數據庫連接了
public class UserDao { public void deleteUserById(Connection connection, Integer id) { // 利用傳入的數據庫連接,執行sql...........刪除用戶表的一條數據 } public void addOperateLog(Connection connection, Integer id, String operator) { // 利用傳入的數據庫連接,執行sql...........插入一條記錄到操作日志表 } }
UserService調用接口時,傳入數據庫連接,修改代碼后如下:
// 事務涉及兩步操作,刪除用戶表,增加操作日志 // 新增參數傳入數據庫連接,保證UserService和UserDao使用同一個連接 userDao.deleteUserById(connection, id); userDao.addOperateLog(connection, id, operator);
這樣做,的確是能解決數據庫事務的問題,但是並不推薦這樣做,耦合度太高,不利於維護,修改起來也不方便;
2.3使用ThreadLocal解決
ThreadLocal可以保存當前線程有效的變量,正好適合解決這個問題,而且改動的點也特別小,只需要在DbUtils獲取連接的時候,將獲取到的連接存到ThreadLocal中即可:
public class DbUtils { // 使用C3P0連接池 private static ComboPooledDataSource dataSource = new ComboPooledDataSource("dev"); // 創建threadLocal對象,保存每個線程的數據庫連接對象 private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>(); public static Connection getConnectionFromPool() throws SQLException { if (threadLocal.get() == null) { threadLocal.set(dataSource.getConnection()); } return threadLocal.get(); } // 省略其他方法..... }
然后UserService和UserDao中,恢復最初的版本,UserService和UserDao中都調用DbUtils獲取數據庫連接,此時他們獲取到的連接則是同一個Connection對象,就可以解決數據庫事務問題了。
三.使用場景2——日志追蹤問題
如果理解了場景1的數據庫事務問題,那么對於本小節的日志追蹤,光看標題就知道是怎么回事了;
開發過程時,會在項目中打很多的日志,一般來說,查看日志的時候,都是通過關鍵字去找日志,這就需要我們在打日志的時候明確的寫入某些標識,比如用戶ID、訂單號、流水號...
如果業務比較復雜,那么一個請求的處理流程就會比較長,如果將這么一長串的流程給串起來,也可以通過前面說的用戶ID、訂單號、流水號來串,但有個問題,某些接口沒有用戶ID或者訂單號作為參數!!!!這個時候,雖然可以像場景1中給接口增加用戶ID或者訂單號作為參數,但是並不推薦這么做。
此時可以就可以使用ThreadLocal,封裝一個工具類,提供唯一標識(可以是用戶ID、訂單號、或者是分布式全局ID),示例如下:
package cn.ganlixin.util; /** * 描述: * 日志追蹤工具類,設置和獲取traceId, * 此處的traceId使用snowFlake雪花數算法,詳情可以參考:https://www.cnblogs.com/-beyond/p/12452632.html * * @author ganlixin * @create 2020-06-12 */ public class TraceUtils { // 創建ThreadLocal靜態屬性,存Long類型的uuid private static final ThreadLocal<Long> threadLocal = new ThreadLocal<>(); // 全局id生成器(雪花數算法) private static final SnowFlakeIdGenerator generator = new SnowFlakeIdGenerator(1, 1); public static void setUuid(String uuid) { // 雪花數算法 threadLocal.set(generator.nextId()); } public static Long getUuid() { if (threadLocal.get() == null) { threadLocal.set(generator.nextId()); } return threadLocal.get(); } }
使用示例:
@Slf4j public class UserService { private UserDao userDao; public void deleteUserInfo(Integer id, String operator) { log.info("traceId:{}, id:{}, operator:{}", TraceUtils.getUuid(), id, operator); //..... } } @Slf4j public class UserDao { public void deleteUserById(Connection connection, Integer id) { log.info("traceId:{}, id:{}", TraceUtils.getUuid(), id); } }
四.其他場景
其他場景,其實就是利用ThreadLocal“線程私有且線程間互不影響”特性,除了上面的兩個場景,常見的還有用來記錄用戶的登錄狀態(當然也可以用session或者cookie實現)。