RocksDB線程局部緩存

来源:https://www.cnblogs.com/cchust/archive/2019/09/22/11562949.html
-Advertisement-
Play Games

概述 在開發過程中,我們經常會遇到併發問題,解決併發問題通常的方法是加鎖保護,比如常用的spinlock,mutex或者rwlock,當然也可以採用無鎖編程,對實現要求就比較高了。對於任何一個共用變數,只要有讀寫併發,就需要加鎖保護,而讀寫併發通常就會面臨一個基本問題,寫阻塞讀,或則寫優先順序比較低, ...


概述

      在開發過程中,我們經常會遇到併發問題,解決併發問題通常的方法是加鎖保護,比如常用的spinlock,mutex或者rwlock,當然也可以採用無鎖編程,對實現要求就比較高了。對於任何一個共用變數,只要有讀寫併發,就需要加鎖保護,而讀寫併發通常就會面臨一個基本問題,寫阻塞讀,或則寫優先順序比較低,就會出現寫餓死的現象。這些加鎖的方法可以歸類為悲觀鎖方法,今天介紹一種樂觀鎖機制來控制併發,每個線程通過線程局部變數緩存共用變數的副本,讀不加鎖,讀的時候如果感知到共用變數發生變化,再利用共用變數的最新值填充本地緩存;對於寫操作,則需要加鎖,通知所有線程局部變數發生變化。所以,簡單來說,就是讀不加鎖,讀寫不衝突,只有寫寫衝突。這個實現邏輯來源於Rocksdb的線程局部緩存實現,下麵詳細介紹Rocksdb的線程局部緩存ThreadLocalPtr的原理。

線程局部存儲(TLS)

簡單介紹下線程局部變數,線程局部變數就是每個線程有自己獨立的副本,各個線程對其修改相互不影響,雖然變數名相同,但存儲空間並沒有關係。一般在linux 下,我們可以通過以下三個函數來實現線程局部存儲創建,存取功能。

int pthread_key_create(pthread_key_t *key, void (*destr_function) (void*)), 
int pthread_setspecific(pthread_key_t key, const void *pointer) ,
void * pthread_getspecific(pthread_key_t key)

ThreadLocalPtr類

     有時候,我們並不想要各個線程獨立的變數,我們仍然需要一個全局變數,線程局部變數只是作為全局變數的緩存,用以緩解併發。在RocksDB中ThreadLocalPtr這個類就是來乾這個事情的。ThreadLocalPtr類包含三個內部類,ThreadLocalPtr::StaticMeta,ThreadLocalPtr::ThreadData和ThreadLocalPtr::Entry。其中StaticMeta是一個單例,管理所有的ThreadLocalPtr對象,我們可以簡單認為一個ThreadLocalPtr對象,就是一個線程局部存儲(ThreadLocalStorage)。但實際上,全局我們只定義了一個線程局部變數,從StaticMeta構造函數可見一斑。那麼全局需要多個線程局部緩存怎麼辦,實際上是在局部存儲空間做文章,線程局部變數實際存儲的是ThreadData對象的指針,而ThreadData裡面包含一個數組,每個ThreadLocalPtr對象有一個獨立的id,在其中占有一個獨立空間。獲取某個變數局部緩存時,傳入分配的id即可,每個Entry中ptr指針就是對應變數的指針。

ThreadLocalPtr::StaticMeta::StaticMeta() : next_instance_id_(0), head_(this) {
  if (pthread_key_create(&pthread_key_, &OnThreadExit) != 0) {
    abort();
  }
  ......
}

void* ThreadLocalPtr::StaticMeta::Get(uint32_t id) const {
   auto* tls = GetThreadLocal();
   return tls->entries[id].ptr.load(std::memory_order_acquire);
}

struct Entry {
  Entry() : ptr(nullptr) {}
  Entry(const Entry& e) : ptr(e.ptr.load(std::memory_order_relaxed)) {}
  std::atomic<void*> ptr;
};

整體結構如下:每個線程有一個線程局部變數ThreadData,裡面包含了一組ThreadLocalPtr的指針,對應的是多個變數,同時ThreadData之間相互通過指針串聯起來,這個非常重要,因為執行寫操作時,寫線程需要修改所有thread的局部緩存值來通知共用變數發生變化了。

 ---------------------------------------------------
 |          | instance 1 | instance 2 | instnace 3 |
 ---------------------------------------------------
 | thread 1 |    void*   |    void*   |    void*   | <- ThreadData
 ---------------------------------------------------
 | thread 2 |    void*   |    void*   |    void*   | <- ThreadData
 ---------------------------------------------------
 | thread 3 |    void*   |    void*   |    void*   | <- ThreadData

