@Myles Gray - 你的解决方案有一些问题。
首先是小问题:
1) 在队列循环的每次迭代之后,您重新创建队列作为原始队列减去您当前正在处理的行(您希望!稍后会详细介绍)。重新创建队列后,将其附加到日志中。这会起作用,但它似乎非常低效,并且有可能使日志变得庞大而笨重。假设您有一个包含 10,000 行的队列。到您处理队列时,您将在日志中写入 99,989,998 个队列行,其中包括 49,994,999 个队列行!即使没有实际工作,这也需要很长时间来处理。
2) 您使用 FINDSTR 重新创建队列,保留所有与您当前 ID 不匹配的行。但是,如果它们碰巧与您当前的 ID 匹配,这也会删除后续行。那可能不是问题。但是你正在做一个子字符串匹配。您的 FINDSTR 还将消除在其中任何位置包含您当前 ID 的后续行。我不知道你的身份证是什么样子的。但是,如果您当前的 ID 是 123,那么以下所有 ID 都将被错误地剥离 - 31236、12365 等。这是一个潜在的毁灭性问题。我说这是可能的,因为 FOR 循环已经缓冲了队列,所以它不在乎 - 除非你中止循环,因为新的工作已附加到 late.txt 文件 - 那么你实际上会跳过那些丢失的 ID!这可以通过将 /X 选项添加到 FINDSTR 来解决。至少那样你只会跳过真正的重复。
现在的主要问题 - 所有这些都源于只有一个进程可以为任何类型的写入(或删除)操作打开文件。
3) 即使 FOR /F 循环不会写入文件,但如果文件正被另一个进程主动写入,它也会失败。因此,如果您的 FOR 循环尝试在另一个进程附加到队列时读取队列,您的队列处理脚本将失败。您已经检查了busy.txt 文件,但您的队列编写器可能在busy.txt 文件创建之前就已经开始写入。写入操作可能需要一段时间,尤其是在附加许多行的情况下。在写入行时,您的队列处理器可能会启动,然后您会遇到冲突和失败。
4) 您的队列处理器将late.txt 附加到您的队列中,然后删除late.txt。但是在追加和删除之间有一个时间点,队列编写器可以在late.txt 中追加一行。这条迟到的行将被删除而不被处理!
5) 另一种可能性是写入者可能会在队列处理器正在删除它的过程中尝试写入late.txt。写入将失败,您的队列将再次丢失工作。
6) 另一种可能性是您的队列可能会在队列写入器附加到它时尝试删除late.txt。删除将失败,并且下次队列处理器将 late.txt 附加到 queue.txt 时,您的队列中将出现重复项。
总之,并发问题可能会导致队列中的工作丢失,以及队列中的重复工作。每当您有多个进程同时对文件进行更改时,您必须建立某种锁定机制来序列化事件。
您已经在使用 SqlServer 数据库。最合乎逻辑的做法是将队列移出文件系统并移入数据库。关系数据库是从头开始构建的以处理并发性。
话虽如此,只要您采用锁定策略,在 Windows 批处理中将文件用作队列并不难。您必须确保队列处理器和队列编写器都遵循相同的锁定策略。
以下是基于文件的解决方案。我将假设您只有一个队列处理器,并且可能有多个队列编写器。通过额外的工作,您可以调整文件队列解决方案以支持多个队列处理器。但是使用我在my first answer 末尾描述的基于文件夹的队列可能更容易实现多个队列处理器。
与让队列编写器写入 queue.txt 或 late.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 以找出当前拥有(或最近拥有)锁的进程。只有在某些进程挂起时,这才是一个问题。