Posted at

DjangoのModel.save()の動作と、MySQLのデッドロックエラーについて

More than 3 years have passed since last update.


はじめに

DjangoのModel.save()を使ってレコード保存をしようとしたところ、MySQLのデッドロックエラーが発生したことがありましたので、その理由を調べました。


前提

Django1.8.4, MySQL5.5 InnoDB、トランザクション分離レベルは REPEATABLE-READ です。


DjangoのModel.save()について

https://docs.djangoproject.com/en/1.8/ref/models/instances/#how-django-knows-to-update-vs-insert を引用します。


You may have noticed Django database objects use the same save() method for creating and changing objects. Django abstracts the need to use INSERT or UPDATE SQL statements. Specifically, when you call save(), Django follows this algorithm:


  • If the object’s primary key attribute is set to a value that evaluates to True (i.e., a value other than None or the empty string), Django executes an UPDATE.

  • If the object’s primary key attribute is not set or if the UPDATE didn’t update anything, Django executes an INSERT.


Djangoはsave()を使う際に、利用者にINSERTかUPDATEを意識させない作りにしているようです。インスタンスのprimary keyがTrueと評価できる場合、UPDATEを発行します。このUPDATEで変更行が0だった場合には改めてINSERTを発行するようです。


デッドロックが起きる例

以下のようなmodelを用意します。DerivedはBaseを継承したモデルです。


models.py

from django.db import models

class Base(models.Model):
a = models.IntegerField()

class Derived(Base):
b = models.IntegerField()


Derivedを Model.save() を使ってレコードを生成しようとするとどうなるでしょうか。答えを言うと、以下のような簡単なレコード生成処理を高頻度並列で行った場合、MySQLのデッドロックエラー ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction が発生します。


create_derived.py

d = Derived(a=1, b=2)

d.save()

=> ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction !!!!



理由

まず model.py のテーブル定義を確認します。以下のようなCREATE TABLE文になっています。Derivedモデルは base_ptr_id を継承元情報として保持し、なおかつPRIMARY KEYになっています。

CREATE TABLE `ham_base` (

`id` int(11) NOT NULL AUTO_INCREMENT,
`a` int(11) NOT NULL,
PRIMARY KEY (`id`)
)

CREATE TABLE `ham_derived` (
`base_ptr_id` int(11) NOT NULL,
`b` int(11) NOT NULL,
PRIMARY KEY (`base_ptr_id`),
CONSTRAINT `ham_derived_base_ptr_id_12f18f813c81ff4f_fk_ham_base_id` FOREIGN KEY (`base_ptr_id`) REFERENCES `ham_base` (`id`)
)

次に create_derived.pysave() 時に発行されるクエリを確認します。

INSERT INTO `ham_base` (`a`) VALUES (1);

UPDATE `ham_derived` SET `b` = 2 WHERE `ham_derived`.`base_ptr_id` = 1 ; args=(2, 1)
INSERT INTO `ham_derived` (`base_ptr_id`, `b`) VALUES (1, 2); args=(1, 2)


  • 最初にBaseのレコードを生成しています。これはPRIMARY KEYがFalseなのでINSERTになっています。

  • 次にDerivedに対してUPDATEが発行されています。これは base_ptr_id が最初のINSERTによって確定し、PRIMARY KEYがTrueと評価できたからです。ただし、この時点でDerivedのレコードはないので UPDATEは必ず空振りします。

  • 最後に、UPDATEが空振ったのでINSERTでDerivedレコードを作成しています。

2番目の空振りするUPDATEがクセモノで、トランザクション分離レベルREPEATABLE-READの場合にギャップロックを獲得するようです。タイミング次第で以下のような状況になり、デッドロックが発生します。

Transaction1
Transaction2
コメント

INSERT INTO ham_base (a) VALUES (1);

INSERT INTO ham_base (a) VALUES (1);

UPDATE ham_derived SET b = 2 WHERE ham_derived.base_ptr_id = 1;

ロックA獲得

UPDATE ham_derived SET b = 2 WHERE ham_derived.base_ptr_id = 2;
ロックB獲得

INSERT INTO ham_derived (base_ptr_id, b) VALUES (1, 2)

ロックB待ち

INSERT INTO ham_derived (base_ptr_id, b) VALUES (2, 2)
ロックA待ちになるので即座にデッドロック検出


回避方法

INSERTであることを明示すればよいです。以下のような方法で回避できます。



  • Model.objects.create() を使う


  • Model.save()force_insert オプションを利用する。


  • Modelのmetaオプションで select_on_save=True を指定する。


    Determines if Django will use the pre-1.6 django.db.models.Model.save() algorithm. The old algorithm uses SELECT to determine if there is an existing row to be updated. The new algorithm tries an UPDATE directly.