在SQL Server中对表执行并发脚本
我有一些相同的Python脚本需要在多个服务器上运行,这些脚本都要访问同一个数据库表。每个脚本运行的时间在5到20秒之间,而且必须每5分钟运行一次。
Server1 ---> -------------
| DB Table |
Server2 ---> -------------
这个脚本会查看一个长得像这样的表:
Type | many other fields | DirtyBit | Owner
--------------------------------------------
X | ... | UnUsed | NULL
X | ... | UnUsed | NULL
X | ... | UnUsed | NULL
Y | ... | UnUsed | NULL
Y | ... | UnUsed | NULL
这个脚本的工作流程如下:
抓取所有类型为X的记录(在一个事务中),条件是
DirtyBit
为UnUsed
,并且Owner
为NULL
。更新所有记录,把
DirtyBit
设置为InUse
,把Owner
设置为Server1
。在Python中对这些数据进行一些操作。
根据第三步的操作更新所有记录。把
DirtyBit
重新设置为UnUsed
,把Owner
重新设置为NULL
。
因为这个脚本在多个服务器上运行,所以DirtyBit
和Owner
的组合可以确保这些脚本不会互相干扰。另外,每一行记录都是独立的,互不影响。
问题:这样让脚本并发运行的方式合理吗?数据库有没有办法帮我处理这个问题(比如改变事务隔离级别)?理想情况下,如果脚本同时运行,应该是这样的:
服务器1上的脚本开始运行。
服务器2上的脚本开始运行,发现服务器1的脚本正在运行,因此决定不再运行。
服务器1上的脚本完成,更新所有数据。
2 个回答
我不太建议你用你这里的方法。自己做的解决方案通常不太稳定。
这个问题看起来适合用一个定时任务来处理,同时可以通过sp_getapplock来控制并发。
开发基于数据同时访问和修改的解决方案总是很有意义的。不过,这种方式也容易出现一些很少发生但又难以发现的错误。
在你的情况下,你实际上想要的是对你的表进行访问的序列化,而不仅仅是更新。也就是说,只允许一个线程(事务)获取它需要的数据(在这里DirtyBit
是UnUsed
,而Owner
是NULL
),并将这些行标记为“已使用”。我很确定你现在的解决方案并不能正常工作。为什么呢?想象一下这样的场景:
- 事务1开始
- 事务2开始
- 事务1从表中读取数据
- 事务2也从表中读取数据 - 这是允许的,因为它是在共享锁模式下。它读取的数据和事务1是一样的
- 事务1更新了表
- 事务2想要更新表,但被事务1阻塞 - 因为事务1正在进行中
- 事务1提交
- 现在事务2可以更新数据并提交
结果是,事务1和事务2都读取了相同的行,而你在两个服务器上的脚本都会对这些行进行操作。你可以通过手动操作数据库轻松重现这样的场景。
你可以通过显式地获取独占表锁来避免这种情况。这样做的代码看起来是这样的:
begin transaction;
select * from test where DirtyBit = 'UnUsed' and Owner is null (TABLOCKX);
update test set DirtyBit = 'Used', Owner = 'Server1' where id in (...);
commit;
在这里,(TABLOCKX)
会导致其他事务等待,直到这个事务提交或回滚 - 它们将无法读取数据。这能解决你的问题吗?
但是……如果在这个特定情况下你能避免并发操作,我建议你这么做(因为我在回答的第一段提到过)。