1、一次一千萬條數據插入和刪除案例:
第一次:插入--批量插入,每次插入5000條數據,總耗時28min,數據無異常
? ? ? ? ? ? ? ? 刪除--通過sql語句一次性刪除,總耗時1h52min;一次刪除的數據過多導致mysql的備份恢復文件極其龐大,相應的日志文件也在不停的膨脹
第二次:鑒于第一次插入和刪除都非常耗時,對mysql參數和表做了如下修改;
- ?查看當前設置:
SHOW VARIABLES LIKE 'max_allowed_packet';
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';
SHOW VARIABLES LIKE 'innodb_log_file_size';
SHOW VARIABLES LIKE 'innodb_log_buffer_size';
其中max_allowed_packet:
默認值通常為4MB或16MB。(實際為1M)
如果需要處理大型數據包(如包含大量數據的INSERT語句),可能需要將其設置為更大的值,如64MB或更大。但請注意,增加此值也會增加內存使用。(一次批處理數據超過1萬條就放不下)
innodb_buffer_pool_size:(能等待多次批處理數據,再和數據庫交互,實際設置為1024M)
建議將其設置為系統內存的50%-80%。
如果服務器上只運行MySQL,可以設置為系統內存的更高比例(如80%)。
如果還運行其他應用程序,可能需要將其設置為較低的比例(如70%)。
innodb_log_file_size:(默認為24M,無法修改,修改后mysql服務無法啟動)
建議將其設置為系統總內存的10%-25%。
較大的日志文件可以減少寫入頻率,但也會增加恢復時間。
innodb_log_buffer_size:(默認為1M,修改為4M)
默認值通常足夠用于大多數應用程序。
如果執行大量小事務,并且發現重做日志寫入成為性能瓶頸,可以考慮增加此值。
- 找到MySQL的配置文件(通常是my.cnf或my.ini),并編輯它以包含新的參數值。
編輯配置文件:
打開my.cnf或my.ini文件。
在[mysqld]部分下添加或修改以下行:
[mysqld] ?
max_allowed_packet = 64M ?
innodb_buffer_pool_size = 1024M??
innodb_log_file_size = 24M ?
innodb_log_buffer_size = 4M - 重啟驗證和注意事項
重啟MySQL服務
驗證更改
修改innodb_log_file_size時,需要特別注意,因為只有在MySQL服務完全停止后才能更改此值。如果MySQL正在運行,則無法直接修改此值。
在修改任何配置之前,請務必備份原始配置文件,以防止意外情況發生。 - 將對應表的非主鍵索引和外鍵都暫時禁用(插入時需要用到mysql的自增功能,主鍵和主鍵的索引不能刪除(在批量刪除時,如果去除主鍵和主鍵索引,反而會變得更慢);禁用方式為臨時刪除,后期再加上)
- 插入時分批添加,一次批處理數據量為5萬條,耗時6-7min;刪除時也是分批刪除,一次批處理數據量為5萬條,耗時2-3min
第三次:考慮在插入數據時使用線程池多線程進行批量插入,總耗時也是在6-7min,數據無異常,本次多線程對插入時間提升不明顯。以下是第二次和第三次操作的具體實現:
第二次插入
if (users.size() >= 50000) {userDao.insertBatch(users);
// users集合清空users.clear();}
第三次插入:多線程中使用的集合為線程安全的集合,每個線程都只會批量插入自己線程中的副本數據;每次批量插入完成(或者發生異常時)需要將存在本地變量中的集合清空,并且將本地變量清空(防止內存溢出);
ThreadLocal<List<User>> threadLocalObjects = new ThreadLocal<>();if (users.size() >= 50000) {CopyOnWriteArrayList<User> usersCopy = new CopyOnWriteArrayList<>(users);threadPoolExecutor.execute(() -> {try {threadLocalObjects.set(usersCopy);// 現在,對threadLocalObjects.get()的操作都是在副本上進行,線程安全userDao.insertBatch(threadLocalObjects.get());} catch (Exception e) {// 異常處理邏輯,可以根據實際情況調整e.printStackTrace();} finally {// 確保在所有情況下都清除線程局部變量threadLocalObjects.get().clear();threadLocalObjects.remove();}});users.clear();}}
// 等待線程執行完畢,但是不再接受新的任務threadPoolExecutor.shutdown();if(!threadPoolExecutor.awaitTermination(20, TimeUnit.MINUTES)){System.out.println("線程執行超時");}System.out.println("數據填充完畢");
分批刪除
// 1千萬條數據刪除,不推薦多線程刪除
// 多次批量刪除,臨時禁用索引ArrayList<Integer> ids = new ArrayList<>();Integer count = userDao.selectCount(new QueryWrapper<>(new User()));for (int i = 40; i < count + 9; i++) {ids.add(i);if (ids.size() >= 50000) {userDao.deleteBatchIds(ids);
// 清空ids集合ids.clear();}if (i == count - 1 + 9 && ids.size() > 0) {userDao.deleteBatchIds(ids);
// 清空ids集合ids.clear();}}System.out.println("數據刪除完畢");
2、redis實現session共享
第一是創建令牌的程序,就是在用戶初次訪問服務器時,給它創建一個唯一的身份標識,并且使用cookie封裝這個標識再發送給客戶端。那么當客戶端下次再訪問服務器時,就會自動攜帶這個身份標識了,這和SESSIONID的道理是一樣的,只是改由我們自己來實現了。另外,在返回令牌之前,我們需要將它存儲起來,以便于后續的驗證。而這個令牌是不能保存在服務器本地的,因為其他服務器無法訪問它。因此,我們可以將其存儲在服務器之外的一個地方,那么Redis便是一個理想的場所。
第二是驗證令牌的程序,就是在用戶再次訪問服務器時,我們獲取到了它之前的身份標識,那么我們就要驗證一下這個標識是否存在了。驗證的過程很簡單,我們從Redis中嘗試獲取一下就可以知道結果。
3、?如何利用Redis實現一個分布式鎖?
加鎖,通過“set...nx...”命令,將加鎖、過期命令編排到一起,它們是原子操作了,可以避免死鎖。
set key value nx ex seconds
解鎖,確保原子操作和自己只能解自己的鎖
# 解鎖 if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
4、LRU算法和LFU算法理解
LRU算法實現方式:
- 通常會使用雙向鏈表(Doubly Linked List)和哈希表(Hash Table)的結合來實現。哈希表用于快速查找數據項,而雙向鏈表則用于保持數據項的順序,即最近訪問的數據項靠近鏈表頭部,最久未訪問的數據項靠近鏈表尾部。
- 當訪問一個數據項時,如果它已經在鏈表中,則將其移動到鏈表頭部;如果它不在鏈表中,則添加到鏈表頭部,并可能需要在達到緩存容量上限時從鏈表尾部移除一個數據項。
LFU算法實現方式:
- LFU算法的實現比LRU更復雜,因為它需要跟蹤每個數據項的訪問頻率。
- 一種常見的實現方式是使用哈希表來存儲數據項及其對應的訪問頻率,同時使用雙向鏈表(或最小堆)來維護頻率的順序。每個鏈表節點代表一個頻率,而鏈表節點內部又使用哈希表或雙向鏈表來存儲具有相同訪問頻率的數據項。
- 當訪問一個數據項時,需要更新其訪問頻率,并根據新的頻率調整其在鏈表中的位置。如果達到緩存容量上限,則需要從具有最低訪問頻率的鏈表節點中移除一個數據項。
?5、MQ處理消息失敗了怎么辦?
一般生產環境中,都會在使用MQ的時候設計兩個隊列:一個是核心業務隊列,一個是死信隊列。核心業務隊列,就是比如專門用來讓訂單系統發送訂單消息的,然后另外一個死信隊列就是用來處理異常情況的。
比如說要是第三方物流系統故障了,此時無法請求,那么倉儲系統每次消費到一條訂單消息,嘗試通知發貨和配送,都會遇到對方的接口報錯。此時倉儲系統就可以把這條消息拒絕訪問,或者標志位處理失敗!注意,這個步驟很重要。
一旦標志這條消息處理失敗了之后,MQ就會把這條消息轉入提前設置好的一個死信隊列中。然后你會看到的就是,在第三方物流系統故障期間,所有訂單消息全部處理失敗,全部會轉入死信隊列。然后你的倉儲系統得專門有一個后臺線程,監控第三方物流系統是否正常,能否請求的,不停的監視。一旦發現對方恢復正常,這個后臺線程就從死信隊列消費出來處理失敗的訂單,重新執行發貨和配送的通知邏輯。死信隊列的使用,其實就是MQ在生產實踐中非常重要的一環,也就是架構設計必須要考慮的。
6、Kafka為什么速度快?
Kafka的消息是保存或緩存在磁盤上的,一般認為在磁盤上讀寫數據是會降低性能的,因為尋址會比較消耗時間,但是實際上,Kafka的特性之一就是高吞吐率。即使是普通的服務器,Kafka也可以輕松支持每秒百萬級的寫入請求,超過了大部分的消息中間件,這種特性也使得Kafka在日志處理等海量數據場景廣泛應用。
下面從數據寫入和讀取兩方面分析,為什么Kafka速度這么快:
寫入數據:
Kafka會把收到的消息都寫入到硬盤中,它絕對不會丟失數據。為了優化寫入速度Kafka采用了兩個技術,順序寫入和MMFile 。
一、順序寫入
磁盤讀寫的快慢取決于你怎么使用它,也就是順序讀寫或者隨機讀寫。在順序讀寫的情況下,磁盤的順序讀寫速度和內存持平。因為硬盤是機械結構,每次讀寫都會尋址->寫入,其中尋址是一個“機械動作”,它是最耗時的。所以硬盤最討厭隨機I/O,最喜歡順序I/O。為了提高讀寫硬盤的速度,Kafka就是使用順序I/O。
而且Linux對于磁盤的讀寫優化也比較多,包括read-ahead和write-behind,磁盤緩存等。如果在內存做這些操作的時候,一個是JAVA對象的內存開銷很大,另一個是隨著堆內存數據的增多,JAVA的GC時間會變得很長,使用磁盤操作有以下幾個好處:
-
磁盤順序讀寫速度超過內存隨機讀寫;
-
JVM的GC效率低,內存占用大。使用磁盤可以避免這一問題;
-
系統冷啟動后,磁盤緩存依然可用。
下圖就展示了Kafka是如何寫入數據的, 每一個Partition其實都是一個文件 ,收到消息后Kafka會把數據插入到文件末尾(虛框部分):
這種方法有一個缺陷——沒有辦法刪除數據 ,所以Kafka是不會刪除數據的,它會把所有的數據都保留下來,每個消費者(Consumer)對每個Topic都有一個offset用來表示讀取到了第幾條數據 。
二、Memory Mapped Files
即便是順序寫入硬盤,硬盤的訪問速度還是不可能追上內存。所以Kafka的數據并不是實時的寫入硬盤 ,它充分利用了現代操作系統分頁存儲來利用內存提高I/O效率。Memory Mapped Files(后面簡稱mmap)也被翻譯成 內存映射文件,在64位操作系統中一般可以表示20G的數據文件,它的工作原理是直接利用操作系統的Page來實現文件到物理內存的直接映射。完成映射之后你對物理內存的操作會被同步到硬盤上(操作系統在適當的時候)。
通過mmap,進程像讀寫硬盤一樣讀寫內存(當然是虛擬機內存),也不必關心內存的大小有虛擬內存為我們兜底。使用這種方式可以獲取很大的I/O提升,省去了用戶空間到內核空間復制的開銷(調用文件的read會把數據先放到內核空間的內存中,然后再復制到用戶空間的內存中。)
但也有一個很明顯的缺陷——不可靠,寫到mmap中的數據并沒有被真正的寫到硬盤,操作系統會在程序主動調用flush的時候才把數據真正的寫到硬盤。Kafka提供了一個參數——producer.type來控制是不是主動flush,如果Kafka寫入到mmap之后就立即flush然后再返回Producer叫 同步 (sync);寫入mmap之后立即返回Producer不調用flush叫異步 (async)。
讀取數據:
一、基于sendfile實現Zero Copy
傳統模式下,當需要對一個文件進行傳輸的時候,其具體流程細節如下:
-
調用read函數,文件數據被copy到內核緩沖區;
-
read函數返回,文件數據從內核緩沖區copy到用戶緩沖區;
-
write函數調用,將文件數據從用戶緩沖區copy到內核與socket相關的緩沖區;
-
數據從socket緩沖區copy到相關協議引擎。
以上細節是傳統read/write方式進行網絡文件傳輸的方式,我們可以看到,在這個過程當中,文件數據實際上是經過了四次copy操作:硬盤->內核buf->用戶buf->socket相關緩沖區->協議引擎。而sendfile系統調用則提供了一種減少以上多次copy,提升文件傳輸性能的方法。
在內核版本2.1中,引入了sendfile系統調用,以簡化網絡上和兩個本地文件之間的數據傳輸。sendfile的引入不僅減少了數據復制,還減少了上下文切換。運行流程如下:
-
sendfile系統調用,文件數據被copy至內核緩沖區;
-
再從內核緩沖區copy至內核中socket相關的緩沖區;
-
最后再socket相關的緩沖區copy到協議引擎。
相較傳統read/write方式,2.1版本內核引進的sendfile已經減少了內核緩沖區到user緩沖區,再由user緩沖區到socket相關緩沖區的文件copy,而在內核版本2.4之后,文件描述符結果被改變,sendfile實現了更簡單的方式,再次減少了一次copy操作。(即由內核緩沖區copy到內核中socket相關緩沖區的過程,使用偏移量替代,只是將引用傳遞給socket緩沖區,實際數據還是在內核緩沖區)
在Apache、Nginx、lighttpd等web服務器當中,都有一項sendfile相關的配置,使用sendfile可以大幅提升文件傳輸性能。Kafka把所有的消息都存放在一個一個的文件中,當消費者需要數據的時候Kafka直接把文件發送給消費者,配合mmap作為文件讀寫方式,直接把它傳給sendfile。
二、批量壓縮
在很多情況下,系統的瓶頸不是CPU或磁盤,而是網絡IO,對于需要在廣域網上的數據中心之間發送消息的數據流水線尤其如此。進行數據壓縮會消耗少量的CPU資源,不過對于kafka而言,網絡IO更應該需要考慮。
-
如果每個消息都壓縮,但是壓縮率相對很低,所以Kafka使用了批量壓縮,即將多個消息一起壓縮而不是單個消息壓縮;
-
Kafka允許使用遞歸的消息集合,批量的消息可以通過壓縮的形式傳輸并且在日志中也可以保持壓縮格式,直到被消費者解壓縮;
-
Kafka支持多種壓縮協議,包括Gzip和Snappy壓縮協議。
總結:
Kafka速度的秘訣在于,它把所有的消息都變成一個批量的文件,并且進行合理的批量壓縮,減少網絡IO損耗,通過mmap提高I/O速度,寫入數據的時候由于單個Partion是末尾添加所以速度最優。讀取數據的時候配合sendfile直接暴力輸出。
7、說說ElasticSearch put的全過程
put過程主要分為三個階段:
-
協調階段:
Client 客戶端選擇一個 node 發送 put 請求,此時當前節點就是協調節點(coordinating node)。協調節點根據 document 的 id 進行路由,將請求轉發給對應的 node。這個 node 上的是 primary shard 。
-
主要階段:
對應的 primary shard 處理請求,寫入數據 ,然后將數據同步到 replica shard。
-
primary shard 會驗證傳入的數據結構;
-
本地執行相關操作;
-
將操作轉發給 replica shard。
當數據寫入 primary shard 和 replica shard 成功后,路由節點返回響應給 Client。
-
-
副本階段:
每個 replica shard 在轉發后,會進行本地操作。
在寫操作時,默認情況下,只需要 primary shard 處于活躍狀態即可進行操作。在索引設置時可以設置這個屬性:index.write.wait_for_active_shards。默認是 1,即 primary shard 寫入成功即可返回。 如果設置為 all 則相當于 number_of_replicas+1 就是 primary shard 數量 + replica shard 數量。就是需要等待 primary shard 和 replica shard 都寫入成功才算成功。可以通過索引設置動態覆蓋此默認設置。
8、說說ElasticSearch的倒排索引
Elasticsearch 使用一種稱為倒排索引的結構,它適用于快速的全文搜索。一個倒排索引由文檔中所有不重復詞的列表構成,對于其中每個詞,有一個包含它的文檔列表。
例如,假設我們有兩個文檔,每個文檔的?content?域包含如下內容:
-
The quick brown fox jumped over the lazy dog
-
Quick brown foxes leap over lazy dogs in summer
為了創建倒排索引,我們首先將每個文檔的?content?域拆分成單獨的 詞(我們稱它為?詞條?或?tokens?),創建一個包含所有不重復詞條的排序列表,然后列出每個詞條出現在哪個文檔。結果如下所示:
現在,如果我們想搜索?quick brown?,我們只需要查找包含每個詞條的文檔:
兩個文檔都匹配,但是第一個文檔比第二個匹配度更高。如果我們使用僅計算匹配詞條數量的簡單相似性算法 ,那么,我們可以說,對于我們查詢的相關性來講,第一個文檔比第二個文檔更佳。
但是,我們目前的倒排索引有一些問題:
-
Quick?和?quick?以獨立的詞條出現,然而用戶可能認為它們是相同的詞。
-
fox?和?foxes?非常相似, 就像?dog?和?dogs?;他們有相同的詞根。
-
jumped?和?leap, 盡管沒有相同的詞根,但他們的意思很相近。他們是同義詞。
使用前面的索引搜索?+Quick +fox?不會得到任何匹配文檔。(記住,+?前綴表明這個詞必須存在。)只有同時出現?Quick?和?fox?的文檔才滿足這個查詢條件,但是第一個文檔包含?quick fox?,第二個文檔包含?Quick foxes?。
我們的用戶可以合理的期望兩個文檔與查詢匹配。我們可以做的更好。如果我們將詞條規范為標準模式,那么我們可以找到與用戶搜索的詞條不完全一致,但具有足夠相關性的文檔。例如:
-
Quick?可以小寫化為?quick?。
-
foxes?可以?詞干提取?--變為詞根的格式-- 為?fox?。類似的,?dogs?可以為提取為?dog?。
-
jumped?和?leap?是同義詞,可以索引為相同的單詞?jump?。
現在索引看上去像這樣:
這還遠遠不夠。我們搜索?+Quick +fox?仍然?會失敗,因為在我們的索引中,已經沒有?Quick?了。但是,如果我們對搜索的字符串使用與?content?域相同的標準化規則,會變成查詢?+quick +fox?,這樣兩個文檔都會匹配!