ThreadLocal的使用場景分析

尋覓beyond發表於2020-06-12

目錄

一.ThreadLocal介紹

二.使用場景1——資料庫事務問題

  2.1 問題背景

  2.2 方案1-修改介面傳參

  2.3 方案2-使用ThreadLocal

三.使用場景2——日誌追蹤問題

四.其他使用場景

 

 

一.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實現)。

 

  原文地址:https://www.cnblogs.com/-beyond/p/13111015.html 

相關文章