目錄
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實現)。