【问题标题】:Locking tables causes savepoint issues with django锁定表会导致 django 的保存点问题
【发布时间】:2019-05-29 08:16:14
【问题描述】:

我正在尝试在 Django 应用程序的 MariaDB 数据库中构建有向无环图 (DAG)。因为这是非循环的,所以我需要验证任何添加的元素(顶点/边)不会在图中创建循环。

许多客户端会尝试在一天中同时添加元素,但是这些循环检查需要是原子的,所以我认为在添加/更新元素时​​需要使用一些锁。 Django 似乎没有提供类似的东西,所以我尝试使用原始的LOCK TABLES/UNLOCK TABLES 查询。这是我用来执行此操作的代码...

def lock_tables():                                                                    
    cursor = get_connection(DEFAULT_DB_ALIAS).cursor()                            

    tables = [                                                                    
        'vertex',                                                  
        'edge'                                                                                             
    ]                                                                             

    lock_query = ', '.join(                                                
        "{} {}".format(table, 'WRITE') for table in tables                        
    )                                                                             

    query = 'LOCK TABLES {}'.format(lock_query)                            
    cursor.execute(query)                                                         


def unlock_tables():                                                                  
    cursor = get_connection(DEFAULT_DB_ALIAS).cursor()                            
    cursor.execute('UNLOCK TABLES')

然后在我模式的save 方法中......

@transaction.atomic()
def save(self, *args, **kwargs):

    print("---INSIDE MODEL SAVE")

    try:
        print("---LOCKING TABLES")
        lock_tables()
        print("---LOCKED TABLES")

        super().save(*args, **kwargs)

        # TODO: Add Cycle check here
    except Exception as ex:
        print("---EXCEPTION THROWN INSIDE SAVE: {}".format(ex))
        raise
    finally:
        print("---UNLOCKING TABLES")
        unlock_tables()
        print("---UNLOCKED TABLES")   

然而,关于锁定和解锁这些表的一些事情会弄乱使用django.db.transaction.atomic 创建的保存点...有时当 Django 尝试退出 atomic 上下文时,它会尝试回滚到它已经存在的保存点发布。

这里有一些我试图捕捉问题的日志,Executing Query 行来自django.db.backends.mysql.baseSTARTING/EXITING ATOMIC 行来自django.db.transactions.atomic __enter__/__exit__ 方法,#### 之后的注释是我在事后添加的 cmets 试图解释我的想法。

---STARTING ATOMIC  #### Atomic context wrapping my serializer's create method
Executing query: 'SAVEPOINT `s139667621889792_x1`' - args: None

---STARTING ATOMIC  #### Atomic context wrapping my model's save method
Executing query: 'SAVEPOINT `s139667621889792_x2`' - args: None

---INSIDE MODEL SAVE
---LOCKING TABLES
Executing query: 'LOCK TABLES vertex WRITE, edge WRITE
---LOCKED TABLES

---STARTING ATOMIC  #### I think Django must wrap some queries in an atomic block, but this doesnt even create a savepoint
Executing query: 'INSERT INTO `edge`...
---EXITING ATOMIC

#### WHERE MY CYCLE CHECK CODE WOULD RUN - not implemented yet

---UNLOCKING TABLES
Executing query: 'UNLOCK TABLES' - args: None
---UNLOCKED TABLES

---EXITING ATOMIC
Executing query: 'RELEASE SAVEPOINT `s139667621889792_x2`' - args: None
Executing query: 'ROLLBACK TO SAVEPOINT `s139667621889792_x2`' - args: None   ### WHAT I BELIEVE TO BE THE OFFENDING QUERY

---EXITING ATOMIC
Executing query: 'ROLLBACK TO SAVEPOINT `s139667621889792_x1`' - args: None

Traceback (most recent call last):
  File ".../site-packages/django/db/backends/utils.py", line 83, in _execute
    return self.cursor.execute(sql)
  File ".../site-packages/django/db/backends/mysql/base.py", line 72, in execute
    return self.cursor.execute(query, args)
  File ".../site-packages/pymysql/cursors.py", line 170, in execute
    result = self._query(query)
  File ".../site-packages/pymysql/cursors.py", line 328, in _query
    conn.query(q)
  File ".../site-packages/pymysql/connections.py", line 516, in query
    self._affected_rows = self._read_query_result(unbuffered=unbuffered)
  File ".../site-packages/pymysql/connections.py", line 727, in _read_query_result
    result.read()
  File ".../site-packages/pymysql/connections.py", line 1066, in read
    first_packet = self.connection._read_packet()
  File ".../site-packages/pymysql/connections.py", line 683, in _read_packet
    packet.check_error()
  File ".../site-packages/pymysql/protocol.py", line 220, in check_error
    err.raise_mysql_exception(self._data)
  File ".../site-packages/pymysql/err.py", line 109, in raise_mysql_exception
    raise errorclass(errno, errval)
pymysql.err.InternalError: (1305, 'SAVEPOINT s139667621889792_x2 does not exist')

如上所示,django 尝试回滚到它已经释放的保存点。如果我删除对锁定/解锁表的调用,此代码可以完美运行,但是我不能再保证我的循环检查是原子的。

以前有没有人遇到过这个问题,或者有任何关于如何深入挖掘原因的提示?

编辑:我读得越多,我就越认为我想要的行为是不可能的。根据MySQL docs on locks,似乎在您获得表锁定时提交了事务。这打破了我的用例,因为如果我的周期检查失败,我希望回滚事务。

【问题讨论】:

    标签: mysql django mariadb directed-acyclic-graphs


    【解决方案1】:

    任何反循环算法都依赖于在执行检查时表不发生变化。正确的?执行周期检查需要多长时间?您每天需要多少张支票?

    假设您有足够的时间来完成所有这些工作,那么请考虑一下:

    SELECT GET_LOCK('cycle_check');  -- (you may want timeout)
    BEGIN;
    INSERT new item in graph
    perform cycle check
    if ... COMMIT else ROLLBACK
    SELECT RELEASE_LOCK('cycle_check');
    

    请注意,这种锁定机制与导致LOCK TABLES 无用的特性不同。

    为了防止在循环检查期间读取,您还需要:

    SELECT GET_LOCK('cycle_check');
    SELECT ...;
    SELECT RELEASE_LOCK('cycle_check');
    

    (旁注:GET_LOCK 是执行锁定的“正确”方式极为罕见。请不要将其扩展到任意其他情况。)

    【讨论】:

    • 我不知道锁也可以独立于表!这可能有用...将报告回来。循环检查应该阻止更改和读取(不希望有人在检查新顶点/边时读取它)
    • @wKavey - 我猜你也需要在SELECTs 附近获得/释放。这是一个简单的实现,不像大多数允许多个同时读取的数据库锁。我添加了一个关于阅读的注释。 (我看不到同时允许SELECTs 避免循环检查的方法。)
    • 感谢您的回复。我正在构建一个依赖管理系统,所以我需要对每个添加执行此检查(以防止添加错误的依赖),这可能会产生很多开销。我可能需要重新评估我的方法,因为手动包装 Django 执行的选择并不是最简单的事情。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多