在SQL Server中对表执行并发脚本

3 投票
2 回答
1093 浏览
提问于 2025-04-17 22:34

我有一些相同的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

这个脚本的工作流程如下:

  1. 抓取所有类型为X的记录(在一个事务中),条件是DirtyBitUnUsed,并且OwnerNULL

  2. 更新所有记录,把DirtyBit设置为InUse,把Owner设置为Server1

  3. 在Python中对这些数据进行一些操作。

  4. 根据第三步的操作更新所有记录。把DirtyBit重新设置为UnUsed,把Owner重新设置为NULL

因为这个脚本在多个服务器上运行,所以DirtyBitOwner的组合可以确保这些脚本不会互相干扰。另外,每一行记录都是独立的,互不影响。

问题:这样让脚本并发运行的方式合理吗?数据库有没有办法帮我处理这个问题(比如改变事务隔离级别)?理想情况下,如果脚本同时运行,应该是这样的:

  1. 服务器1上的脚本开始运行。

  2. 服务器2上的脚本开始运行,发现服务器1的脚本正在运行,因此决定不再运行。

  3. 服务器1上的脚本完成,更新所有数据。

2 个回答

1

我不太建议你用你这里的方法。自己做的解决方案通常不太稳定。

这个问题看起来适合用一个定时任务来处理,同时可以通过sp_getapplock来控制并发。

2

开发基于数据同时访问和修改的解决方案总是很有意义的。不过,这种方式也容易出现一些很少发生但又难以发现的错误。

在你的情况下,你实际上想要的是对你的表进行访问的序列化,而不仅仅是更新。也就是说,只允许一个线程(事务)获取它需要的数据(在这里DirtyBitUnUsed,而OwnerNULL),并将这些行标记为“已使用”。我很确定你现在的解决方案并不能正常工作。为什么呢?想象一下这样的场景:

  1. 事务1开始
  2. 事务2开始
  3. 事务1从表中读取数据
  4. 事务2也从表中读取数据 - 这是允许的,因为它是在共享锁模式下。它读取的数据和事务1是一样的
  5. 事务1更新了表
  6. 事务2想要更新表,但被事务1阻塞 - 因为事务1正在进行中
  7. 事务1提交
  8. 现在事务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)会导致其他事务等待,直到这个事务提交或回滚 - 它们将无法读取数据。这能解决你的问题吗?

但是……如果在这个特定情况下你能避免并发操作,我建议你这么做(因为我在回答的第一段提到过)。

撰写回答