使用數據庫構建高性能隊列用于存儲訂單、通知和任務
引言
幾乎在每個地方都能用到隊列。在許多web站點里,比如其中的email和SMS都是使用隊列來異步發送通知。電子商務網站都是使用隊列來存儲訂單,處理訂單以及實現訂單的分發。工廠生產線的自動化系統也是使用隊列來按某種順序運行并發工作任務的。隊列是使用很廣泛的一種數據結構,它有時可以創建在數據庫里,而不是使用類似于MSMQ那樣的特定的隊列技術創建。使用數據庫技術來運行一個高性能且高可擴展性的隊列對我們來說是一個巨大的挑戰。當每天進入隊列和從隊列中提取的信息達到數百萬行的時候,這個隊列就很難維護了。我將向你展示在設計類似隊列表時常犯的設計錯誤以及如何使用簡單的數據庫功能實現隊列的最大性能和強大的可擴展性。
首先讓我們明確在隊列表中遇到的挑戰:
-
表的讀寫。在大負載下,入隊列和出隊列是相互影響而引起鎖的競爭、事務死鎖、IO超時等等。
</li> -
當多個接收者試圖從同一隊列讀數據時,它們隨機地獲取重復項,從而導致重復的處理過程。你需要在隊列上實現一些高性能的行鎖以至于并發接受器不會接收相同的數據項。
</li> -
隊列表需要以特定的順序去存儲行、以特定的順序讀取行數據,這是一個索引設計的挑戰。盡管并不總是先進先出。間或順序有較高的優先級,無論何時入棧都需要進行處理。
</li> -
隊列表需要以序列化的XML對象或者二進制形式存儲, 這帶來了存儲和索引重建的挑戰。由于數據表包含文本和/或二進制,所以你不能在隊列表上重建索引。因此隊列表每天變的越來越慢,最終查詢開始時間超時,最后你不得不關閉服務并重建索引。
</li> -
出隊列的過程中,一批行數據被選中、更新,然后重新處理。你需要一個"State"列定義數據項的狀體。出隊列時,你只選擇特定狀態的數據項。現在State只是一個包含PENDING、PROCESSING、PROCESSED、ARCHIVED等元素的集合。結果是你不能在"State"列上創建索引,原因是安全性差。隊列中可以有成千上萬行具有相同的狀態。因此 任何由掃面索引的出隊列操作都會導致CPU和IO資源緊張以及鎖競爭。
</li> -
在出隊列過程中,你不只是從表中移出行,原因是容易引起存儲殘片。進而,你需要重新訂單/任務/通知N次以防止他們在第一次嘗試中失敗。這意味著行數據需要更長的存儲周期、索引持續增長和出隊列越來越慢。
</li> -
你不得不歸檔以隊列表中處理過的數據項到不同的表或者數據庫,以保持主隊列表的精簡。這意味著需要移動大量的具有特殊狀態的行到另一個數據庫。大量的數據移動產生了的存儲碎片,而表的高頻度碎片整理降低入棧和出棧的性能。
</li> -
你需要24X7的工作。你不可能關閉服務而歸檔大量行數據。這意味者在不影響入棧和出棧,你不得不持續的歸檔行數據。
</li> </ol>如果您已實現這樣的隊列表,你可能已經遇到了以上挑戰中的一個或者更多。本文將給你一個關于如何克服這些挑戰的一些技巧,以及如何設計和維護一個高性能的隊列表。
SQL Server典型隊列
下面以常見的隊列類型為例,看看在并發負載時的情況.
CREATE TABLE [dbo].[QueueSlow]( [QueueID] [int] IDENTITY(1,1) NOT NULL, [QueueDateTime] [datetime] NOT NULL, [Title] [nvarchar](255) NOT NULL, [Status] [int] NOT NULL, [TextData] [nvarchar](max) NOT NULL ) ON [PRIMARY] GO CREATE UNIQUE CLUSTERED INDEX [PK_QueueSlow] ON [dbo].[QueueSlow] ( [QueueID] ASC ) GO CREATE NONCLUSTERED INDEX [IX_QuerySlow] ON [dbo].[QueueSlow] ( [QueueDateTime] ASC, [Status] ASC ) INCLUDE ( [Title]) GO
該隊列中使用QueueDateTime排序來模擬先進先出的隊列結構 . QueueDateTime不一定非得是對象加入隊列的時間,而是其將要被處理的開始時間 于是保證了先進先出的次序關系. TextData字段是為了保存進行負載測試的大字段設計的.
表中使用QueueDateTime 的非聚簇索引以加速隊列操作速度
首先,插入大約4萬行大約500兆數據量的記錄,其中每行的負載數大小各不相同.
set nocount on declare @counter int set @counter = 1 while @counter < @BatchSize begin insert into [QueueSlow] (QueueDateTime, Title, Status, TextData) select GETDATE(), 'Item no: ' + CONVERT(varchar(10), @counter), 0, REPLICATE('X', RAND() * 16000)set @counter = @counter + 1 end</pre>
接下來我們將一次性出隊10個元素。在出隊的時候,它會根據QueueDateTime 和Status = 0進行判斷,它將會Status更新為1表示該元素正在被處理。當出隊的時候我們并不會從緩存表中刪除這行數據,因為我們想保證這些元素在處理失敗的時候永遠不會丟失。
CREATE procedure [dbo].[DequeueSlow] AS
set nocount on
declare @BatchSize int set @BatchSize = 10
declare @Batch table (QueueID int, QueueDateTime datetime, _ Title nvarchar(255), TextData nvarchar(max) )
begin tran
insert into @Batch select Top (@BatchSize) QueueID, QueueDateTime, Title, TextData from QueueSlow WITH (UPDLOCK, HOLDLOCK) where Status = 0 order by QueueDateTime ASC
declare @ItemsToUpdate int set @ItemsToUpdate = @@ROWCOUNT
update QueueSlow SET Status = 1 WHERE QueueID IN (select QueueID from @Batch) AND Status = 0
if @@ROWCOUNT = @ItemsToUpdate begin commit tran select * from @Batch print 'SUCCESS' end else begin rollback tran print 'FAILED' end</pre>
上面的查詢將會從QueueSlow 表中取出10行數據,然后存儲在臨時表中。緊接著改變選中的記錄的狀態以保證其他的會話不會再次提取數據。如果能夠更新10行記錄并且沒有被其他的會話占有,那么久可以提交該事務,意味著將會將他們的狀態標記為已完成,沒有子過程進行調用。反之,其他的會話可以更新,事務將會拒絕被提交以保證事務的一致性
讓我測量下IO性能:
set statistics IO on exec dequeueslow
輸出如下:
Table '#3B75D760'. Scan count 0, logical reads 112, physical reads 0, read-ahead reads 0, lob logical reads 83, lob physical reads 0, lob read-ahead reads 0. Table 'QueueSlow'. Scan count 1, logical reads 651, physical reads 0, read-ahead reads 0, lob logical reads 166, lob physical reads 0, lob read-ahead reads 166. Table 'QueueSlow'. Scan count 0, logical reads 906, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. Table '#3B75D760'. Scan count 1, logical reads 11, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. Table '#3B75D760'. Scan count 1, logical reads 11, physical reads 0, read-ahead reads 0, lob logical reads 464, lob physical reads 0, lob read-ahead reads 0.概括如下:
-
Total Logical Read = 1695
</li> -
Total LOB Logical Read = 675
</li> -
LOB Logical Read Count = 3
</li> </ul>我們將用最快的方式進行標記看性能到底提升了多少。這里我們需要注意的是 LOB Logical Read和 LOB Logical Read 的訪問次數得到改善。讀三次數據是非常有必要的,因為我們需要重新載入數據。這很清楚的表明SQL Server沒有必要讀取大對象以滿足查詢。
經常產生出隊和入隊,使得許多的行被移除表進行歸檔,數據表逐漸變成碎片,你不能在線夠重建聚族索引來消除碎片引文它有varchar(max)字段。因此,你不得不選擇停止服務器重建索引,停止服務器是非常耗資源的
創建一個更快速的隊列
第一,你必須降低比較過的邏輯讀取。所以你只能將QueueSlow進行拆分為兩張表-QueueMeta ,QueueData。QueueData只包含參與where子句的字段。他是一張很小的表只用于保存查詢。使得SQL Server必須修正幾行數據占據8K的頁然后再運行, 這樣的表會比表QueueSlow運行的更快。
第二,你能夠在線重建QueueMeta 的索引,當其事務還在進行的時候。這樣的話,QueueMeta表的性能將被不會降低,你再也不用擔心停止服務器進行索引重建了。
CREATE TABLE [dbo].[QueueMeta]( [QueueID] [int] IDENTITY(1,1) NOT NULL, [QueueDateTime] [datetime] NOT NULL, [Title] [nvarchar](255) NOT NULL, [Status] [int] NOT NULL, CONSTRAINT [PK_Queue] PRIMARY KEY CLUSTERED ( [QueueID] ASC ) GO ALTER TABLE [dbo].[QueueMeta] ADD CONSTRAINT [PK_Queue] PRIMARY KEY CLUSTERED ( [QueueID] ASC ) GO CREATE NONCLUSTERED INDEX [IX_QueueDateTime] ON [dbo].[QueueMeta] ( [QueueDateTime] ASC, [Status] ASC ) INCLUDE ( [Title])
這個表保留了出現在搜索查詢中的所有的字段。其他所有和有效負載相關的字段被移到了QueueData表。
CREATE TABLE [dbo].QueueData NOT NULL ) ON [PRIMARY]
GO CREATE UNIQUE NONCLUSTERED INDEX [IX_QueueData] ON [dbo].[QueueData] ( [QueueID] ASC ) GO</pre>
在這個表中沒有聚類索引項。“Dequeue”過程先被簡單的修改后去執行QueueMeta表上的查詢操作,然后從QueueData表中選擇有效負載。
CREATE procedure [dbo].[Dequeue] AS
set nocount on
declare @BatchSize int set @BatchSize = 10
declare @Batch table (QueueID int, QueueDateTime datetime, Title nvarchar(255))
begin tran
insert into @Batch select Top (@BatchSize) QueueID, QueueDateTime, Title from QueueMeta WITH (UPDLOCK, HOLDLOCK) where Status = 0 order by QueueDateTime ASC
declare @ItemsToUpdate int set @ItemsToUpdate = @@ROWCOUNT
update QueueMeta SET Status = 1 WHERE QueueID IN (select QueueID from @Batch) AND Status = 0
if @@ROWCOUNT = @ItemsToUpdate begin commit tran select b.*, q.TextData from @Batch b inner join QueueData q on q.QueueID = b.QueueID print 'SUCCESS' end else begin rollback tran print 'FAILED' end</pre>
當把在QueueSlow提取的相同數據填充到QueueMeta和QueueData表中,然后重建兩個表的索引并作對比,分析發現有明顯的提高:
-
Total Logical Read = 1546 (vs 1695)
</li> -
Total LOB Read = 380 (vs 675)
</li> -
LOB Read Count = 1 (vs 3)
</li> </ul>你會看到邏輯讀的次數低于149;LOB讀取低于295;LOB讀取計數為1,以上數據正式我們期待的。
負載下的性能比較
當我模擬并發隊列和出隊列,并且測量性能計數器,結果如下所示:
緩慢隊列 快速隊列 </tr> </tbody>
</tr> </tbody> </table>
讓我們來分析這些重要的計數器,看看有哪些改進:
-
頁面分割/秒 – 更快的解決方案有比較低的頁面分割,和較慢的解決方案相比幾乎沒有。這是因為在插入的過程中,有時候一行無法放下而部分填充,因此需要分割成新的一頁。你可以沖這里了解更多有關頁面分割的信息。
</li> -
事務/秒 – 我們從金錢中獲得更多價值。每秒越多的事務,就會有越多的隊列操作隨著出隊列發生,它顯示出了快速隊列操作的性能相比于慢的是更好的選項。
</li> -
鎖超時/秒 - 這說明有多少個查詢在等待某個對象上的鎖,并最終放棄了,因為它沒有及時得到該鎖。該值越高,表示數據庫讀取性能越差。你要盡量保持該值接近零。上述結果表明鎖定超時在快速隊列的數目小于緩慢隊列。
</li> -
批量請求/秒 - 顯示每秒執行SELECT查詢多少次。它顯示了這兩種解決方案都執行的相同次數的SELECT操作,由于更快的解決方案是從多個表讀取的。所以,與較慢的解解方案相比,出隊列的存儲過程沒有顯著優化。
</li> </ul>這不僅僅是有更好的性能,最大的好處是你可以在QueueMeta表在線運行INDEX DEFRAG,從而阻止Queue逐漸放緩。
在SQL Server 2005,2008里實現的最快隊列
SQL Server 2005給
UPDATE、INSERT和DELETE語句引入了OUTPUT子句。這么做就可以讓你通過一個查詢就可以得到由insert,update和delete更改的哪些數據行。你不需要先查詢一些數據行,然后對它們進行加鎖,接著再更新這些數據行,最后才返回這些數據行。你只需一條語句就可以完成這些功能-更新并返回這些數據行。下面從隊列獲取信息的過程是修改過的:
alter procedure [dbo].[DequeueFast] AS
set nocount on
declare @BatchSize int set @BatchSize = 100
update top(@BatchSize) QueueMeta WITH (UPDLOCK, READPAST) SET Status = 1 OUTPUT inserted.QueueID, inserted.QueueDateTime, inserted.Title, qd.TextData FROM QueueMeta qm INNER JOIN QueueData qd ON qm.QueueID = qd.QueueID WHERE Status = 0</pre>
一行
UPDATE語句就可以做到我們到現在為止看到的Dequeue存儲過程所做的一切。同時IO統計狀態也得到了很大的改善:Table 'QueueMeta'. Scan count 1, logical reads 522, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. Table 'QueueData'. Scan count 0, logical reads 31, physical reads 0, read-ahead reads 0, lob logical reads 56, lob physical reads 0, lob read-ahead reads 0.
-
總的邏輯讀取數=553
</li> -
LOB邏輯讀取數=56
</li> </ul>與較快隊列的解決方案相比,在IO統計方面,這種方案至少要快3倍。
這兒,我使用了特殊的加鎖策略 -READPAST。這種策略是這樣運行的:如果查詢發現一些行已經加了鎖,那么它不會等待這些行解鎖。它會立刻忽略這些數據行。在這兒,由于我們沒有先SELECT,然后再UPDATE,所以就不需要使用HOLDLOCK了。這也是獲得更佳性能的原因之一。隊列表的歸檔策略
當你向隊列表中插入記錄時,隊列表的大小會一直在增長。你需要做的是要確保隊列表保持合理的大小,這樣就永遠不會對此隊列表進行備份和重建索引了。有兩種方法可以進行隊列消息的數據行歸檔-晝夜不停地進行小批量歸檔或者在非工作高峰時進行大批量歸檔。如果你運行的是24x7服務的系統,而且也沒有非工作高峰時段,那么你就需要接連不斷地運行小批量歸檔,這樣的小批量歸檔之間稍有時延。不過,在從隊列提取消息的時候,你不能刪除隊列表中的消息數據,因為刪除操作是費時費力的操作。如果你刪除隊列中的數據,那么從隊列提取消息的過程將會慢很多。不過,你可以通過另一個后臺任務來刪除隊列里已經處理過的消息數據,這么做就不會降低從隊列提取消息的性能。 此外,如果要求實現的是一個可靠性很高的隊列,那么你就不能在從隊列中提取消息的時候刪除消息數據。如果處理隊列消息數據的進程由于某種原因而失效,而且重新運行消息數據插入后仍然不能把這些消息數據插入到隊列表里,那么這些消息數據就會永遠丟失了。有時候,你需要密切關注消息隊列,確保已經被提取的消息在某個時間段內得到處理。如果沒有得到處理的話,那么就需要把這些消息放在隊列的最前端,這樣才能保證處理進程能提取到這些消息。正是由于以上這些原因,最好在從隊列提取消息的時候保持這些消息數據不變,只是更改這些消息數據行的狀態。
結論
你的訂單處理系統、任務執行系統或通知系統的性能和可靠性,取決于你如何設計你的隊列。由于這些直接影響客戶滿意度并最終直戳你的底線,所以當你要建立你的隊列時,花足夠的時間做出正確的設計決策是很重要的。否則,隨著時間的推移,它會成為一個債務,在你付出了失去業務和很高的資源消耗的代價之后最終仍然不得不重新設計隊列。
本文地址:http://www.oschina.net/translate/building-high-performance-queue-in-database-for-storing-orders
原文地址:http://www.codeproject.com/Articles/110931/Building-High-Performance-Queue-in-Database-for-st
本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!相關資訊
sesese色
-
-
-
-