struct ThreadData {
  explicit ThreadData(ThreadLocalPtr::StaticMeta* _inst)
      : entries(), inst(_inst) {}
  std::vector<Entry> entries;
  ThreadData* next;
  ThreadData* prev;
  ThreadLocalPtr::StaticMeta* inst;
};

讀寫無併發衝突

     現在說到最核心的問題,我們如何實現利用TLS來實現本地局部緩存,做到讀不上鎖,讀寫無併發衝突。讀、寫邏輯和併發控制主要通過ThreadLocalPtr中通過3個關鍵介面Swap,CompareAndSwap和Scrape實現。對於ThreadLocalPtr< Type* > 變數來說,在具體的線程局部存儲中,會保存3中不同類型的值:

  1). 正常的Type* 類型指針;

  2). 一個Type*類型的Dummy變數,記為InUse;

  3). nullptr值,記為obsolote;

讀線程通過Swap介面來獲取變數內容,寫線程則通過Scrape介面,遍歷並重置所有ThreadData為(obsolote)nullptr,達到通知其他線程局部緩存失效的目的。下次讀線程再讀取時,發現獲取的指針為nullptr,就需要重新構造局部緩存。

//獲取某個id對應的局部緩存內容,每個ThreadLocalPtr對象有單獨一個id,通過單例StaticMeta對象管理。
void* ThreadLocalPtr::StaticMeta::Swap(uint32_t id, void* ptr) {
//獲取本地局部緩存
auto* tls = GetThreadLocal();                                        

  return tls->entries[id].ptr.exchange(ptr, std::memory_order_acquire);
}

bool ThreadLocalPtr::StaticMeta::CompareAndSwap(uint32_t id, void* ptr,
                                                void*& expected) {
  //獲取本地局部緩存
  auto* tls = GetThreadLocal();
  return tls->entries[id].ptr.compare_exchange_strong(
      expected, ptr, std::memory_order_release, std::memory_order_relaxed);
}

//將所有管理的對象指針設置為nullptr,將過期的指針返回,供上層釋放,
//下次進行從局部線程棧獲取時,發現內容為nullptr,則重新申請對象。
void ThreadLocalPtr::StaticMeta::Scrape(uint32_t id, std::vector<void*>* ptrs, void* const replacement) {                           
  MutexLock l(Mutex());
  for (ThreadData* t = head_.next; t != &head_; t = t->next) {                               
    if (id < t->entries.size()) {                                                            
      void* ptr =
          t->entries[id].ptr.exchange(replacement, std::memory_order_acquire);               
      if (ptr != nullptr) {
  //搜集各個線程緩存,進行解引用,必要時釋放記憶體
  ptrs->push_back(ptr);
      }                                                                            
    }
  } 
}

//初始化,或者被替換為nullptr後,說明緩存對象已經過期,需要重新申請。
ThreadData* ThreadLocalPtr::StaticMeta::GetThreadLocal() {
   申請線程局部的ThreadData對象,通過StaticMeta對象管理成一個雙向鏈表,每個instance對象管理一組線程局部對象。
   if (UNLIKELY(tls_ == nullptr)) {
     auto* inst = Instance();
     tls_ = new ThreadData(inst);
     {                                                                        
      // Register it in the global chain, needs to be done before thread exit
      // handler registration                                                
      MutexLock l(Mutex());                                                  
      inst->AddThreadData(tls_);
     }
    return tls_;                                             
  }
}

讀操作包括兩部分,Get和Release,這裡面除了從TLS中獲取緩存,還涉及到一個釋放舊對象記憶體的問題。Get時,利用InUse對象替換TLS對象,Release時再將TLS對象替換回去,讀寫沒有併發的場景比較簡單,如下圖,其中TLS Object代表本地線程局部緩存,GlobalObject是全局共用變數,對所有線程可見。

下麵我們再看看讀寫有併發的場景,讀線程讀到TLS object後,寫線程修改了全局對象,並且遍歷對所有的TLS object進行修改,設置nullptr。在此之後,讀線程進行Release時,compareAndSwap失敗,感知到使用的object已經過期,執行解引用,必要時釋放記憶體。當下次再次Get object時,發現TLS object為nullptr,就會使用當前最新的object,併在使用完成後,Release階段將object填回到TLS。

