fpg*_*ost 7 sql-server parallel-processing pyodbc scrapy celery
我有一些代码可以将 Scrapy 抓取的数据写入 SQL 服务器数据库。数据项包括一些基本的酒店数据(名称、地址、评级...)和一些带有相关数据(价格、入住率等)的房间列表。可以有多个 celery 线程和多个服务器运行此代码并同时写入数据库不同的项目。我遇到死锁错误,例如:
[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)')
Run Code Online (Sandbox Code Playgroud)
实际执行插入/更新的代码示意如下:
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(...)`.
Run Code Online (Sandbox Code Playgroud)
在实践中它比这更复杂一些,但这说明 Python 代码curs.executes在循环之前和循环期间使用了多个。
如果不是以上述方式更新数据,我会生成一个大 SQL 命令,它执行相同的操作(检查酒店,更新它,将 id 记录到一个临时变量中,对于每个房间检查是否存在并针对酒店进行更新) id var 等),然后curs.execute(...) 在 python 代码中只做一个,然后我就不再看到死锁错误了。
但是,我真的不明白为什么这会有所不同,而且我也不完全确定在单个 pyodbc 中运行具有多个 SELECTS、INSERTS、UPDATES 的大型 SQL 块是否安全curs.execute。据我了解,pyodbc 假设只处理单个语句,但它似乎确实有效,而且我看到我的表填充时没有死锁错误。
尽管如此,如果我执行这样的大命令,似乎不可能获得任何输出。我尝试@output_string在 finally 之前声明一个变量并向其记录各种内容(例如,我们是否必须插入或更新酒店)SELECT @output_string as outputstring,但是在 pyodbc 中执行后执行提取总是失败
<class 'pyodbc.ProgrammingError'>: No results. Previous SQL was not a query.
Run Code Online (Sandbox Code Playgroud)
shell 中的实验表明 pyodbc 会忽略第一条语句之后的所有内容:
In [11]: curs.execute("SELECT 'HELLO'; SELECT 'BYE';")
Out[11]: <pyodbc.Cursor at 0x7fc52c044a50>
In [12]: curs.fetchall()
Out[12]: [('HELLO', )]
Run Code Online (Sandbox Code Playgroud)
因此,如果第一个语句不是查询,则会出现该错误:
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.
Run Code Online (Sandbox Code Playgroud)
尽管如此,除了无法获取我@output_string真正的“大查询”,包括多个选择、更新、插入实际上有效并填充数据库中的多个表。
尽管如此,如果我尝试类似的东西
curs.execute('INSERT INTO testX (entid, thecol) VALUES (4, 5); INSERT INTO testX (entid, thecol) VALUES (5, 6); SELECT * FROM testX; '
...: )
Run Code Online (Sandbox Code Playgroud)
我看到两行都插入到表中tableX,即使随后的curs.fetchall()失败也显示“以前的 SQL 不是查询”。错误,所以似乎 pyodbc execute 确实执行了所有内容......而不仅仅是第一条语句。
如果我可以相信这一点,那么我的主要问题是如何获得一些输出以进行日志记录。
autocommit=Truedbargs 中的编辑设置似乎可以防止死锁错误,即使有多个 curs.executes。但是为什么要解决这个问题?
Gor*_*son 10
设置
autocommit=True在dbargs似乎防止死锁错误,甚至与多个curs.executes。但是为什么要解决这个问题?
建立连接时,pyodbc 默认autocommit=False按照 Python DB-API 规范。因此,当执行第一条 SQL 语句时,ODBC 开始一个数据库事务,该事务一直有效,直到 Python 代码对连接执行 a.commit()或 a为止.rollback()。
SQL Server 中的默认事务隔离级别是“已提交读”。除非数据库默认配置为支持 SNAPSHOT 隔离,否则在 Read Committed 隔离下的事务中的写操作将在更新的行上放置事务范围的锁。在高并发的情况下,如果多个进程产生冲突的锁,就会发生死锁。如果这些进程使用生成大量此类锁的长期事务,则死锁的可能性更大。
设置autocommit=True将避免死锁,因为每个单独的 SQL 语句将被自动提交,从而结束事务(当该语句开始执行时自动启动)并释放更新行上的任何锁。
因此,为了避免死锁,您可以考虑几种不同的策略:
autocommit=True,或.commit()更频繁,或者SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED“放宽”事务隔离级别并避免由写操作创建的持久锁,或您需要做一些功课来确定针对您的特定用例的最佳策略。