我有一些代码可以将报废的数据写入SQL服务器数据库。数据项包括一些基本的酒店数据(名称、地址、评级等)和一些带有相关数据的房间列表(价格、入住率等)。可以有多个芹菜线程和多个服务器运行此代码,同时向数据库写入不同的项目。我遇到死锁错误,如:
[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(...)
,那么我就不再看到死锁错误。
然而,我真的不明白为什么这会有什么不同,而且我也不完全确定在一个pyodbccurs.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.
外壳内的实验表明,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()
也会失败,并出现"Previous SQL was not a query."错误,所以pyodbc execute似乎确实执行了所有。。。不仅仅是第一句话。
如果我可以相信这一点,那么我的主要问题是如何获得一些日志输出。
EDIT在dbargs中设置autocommit=True
似乎可以防止死锁错误,即使有多个curs.executes也是如此。但为什么要修复它呢?
在dbargs中设置
autocommit=True
似乎可以防止死锁错误,即使有多个curs.executes也是如此。但为什么要修复它呢?
建立连接时,pyodbc根据Python DB-API规范默认为autocommit=False
。因此,当执行第一条SQL语句时,ODBC将开始一个数据库事务,该事务将一直有效,直到Python代码在连接上执行.commit()
或.rollback()
。
SQL Server中的默认事务隔离级别为"已提交读取"。除非默认情况下将数据库配置为支持SNAPSHOT隔离,否则处于"读取-提交"隔离下的事务中的写入操作将在更新的行上放置事务范围的锁。在高并发条件下,如果多个进程生成冲突的锁,就会发生死锁。如果这些进程使用生成大量此类锁的长寿命事务,那么死锁的可能性更大。
设置autocommit=True
将避免死锁,因为每个单独的SQL语句都将自动提交,从而结束事务(在该语句开始执行时自动启动)并释放更新行上的任何锁。
因此,为了避免死锁,您可以考虑几种不同的策略:
- 继续使用
autocommit=True
,或 - 让Python代码更频繁地显式
.commit()
,或者 - 使用
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
来"放松"事务隔离级别,避免由写操作创建的持久锁,或者 - 将数据库配置为使用SNAPSHOT隔离,这将避免锁争用,但会使SQL Server更难工作
您需要做一些功课来确定针对特定用例的最佳策略。