應用場景

      從前面的分析來看,TLS作為cache,仍然需要一個全局變數,全局變數保持最新值,而TLS則可能存在滯後,這就要求我們的使用場景不要求讀寫要實時嚴格一致,或者能容忍多版本。全局變數和局部緩存有交互,交互邏輯是,全局變數變化後,局部線程要能及時感知到,但不需要實時。允許讀寫併發,即允許讀的時候,使用舊值讀,待下次讀的時候,再獲取到新值。Rocksdb中的superversion管理則符合這種使用場景,swich/flush/compaction會產生新的superversion,讀寫數據時,則需要讀supversion。往往讀寫等前臺操作相對於switch/flush/compaction更頻繁,所以讀superversion比寫superversion比例更高,而且允許系統中同時存留多個superversion。

每個線程可以拿superversion進行讀寫,若此時併發有flush/compaction產生,會導致superversion發生變化,只要後續再次讀取superversion時,能獲取到最新即可。細節上來說,擴展到應用場景,一般在讀場景下,我們需要獲取snapshot,並藉助superversion信息來確認這次讀取要讀哪些物理介質(mem,imm,L0,L1...LN)。

1).獲取snapshot後,拿superversion之前,其它線程做了flush/compaction導致superversion變化

這種情況下,可以拿到最新的superversion。

2).獲取snapshot後,拿superversion之後,其它線程做了flush/compaction導致superversion變化

這種情況下,雖然superversion比較舊,但是依然包含了所有snapshot需要的數據。那麼為什麼需要及時獲取最新的superversion,這裡主要是為了回收廢棄的sst文件和memtable,提高記憶體和存儲空間利用率。

總結

     RocksDB的線程局部緩存是一個很不錯的實現,用戶使用局部緩存可以大大降低讀寫併發衝突,尤其在讀遠大於寫的場景下,整個緩存維護代價也比較低,只有寫操作時才需要鎖保護。只要系統中允許共用變數的多版本存在,並且不要求實時保證一致,那麼線程局部緩存是提升併發性能的一個不錯的選擇。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一,設計規範 三大範式 第一範式1NF:屬性不可分【反例:address可1分為國家,省市,地區】 第二範式2NF:屬性完全依賴主鍵 【反例:訂單編號和商品編號位於同一張表中,前者與訂單信息強相關,後者與商品信息強相關】【該拆表了】 第三範式3NF:不允許數據冗餘【兩張表很多屬性相同】 命名規範 1 ...
  • 註意:新版mysql驅動的url必須設置時區,即serverTimezone=UTC,否則會報如下錯誤: ...
  • 排錯-解決MySQL非聚合列未包含在GROUP BY子句報錯問題 By:授客 QQ:1033553122 測試環境 win10 MySQL 5.7 問題描述: 執行類似以下mysql查詢, SELECT id, name, count(*) AS cnt FROM case_table GROUP ...
  • 1.查看mongodb服務是否開啟: ps -ef | grep mongod 2.管理員角色必須在啟用--auth認證參數之前創建,否則會沒有操作許可權。如果之前已經創建過用戶,請先刪除。 kill掉mongod服務,重新啟動,以noauth模式啟動: mongod --dbpath /var/lo ...
  • 1.dos命令 set names gbk; 2.MySQL練習#創建school資料庫: create database school;#切換school資料庫: use school; # primary key : 主鍵約束,不可重覆# auto_increment : 自動增長# not n ...
  • 從bson中導入ObjectId對象,將字元串轉換成id對象查詢使用: ...
  • 如何更規範化使用MySQL 背景:一個平臺或系統隨著時間的推移和用戶量的增多,資料庫操作往往會變慢;而在Java應用開發中資料庫更是尤為重要,絕大多數情況下資料庫的性能決定了程式的性能,如若前期埋下的坑越多到後期資料庫就會成為整個系統的瓶頸;因此,更規範化的使用MySQL在開發中是不可或缺的。 一、 ...
  • 一.什麼是大數據 大數據(big data)是指無法在一定時間範圍內用常規軟體工具進行捕捉、管理和處理的數據集合,是需要新處理模式才能具有更強的決策力、洞察發現力和流程優化能力的海量、高增長率和多樣化的信息資產。大數據指不用隨機分析法(抽樣調查)這樣捷徑,而採用所有數據進行分析處理。大數據的5V特點 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...