@Myles Gray - 您的解決方案有一些問題。
首先是小問題:
1)隊列循環的每次迭代後,重新創建隊列作爲原始隊列減去行您目前正在使用的(你希望稍後更多)!重新創建隊列後,將其附加到日誌中。這將起作用,但它看起來效率很低,並有可能使日誌變得龐大而不穩定。假設你有一個擁有10,000行的隊列。在處理完隊列之後,您將寫入99,989,998個隊列行,其中包括49,994,999個隊列行。即使沒有真正做好你的工作,這也需要很長時間才能完成。
2)使用FINDSTR重新創建隊列,保留所有與當前ID不匹配的行。但是如果它們碰巧與你當前的ID匹配,這也會剝離後續行。這可能不成問題。但是你正在做一個子串匹配。您的FINDSTR也將消除後續行中包含您當前的ID在其中的任何地方。我不知道你的ID是什麼樣子。但是,如果您當前的ID是123,那麼所有以下ID將被錯誤地刪除 - 31236,12365等。這是一個潛在的不利問題。我說這是潛在的,因爲FOR循環已經緩衝了隊列,所以它不會在意 - 除非因爲新工作被附加到late.txt文件而中止循環 - 那麼你實際上會跳過那些缺少的ID!這可以通過向FINDSTR添加/ X選項來解決。至少你只會跳過真正的重複。
現在的主要問題 - 全部源於事實,只有一個進程可以爲任何類型的寫入(或刪除)操作打開文件。 3)即使FOR/F循環沒有寫入文件,它的設計也會失敗,如果文件正在被另一個進程寫入。因此,如果您的FOR循環試圖在另一個進程附加到隊列中時讀取隊列,那麼您的隊列處理腳本將失敗。您有busy.txt文件檢查,但您的隊列編寫器可能已經在busy.txt文件創建之前開始寫入。寫入操作可能需要一段時間,特別是在追加許多行時。在寫入隊列時,您的隊列處理器可能會啓動,然後發生碰撞和失敗。
4)您的隊列處理器會將late.txt附加到您的隊列中,然後刪除late.txt。但是在append和delete之間有一個時間點,隊列編寫者可以在late.txt中附加一行。這條遲到的線路將被刪除而未經處理!
5)另一種可能性是寫入者可能試圖在隊列處理器正在刪除的過程中寫入late.txt。寫入將失敗,並且您的隊列將再次失去工作。
6)另一種可能性是隊列可能嘗試刪除late.txt,而隊列作者正在追加它。刪除將失敗,並且在下一次隊列處理器將late.txt附加到queue.txt時,最終將在隊列中出現重複項。
總之,併發問題可能會導致您的隊列中缺少工作以及隊列中的重複工作。只要你有多個進程同時修改一個文件,你必須建立某種鎖定機制來序列化這些事件。
您已經在使用SqlServer數據庫。最合理的做法是將您的隊列移出文件系統並移入數據庫。關係數據庫是從頭開始構建的,以處理併發性。
也就是說,只要你使用鎖定策略,在Windows批處理中使用文件作爲隊列並不困難。您必須確保您的隊列處理器和隊列編寫器遵循相同的鎖定策略。
下面是一個基於文件的解決方案。我假設你只有一個隊列處理器,可能還有多個隊列編寫器。通過額外的工作,您可以調整文件隊列解決方案以支持多個隊列處理器。但是多個隊列處理器可能更容易使用我在my first answer末尾描述的基於文件夾的隊列實現。
而不是讓隊列寫入者寫入queue.txt或遲到。txt,使隊列處理器重新命名現有隊列並將其處理完成變得更容易,而隊列編寫器總是寫入queue.txt。
該解決方案將當前狀態寫入status.txt文件。您可以通過從命令窗口發出TYPE STATUS.TXT
來監控您的隊列處理器狀態。
我做了一些延遲擴展切換,以防止因數據中的!
造成的損壞。如果您知道!
永遠不會出現,那麼您可以簡單地將SETLOCAL EnableDelayedExpansion移到頂部並放棄切換。
另一個優化 - 爲一組語句重新定向輸出只是一次,而不是爲每個語句打開和關閉文件的速度更快。
此代碼完全未經測試,因此可能容易出現一些愚蠢的錯誤。但概念是健全的。希望你明白這個主意。
queueProcessor.bat
@echo off
setlocal disableDelayedExpansion
cd "%UserProfile%\Desktop\Scripting\"
:rerun
::Safely get a copy of the current queue, exit if none or error
call :getQueue || exit /b
::Get the number of lines in the queue to be used in status updates
for /f %%n in ('find /v "" ^<inProcess.txt') do set /a "record=0, recordCount=%%n"
::Main processing loop
for /f "delims=" %%a in (inProcess.txt) do (
rem :: Update the status. Need delayed expansion to access the current record number.
rem :: Need to toggle delayed expansion in case your data contains !
setlocal enableDelayedExpansion
set /a "record+=1"
> status.txt echo processing !record! out of %recordCount%
endlocal
rem :: Create SQL command
> reset.sql (
echo USE dbname
echo EXEC dbo.sp_ResetSubscription @ClientName = '%%a'
echo EXEC dbo.sp_RunClientSnapshot @ClientName = '%%a'
)
rem :: Log this action and execute the SQL command
>> log.txt (
echo #################### %date% - %time% ####################################################
echo Reinitialising '%%a'
sqlcmd -i "reset.sql"
echo.
echo ####################################################################################################
echo.
)
)
::Clean up
delete inProcess.txt
delete status.txt
::Look for more work
goto :rerun
:getQueue
2>nul (
>queue.lock (
if not exist queue.txt exit /b 1
if exist inProcess.txt (
echo ERROR: Only one queue processor allowed at a time
exit /b 2
)
rename queue.txt inProcess.txt
)
)||goto :getQueue
exit /b 0
queueWriter.bat
::Whatever your code is
::At some point you want to append a VALUE to the queue in a safe way
call :appendQueue VALUE
::continue on until done
exit /b
:appendQueue
2>nul (
>queue.lock (
>>queue.txt echo %*
)
)||goto :appendQueue
鎖碼的說明:
:retry
::First redirect any error messages that occur within the outer block to nul
2>nul (
rem ::Next redirect all stdout within the inner block to queue.lock
rem ::No output will actually go there. But the file will be created
rem ::and this process will have a lock on the file until the inner
rem ::block completes. Any other process that tries to write to this
rem ::file will fail. If a different process already has queue.lock
rem ::locked, then this process will fail to get the lock and the inner
rem ::block will not execute. Any error message will go to nul.
>queue.lock (
rem ::you can now safely manipulate your queue because you have an
rem ::exclusive lock.
>>queue.txt echo data
rem ::If some command within the inner block can fail, then you must
rem ::clear the error at the end of the inner block. Otherwise this
rem ::routine can get stuck in an endless loop. You might want to
rem ::add this to my code - it clears any error.
verify >nul
) && (
rem ::I've never done this before, but if the inner block succeeded,
rem ::then I think you can attempt to delete queue.lock at this point.
rem ::If the del succeeds then you know that no process has a lock
rem ::at this point. This could be useful if you are trying to monitor
rem ::the processes. If the del fails then that means some other process
rem ::has already grabbed the lock. You need to clear the error at
rem ::this point to prevent the endless loop
del queue.lock || verify >nul
)
) || goto :retry
:: If the inner block failed to get the lock, then the conditional GOTO
:: activates and it loops back to try again. It continues to loop until
:: the lock succeeds. Note - the :retry label must be above the outer-
:: most block.
如果你有一個獨特的進程ID,你可以把它寫內部塊中的queue.lock。然後,您可以從另一個窗口中鍵入queue.lock,以確定哪個進程當前擁有(或最近有)該鎖。如果某個進程掛起,那應該只是一個問題。
哇,我可以說,出色的工作,你應得到這樣的獎勵,這是一個令人難以置信的工作量,我將通過這個工作,看看事情如何使用你的解決方案,但我的上帝,再次出色的工作!爲你明天+100! –
@MylesGray - 酷。這很好,值得讚賞,而賞金是一個意想不到的甜蜜獎金 - 假設我的答案得到的票數最多:)代碼並不難,因爲我在20世紀80年代做過一些基於文件系統的隊列工作。但解釋確實需要時間。 *注意* - 自第一次發佈以來,我在queueWriter(條件轉移)的最後添加了一個快速錯誤修復程序。 – dbenham
我可以儘早結束並無論如何都將獎勵給您 - 這就是我所期待的(需要24小時才能做到)。對於基於文件夾的解決方案,我認爲我只是交換文件夾中的inProcess.txt,queue.txt等? 你能解釋一下'2> nul(> queue.lock'的工作原理嗎? –