一.ThreadLocal介紹html
二.使用場景1——數據庫事務問題java
2.1 問題背景算法
2.2 方案1-修改接口傳參sql
2.3 方案2-使用ThreadLocal數據庫
三.使用場景2——日誌追蹤問題api
四.其餘使用場景cookie
咱們知道,變量從做用域範圍進行分類,能夠分爲「全局變量」、「局部變量」兩種:session
1.全局變量(global variable),好比類的靜態屬性(加static關鍵字),在類的整個生命週期都有效;多線程
2.局部變量(local variable),好比在一個方法中定義的變量,做用域只是在當前方法內,方法執行完畢後,變量就銷燬(釋放)了;併發
使用全局變量,當多個線程同時修改靜態屬性,就容易出現併發問題,致使髒數據;而局部變量通常來講不會出現併發問題(在方法中開啓多線程併發修改局部變量,仍可能引發併發問題);
再看ThreadLocal,從名稱上就能知道,它能夠用來保存局部變量,只不過這個「局部」是指「線程」做用域,也就是說,該變量在該線程的整個生命週期中有效。
下面介紹示例,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已經執行完了,並不會回滾,因此數據已經出現不一致!!!
上面的例子中,由於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);
這樣作,的確是能解決數據庫事務的問題,可是並不推薦這樣作,耦合度過高,不利於維護,修改起來也不方便;
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對象,就能夠解決數據庫事務問題了。
若是理解了場景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實現)。