【问题标题】:Why don't simultaneous updates to the same record in sqlalchemy fail?为什么在 sqlalchemy 中同时更新同一条记录不会失败?
【发布时间】:2015-04-03 00:06:47
【问题描述】:

(抱歉,这个问题太长了。我试着把它分成几个部分,以便更清楚地说明我在问什么。如果我应该添加任何其他内容或重新组织它,请告诉我。)

背景:

我正在编写一个网络爬虫,它使用生产者/消费者模型和存储在名为 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) 更新,则会失败。我的理由是,所有事务都应该表现得好像它们尽可能按照提交顺序独立执行,或者应该无法提交。在这些情况下,读取的两个事务会更新同一记录的相同值。在版本控制系统中,这相当于合并冲突。显然,数据库与版本控制系统不同,但它们有足够的相似性来说明我对它们的一些假设,无论好坏。

问题:

  • 为什么第二次提交没有引发异常?
    • 我对 SQLAlchemy 如何处理事务有误解吗?
    • 我对 postgresql 如何处理事务有误解吗? (我觉得这个最有可能。)
    • 还有别的吗?
  • 有没有办法让第二次提交引发异常?

【问题讨论】:

标签: python postgresql orm transactions sqlalchemy


【解决方案1】:

PostgreSQLselect . . . for updateSQLAlchemy 似乎支持。

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

嗯,一般来说,交易的意义远不止于此。 PostgreSQL 的默认事务隔离级别是“已提交读”。粗略地说,这意味着多个事务可以同时读取表中相同行的已提交值。如果您想防止这种情况发生,set transaction isolation serializable(可能不起作用)或select...for update,或锁定表,或使用逐列的 WHERE 子句,或其他任何方式。

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

开始交易;开始交易; 选择 * 从测试 其中 pid = 1 和日期 = '2014-10-01' 用于更新; (1 行) 选择 * 从测试 其中 pid = 1 和日期 = '2014-10-01' 用于更新; (等待) 更新测试 设置日期 = '2014-10-31' 其中 pid = 1 和日期 = '2014-10-01'; 犯罪; -- 锁被释放。 SELECT 更新失败。 (0 行)

【讨论】:

    猜你喜欢
    • 2011-07-28
    • 2013-09-01
    • 1970-01-01
    • 2019-03-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-11
    • 2017-11-25
    相关资源
    最近更新 更多