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

Why don't simultaneous updates to the same record in sqlalchemy fail?

(很抱歉,这个问题很长。我试图将其分成几个部分,以便更清楚地说明我的问题。如果我应该添加任何其他内容或重新组织,请告诉我。)

背景:

我正在编写一个网络爬虫,它使用 producer/consumer 模型,作业(要爬网或重新爬网的页面)存储在名为 crawler_table 的 postgresql 数据库 table 中。我正在使用 SQLAlchemy 访问和更改数据库 table。确切的模式对于这个问题并不重要。重要的是我(将)有多个消费者,每个消费者重复 selects 来自 table 的记录,用 phantomjs 加载页面,然后将有关页面的信息写回记录。

有时会发生两个消费者 select 同一份工作。这本身不是问题;但是,重要的是,如果他们同时用他们的结果更新记录,那么他们会做出一致的更改。对我来说,只要查明更新是否会导致记录变得不一致就足够了。如果是这样,我可以处理它。

调查:

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

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)

(显然不正确)期望值:

我原以为从记录中读取值然后在同一个 t运行saction 中更新该值会:

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

问题:

PostgreSQL has select . . . for update, which SQLAlchemy好像支持

My rationale is that all transactions should behave as though they are performed independently in order of commit whenever possible, or should fail to commit.

嗯,一般来说,交易的意义远不止于此。 PostgreSQL 的默认事务隔离级别是 "read committed"。粗略地说,这意味着多个事务可以同时 读取 来自 table 中相同行的提交值。如果你想防止这种情况,set transaction isolation serializable(可能不起作用),或 select...for update,或锁定 table,或使用逐列的 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)