为什么不同时更新sqlalchemy中的同一记录会失败?

时间:2015-02-03 17:20:39

标签: python postgresql orm transactions sqlalchemy

(对不起提前提出这个问题。我试着把它分成几部分,以便让我更清楚我要问的。如果我应该添加任何其他内容或者重新组织它,请告诉我。)

背景

我正在编写一个Web爬虫,它使用生产者/消费者模型,其中包含存储在名为crawler_table的postgresql数据库表中的作业(要爬网或重新爬网的页面)。我正在使用SQLAlchemy访问并更改数据库表。确切的架构对于这个问题并不重要。重要的是我(将)有多个消费者,每个消费者从表中重复选择一个记录,用phantomjs加载页面,然后将有关页面的信息写回记录。

有时两个消费者选择相同的工作。这本身不是问题;然而,重要的是,如果他们同时用结果更新记录,他们会进行一致的更改。对我来说,只要找出更新是否会导致记录变得不一致就足够了。如果是这样,我可以处理它。

调查:

我最初假设如果两个事务在单独的会话中读取然后同时更新相同的记录,则第二个提交将失败。为了测试这个假设,我运行了以下代码(稍微简化):

SQLAlchemySession = sessionmaker(bind=create_engine(my_postgresql_uri))

class Session (object):
    # A simple wrapper for use with `with` statement
    def __enter__ (self):
        self.session = SQLAlchemySession()
        return self.session
    def __exit__ (self, exc_type, exc_val, exc_tb):
        if exc_type:
            self.session.rollback()
        else:
            self.session.commit()
        self.session.close()

with Session() as session:  # Create a record to play with
    if session.query(CrawlerPage) \
              .filter(CrawlerPage.url == 'url').count() == 0:
        session.add(CrawlerPage(website='website', url='url',
                    first_seen=datetime.utcnow()))
    page = session.query(CrawlerPage) \
                  .filter(CrawlerPage.url == 'url') \
                  .one()
    page.failed_count = 0
# commit

# Actual experiment:
with Session() as session:
    page = session.query(CrawlerPage) \
                  .filter(CrawlerPage.url == 'url') \
                  .one()
    print 'initial (session)', page.failed_count
          # 0 (expected)
    page.failed_count += 5
    with Session() as other_session:
        same_page = other_session.query(CrawlerPage) \
                                 .filter(CrawlerPage.url == 'url') \
                                 .one()
        print 'initial (other_session)', same_page.failed_count
              # 0 (expected)
        same_page.failed_count += 10
        print 'final (other_session)', same_page.failed_count
              # 10 (expected)
    # commit other_session, no errors (expected)
    print 'final (session)', page.failed_count
          # 5 (expected)
# commit session, no errors (why?)

with Session() as session:
    page = session.query(CrawlerPage) \
                  .filter(CrawlerPage.url == 'url') \
                  .one()
    print 'final value', page.failed_count
          # 5 (expected, given that there were no errors)

(显然不正确)期望:

我原本希望从记录中读取值然后在同一事务中更新该值:

  1. 是一个原子操作。也就是说,要么完全成功要么完全失败。这似乎是真的,因为最终值是5,即要提交的最后一个事务中设置的值。
  2. 如果正在更新的记录在尝试提交事务时由并发会话(other_session)更新,则会失败。我的理由是,所有事务的行为应该尽可能地以提交的顺序独立执行,或者应该无法提交。在这些情况下,读取的两个事务然后更新相同记录的相同值。在版本控制系统中,这相当于合并冲突。显然,数据库与版本控制系统不同,但它们有足够的相似之处,可以告诉我对它们的一些假设,无论好坏。
  3. 问题:

    • 为什么第二次提交没有引发异常?
      • 我是否误解了SQLAlchemy如何处理交易?
      • 我是否误解了postgresql如何处理交易? (这个似乎对我很有用。)
      • 别的什么?
    • 有没有办法让第二次提交引发异常?

1 个答案:

答案 0 :(得分:1)

PostgreSQLselect . . . for updateSQLAlchemy似乎支持。

  

我的理由是所有交易都应该表现得像   尽可能按提交顺序独立执行,或   应该没有提交。

嗯,总的来说交易比这更多。 PostgreSQL的默认事务隔离级别是" read committed"。简而言之,这意味着多个事务可以同时从表中的相同行读取提交的值。如果要阻止set transaction isolation serializable(可能不起作用)或select...for update,或锁定表,或使用逐列WHERE子句或其他任何内容。

您可以通过打开两个psql连接来测试和演示事务行为。

begin transaction;              begin transaction;
select * 
from test 
where pid = 1 
  and date = '2014-10-01' 
for update;
(1 row)
                                select * 
                                from test 
                                where pid = 1 
                                  and date = '2014-10-01' 
                                for update;
                                (waiting)
update test 
set date = '2014-10-31' 
where pid = 1 
  and date = '2014-10-01';

commit;
                                 -- Locks released. SELECT for update fails.
                                 (0 rows)