【问题标题】:SQL server, pyodbc and deadlock errorsSQL 服务器、pyodbc 和死锁错误
【发布时间】:2017-01-27 18:47:49
【问题描述】:

我有一些代码可以将 Scrapy 抓取的数据写入 SQL 服务器数据库。数据项包括一些基本的酒店数据(名称、地址、评级..)和一些带有相关数据(价格、入住率等)的房间列表。可以有多个 celery 线程和多个服务器运行此代码并同时写入 db 不同的项目。我遇到了像这样的死锁错误:

[Failure instance: Traceback: <class 'pyodbc.ProgrammingError'>: 
('42000', '[42000] [FreeTDS][SQL Server]Transaction (Process ID 62)
 was deadlocked on lock resources with another process and has been 
chosen as the deadlock victim. Rerun the transaction. (1205) (SQLParamData)')

实际执行插入/更新的代码大致如下所示:

1) Check if hotel exists in hotels table, if it does update it, else insert it new. 
   Get the hotel id either way. This is done by `curs.execute(...)`

2) Python loop over the hotel rooms scraped. For each room check if room exists 
   in the rooms table (which is foreign keyed to the hotels table). 
   If not, then insert it using the hotel id to reference the hotels table row. 
   Else update it.  These upserts are done using `curs.execute(...)`.

实际上比这要复杂一些,但这说明 Python 代码在循环之前和循环期间使用了多个 curs.executes

如果不是以上述方式更新数据,我生成一个大 SQL 命令,它执行相同的操作(检查酒店,更新它,将 id 记录到一个临时变量,为每个房间检查是否存在并更新针对酒店 id var 等),然后在 python 代码中只执行一个 curs.execute(...),然后我不再看到死锁错误。

但是,我真的不明白为什么这会有所不同,而且我也不完全确定在单个 pyodbc curs.execute 中运行具有多个 SELECTS、INSERTS、UPDATES 的大型 SQL 块是否安全。据我了解,pyodbc 假设只处理单个语句,但它似乎确实有效,而且我看到我的表填充没有死锁错误。

尽管如此,如果我执行这样的大命令,似乎不可能得到任何输出。我尝试在最终SELECT @output_string as outputstring 之前声明一个变量@output_string 并记录各种内容(例如,我们是否必须插入或更新酒店),但是在pyodbc 中执行后执行获取总是失败

<class 'pyodbc.ProgrammingError'>: No results.  Previous SQL was not a query.

shell 中的实验表明 pyodbc 会忽略第一条语句之后的所有内容:

In [11]: curs.execute("SELECT 'HELLO'; SELECT 'BYE';")
Out[11]: <pyodbc.Cursor at 0x7fc52c044a50>

In [12]: curs.fetchall()
Out[12]: [('HELLO', )]

因此,如果第一条语句不是查询,则会出现该错误:

In [13]: curs.execute("PRINT 'HELLO'; SELECT 'BYE';")
Out[13]: <pyodbc.Cursor at 0x7fc52c044a50>

In [14]: curs.fetchall()
---------------------------------------------------------------------------
ProgrammingError                          Traceback (most recent call last)
<ipython-input-14-ad813e4432e9> in <module>()
----> 1 curs.fetchall()

ProgrammingError: No results.  Previous SQL was not a query.

尽管如此,除了无法获取我的@output_string,我真正的“大查询”,包括多个选择、更新、插入,实际上可以工作并填充数据库中的多个表。

不过,如果我尝试类似的方法

curs.execute('INSERT INTO testX (entid, thecol) VALUES (4, 5); INSERT INTO testX (entid, thecol) VALUES (5, 6); SELECT * FROM testX; '
...: )

我看到这两行都插入到表 tableX 中,即使是后续的 curs.fetchall() 也会失败,并显示“以前的 SQL 不是查询”。错误,所以似乎 pyodbc execute 确实执行了所有内容……而不仅仅是第一条语句。

如果我可以相信这一点,那么我的主要问题是如何获取一些日志输出。

EDIT 在 dbargs 中设置 autocommit=True 似乎可以防止死锁错误,即使使用多个 curs.executes 也是如此。但是为什么这会解决它呢?

【问题讨论】:

  • 您的“大 SQL 命令”(可能包含多个语句)是否以 SET NOCOUNT ON; 开头?
  • @GordThompson 不,它以“BEGIN”开始并以“END;”结束,其中有多个其他块,“BEGIN,END;”,“IF/ELSE”,“SELECTS”, “插入”、“更新”。

标签: sql-server parallel-processing scrapy celery pyodbc


【解决方案1】:

在 dbargs 中设置 autocommit=True 似乎可以防止死锁错误,即使使用多个 curs.executes 也是如此。但是为什么这会解决它呢?

建立连接时,pyodbc 根据 Python DB-API 规范默认为autocommit=False。因此,当执行第一条 SQL 语句时,ODBC 开始一个数据库事务,该事务一直有效,直到 Python 代码在连接上执行 .commit().rollback()

SQL Server 中的默认事务隔离级别是“已提交读”。除非默认情况下将数据库配置为支持 SNAPSHOT 隔离,否则在 Read Committed 隔离下的事务中的写操作将在已更新的行上放置事务范围的锁。在高并发的情况下,如果多个进程产生冲突的锁,就会发生死锁。如果这些进程使用生成大量此类锁的长期事务,那么死锁的可能性就更大。

设置autocommit=True 将避免死锁,因为每个单独的 SQL 语句都将自动提交,从而结束事务(在该语句开始执行时自动启动)并释放更新行上的任何锁。

因此,为了帮助避免死锁,您可以考虑几种不同的策略:

  • 继续使用autocommit=True,或
  • 更频繁地使用您的 Python 代码.commit(),或者
  • 使用SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED“放松”事务隔离级别并避免写操作创建的持久锁,或者
  • 将数据库配置为使用 SNAPSHOT 隔离,这将避免锁争用,但会使 SQL Server 更努力地工作。

您需要做一些功课来确定适合您特定使用案例的最佳策略。

【讨论】:

    猜你喜欢
    • 2013-02-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多