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
  • JWT(JSON Web Token)是一種用於在網路應用之間傳遞信息的開放標準(RFC 7519)。它使用 JSON 對象在安全可靠的方式下傳遞信息,通常用於身份驗證和信息交換。 在Web API中,JWT通常用於對用戶進行身份驗證和授權。當用戶登錄成功後,伺服器會生成一個Token並返回給客戶端 ...
  • 老周在幾個世紀前曾寫過樹莓派相關的 iOT 水文,之所以沒寫 Nano Framework 相關的內容,是因為那時候這貨還不成熟,可玩性不高。不過,這貨現在已經相對完善,老周都把它用在項目上了——第一個是自製的智能插座,這個某寶上50多塊可以買到,搜“esp32 插座”就能找到。一種是 86 型盒子 ...
  • 引言 上一篇我們創建了一個Sample.Api項目和Sample.Repository,並且帶大家熟悉了一下Moq的概念,這一章我們來實戰一下在xUnit項目使用依賴註入。 Xunit.DependencyInjection Xunit.DependencyInjection 是一個用於 xUnit ...
  • 在 Avalonia 中,樣式是定義控制項外觀的一種方式,而控制項主題則是一組樣式和資源,用於定義應用程式的整體外觀和感覺。本文將深入探討這些概念,並提供示例代碼以幫助您更好地理解它們。 樣式是什麼? 樣式是一組屬性,用於定義控制項的外觀。它們可以包括背景色、邊框、字體樣式等。在 Avalonia 中,樣 ...
  • 在處理大型Excel工作簿時,有時候我們需要在工作表中凍結窗格,這樣可以在滾動查看數據的同時保持某些行或列固定不動。凍結窗格可以幫助我們更容易地導航和理解複雜的數據集。相反,當你不需要凍結窗格時,你可能需要解凍它們以獲得完整的視野。 下麵將介紹如何使用免費.NET庫通過C#實現凍結Excel視窗以鎖 ...
  • .NET 部署 IIS 的簡單步驟一: 下載 dotnet-hosting-x.y.z-win.exe ,下載地址:.NET Downloads (Linux, macOS, and Windows) (microsoft.com) .NET 部署 IIS 的簡單步驟二: 選擇對應的版本,點擊進入詳 ...
  • 拓展閱讀 資料庫設計工具-08-概覽 資料庫設計工具-08-powerdesigner 資料庫設計工具-09-mysql workbench 資料庫設計工具-10-dbdesign 資料庫設計工具-11-dbeaver 資料庫設計工具-12-pgmodeler 資料庫設計工具-13-erdplus ...
  • 初識STL STL,(Standard Template Library),即"標準模板庫",由惠普實驗室開發,STL中提供了非常多對信息學奧賽很有用的東西。 vector vetor是STL中的一個容器,可以看作一個不定長的數組,其基本形式為: vector<數據類型> 名字; 如: vector ...
  • 前言 最近自己做了個 Falsk 小項目,在部署上伺服器的時候,發現雖然不乏相關教程,但大多都是將自己項目代碼複製出來,不講核心邏輯,不太簡潔,於是將自己部署的經驗寫成內容分享出來。 uWSGI 簡介 uWSGI: 一種實現了多種協議(包括 uwsgi、http)並能提供伺服器搭建功能的 Pytho ...
  • 1 文本Embedding 將整個文本轉化為實數向量的技術。 Embedding優點是可將離散的詞語或句子轉化為連續的向量,就可用數學方法來處理詞語或句子,捕捉到文本的語義信息,文本和文本的關係信息。 ◉ 優質的Embedding通常會讓語義相似的文本在空間中彼此接近 ◉ 優質的Embedding相 ...