Upgrade to Pro — share decks privately, control downloads, hide ads and more …

どうなってるの?Djangoのトランザクション

 どうなってるの?Djangoのトランザクション

Django Congress2019 での資料です。

Djangoのtranasctionモジュールを中心にコードの話やトランザクション分離レベルの話などをします。

Denzow

May 18, 2019
Tweet

More Decks by Denzow

Other Decks in Technology

Transcript

  1. Ͳ͏ͳͬͯΔͷʁDjangoͷτϥϯβΫγϣϯ
    DjangoCongress JP 2019
    Denzow Yamada
    @denzowill

    View full-size slide

  2. 2
    お前誰よ?
    https://lapras.com/person

    View full-size slide

  3. 3
    ➡ でんぞう (@denzowill, denzow)
    ➡ Python歴 6年 (仕事で1.5年)
    ➡ scouty,inc シニアエンジニア(前職はDBサポートエンジニア)
    ➡ DBスペシャリスト(PostgreSQLが好き、●racleは得意だけど苦⼿)
    お前誰よ?

    View full-size slide

  4. 4
    ➡ でんぞう (@denzowill, denzow)
    ➡ Python歴 6年 (仕事で1.5年)
    ➡ scouty,inc シニアエンジニア(前職はDBサポートエンジニア)
    ➡ DBスペシャリスト(PostgreSQLが好き、●racleは得意だけど苦⼿)
    お前誰よ?
    LAPRAS

    View full-size slide

  5. ͋ΒΏΔࣄ৅ΛඞવԽ͠ɺ
    ੈͷதͷϛεϚονΛͳ͘͢
    ݸਓ͕࣋ͭ͞·͟·ͳ࠽ೳͱɺੈքͱͷ઀఺͸ແݶʹ͋Γ·͢ɻ
    ఱ৬ͱͷग़͍͋ͷΑ͏ʹɺʮϛεϚονͷ઀఺ʯΛഉআ͠ɺʮ࠷దͳ઀఺ʯͱ઀ଓͰ͖Ε͹ɺ
    ਓʑ͸ྗΛ࠷େݶʹൃش͠ɺΑΓ͍͖͍͖ͱͨ͠ঢ়ଶΛ࣮ݱͰ͖Δ͸ͣͰ͢ɻ
    ͋ͳͨͷݸੑΛٻΊΔਓ͕ݱΕΔͷ΋ɺ-"13"4ͳΒܾͯ͠ۮવͰ͸͋Γ·ͤΜɻ
    -"13"4͸"*ٕज़Λ΋ͱʹɺ͋ΒΏΔਓΛج఺ʹ৘ใΛ࠶ߏங͠ɺ
    ৽͍͠αʔϏεΛ૑଄͠·͢ɻ
    M I S S I O N

    View full-size slide

  6. https://startpython.connpass.com/

    View full-size slide

  7. Python と
    Django と

    View full-size slide

  8. Amazon

    DynamoDB
    Amazon ECS
    Amazon ECS
    Amazon ECS
    Amazon ECS
    Amazon

    SQS
    Elastic Load
    Balancing*
    AWS
    Lambda
    Amazon
    CloudWatch
    Amazon

    RDS Aurora

    (MySQL 5.7)
    Amazon 

    ElastiCache
    sns-activity

    watcher
    worker
    Ϋϩʔϧͨ͠

    ੜσʔλͷdiff
    ੔ܗ͞Εͨσʔλ
    event 

    (time-based)











    インフラ構成図
    crawler
    Amazon

    RDS
    (MySQL 8.0)

    View full-size slide

  9. Amazon

    DynamoDB
    Amazon ECS
    Amazon ECS
    Amazon ECS
    Amazon ECS
    Amazon

    SQS
    Elastic Load
    Balancing*
    AWS
    Lambda
    Amazon
    CloudWatch
    Amazon

    RDS Aurora

    (MySQL 5.7)
    Amazon 

    ElastiCache
    sns-activity

    watcher
    worker
    Ϋϩʔϧͨ͠

    ੜσʔλͷdiff
    ੔ܗ͞Εͨσʔλ
    event 

    (time-based)











    インフラ構成図
    crawler
    Amazon

    RDS
    (MySQL 8.0)

    View full-size slide

  10. Amazon

    DynamoDB
    Amazon ECS
    Amazon ECS
    Amazon ECS
    Amazon ECS
    Amazon

    SQS
    Elastic Load
    Balancing*
    AWS
    Lambda
    Amazon
    CloudWatch
    Amazon

    RDS Aurora

    (MySQL 5.7)
    Amazon 

    ElastiCache
    sns-activity

    watcher
    worker
    Ϋϩʔϧͨ͠

    ੜσʔλͷdiff
    ੔ܗ͞Εͨσʔλ
    event 

    (time-based)











    インフラ構成図
    crawler
    Amazon

    RDS
    (MySQL 8.0)

    View full-size slide

  11. 11
    Django と scouty(2018/5)
    ➡ Django 1.11
    ➡ Django Celery
    ➡ Django でDDD的な設計
    ➡ Django Channels でのWebsocket(予定)
    ➡ daphneでの本番運⽤

    View full-size slide

  12. 12
    Django と LAPRAS(2019/5)
    ➡ Django 2.1
    ➡ Django Celery
    ➡ Django でDDD的な設計
    ➡ Django Channels でのWebsocket
    ➡ daphneでの本番運⽤

    View full-size slide

  13. https://speakerdeck.com/denzow/imasarazhen-rifan-ru-django-migration

    View full-size slide

  14. https://speakerdeck.com/denzow/imasarazhen-rifan-ru-django-migration
    શ1

    View full-size slide

  15. https://speakerdeck.com/denzow/djangotovuedezuo-rukanbanapurikesiyon

    View full-size slide

  16. https://speakerdeck.com/denzow/djangotovuedezuo-rukanbanapurikesiyon
    શ1

    View full-size slide

  17. ͏ΘͬʜࢲͷεϥΠυɺ
    ଟ͗͢ʜʁ

    View full-size slide

  18. ࡞ΕΔ͚ͩ࡞͕ͬͯ࣌ؒڐ͚ͩ͢
    ࿩ͤ͹ྑ͍Μͩ

    View full-size slide

  19. 21
    with transaction.atomic():
    Book.objects.create()
    with transaction.atomic():
    Book.objects.create()
    with transaction.atomic():
    Book.objects.create()

    View full-size slide

  20. 22
    このコードに潜りたい

    View full-size slide

  21. 今⽇話すこと 23
    ➡Djangoのトランザクション周りのソースを

    読んで知的好奇⼼を満たす
    ➡そのcommit誰に⾒えるの?でやらかさないために

    View full-size slide

  22. 24
    ➡ そもそもトランザクション
    ➡ RDBMSでのトランザクション
    ➡ RDBMS毎の違い
    ➡ Djangoのトランザクション
    ➡ トランザクション周りの⾟かった話
    アジェンダ

    View full-size slide

  23. そもそもトランザクション

    View full-size slide

  24. 26
    トランザクション
    チケットの値段確定
    ⼝座引き落とし
    購⼊確定
    発注処理

    View full-size slide

  25. 27
    トランザクション
    チケットの値段確定
    ⼝座引き落とし
    購⼊確定
    発注処理
    ⼀連の処理

    View full-size slide

  26. 28
    トランザクション
    チケットの値段確定
    ⼝座引き落とし
    購⼊確定
    発注処理

    View full-size slide

  27. 29
    トランザクション
    チケットの値段確定
    ⼝座引き落とし
    購⼊確定
    発注処理

    View full-size slide

  28. 30
    トランザクション
    チケットの値段確定
    ⼝座引き落とし
    購⼊確定
    発注処理
    残⾼だけ減っている...!

    View full-size slide

  29. 31
    トランザクション
    チケットの値段確定
    ⼝座引き落とし
    購⼊確定
    発注処理
    いっそ全部爆発しろ

    View full-size slide

  30. RDMSでのトランザクション

    View full-size slide

  31. 33
    RDBMSでのトランザクション
    EC#&(*/
    EC*/4&35*/50IPHF7"-6&4
    EC61%"5&GPPTFU
    EC%&-&5&'30.CBS
    EC$0..*5

    View full-size slide

  32. 34
    EC#&(*/
    EC*/4&35*/50IPHF7"-6&4
    EC61%"5&GPPTFU
    EC%&-&5&'30.CBS
    EC$0..*5
    RDBMSでのトランザクション

    View full-size slide

  33. 35
    トランザクション分離レベル
    トランザクションが全成功か全失敗しか無いのは
    同じだけど他のセッションからの⾒え⽅でレベルがある

    View full-size slide

  34. 36
    トランザクション分離レベル
    分離レベル ダーティーリード ファジーリード ファントムリード
    READ UNCOMMITTED 発⽣する 発⽣する 発⽣する
    READ COMMITTED 発⽣しない 発⽣する 発⽣する
    REPETABLE READ 発⽣しない 発⽣しない 発⽣する
    SERIALIZABLE 発⽣しない 発⽣しない 発⽣しない

    View full-size slide

  35. 37
    ダーティーリード
    BEGIN;
    BEGIN;
    INSERT INTO A...;
    SELECT ... FROM A;
    未commitの
    INSERT結果が⾒える

    View full-size slide

  36. 38
    ファジーリード
    BEGIN;
    BEGIN;
    UPDATE A... WHER ID=1;
    SELECT ... FROM A;
    COMMIT;
    SELECT ... FROM A;
    さっきとレコードの
    内容が違う

    View full-size slide

  37. 39
    ファントムリード
    BEGIN;
    BEGIN;
    INSERT INTO A...;
    SELECT ... FROM A;
    COMMIT;
    SELECT ... FROM A;
    ⾏が増えてる

    View full-size slide

  38. 40
    トランザクション分離レベル
    分離レベル ダーティーリード ファジーリード ファントムリード
    READ UNCOMMITTED 発⽣する 発⽣する 発⽣する
    READ COMMITTED 発⽣しない 発⽣する 発⽣する
    REPETABLE READ 発⽣しない 発⽣しない 発⽣する
    SERIALIZABLE 発⽣しない 発⽣しない 発⽣しない

    View full-size slide

  39. RDMSごとの実装の違い

    View full-size slide

  40. 42
    RDBMSの種類は様々

    View full-size slide

  41. 43
    RDBMSの種類は様々(デフォルト分離レベル)
    READ COMMITTED
    READ COMMITTED
    REPETABLE READ

    View full-size slide

  42. 44
    RDBMSの種類は様々(⾃動COMMIT)
    NO
    YES
    YES

    View full-size slide

  43. 45
    RDBMSの種類は様々(⾃動COMMIT)
    NO
    YES
    YES
    あくまでDBレベルなので
    アプリケーションからはほぼ意識しない
    (ドライバが吸収している)

    View full-size slide

  44. 46
    分離レベル ダーティーリード ファジーリード ファントムリード
    READ UNCOMMITTED 発⽣する 発⽣する 発⽣する
    READ COMMITTED 発⽣しない 発⽣する 発⽣する
    REPETABLE READ 発⽣しない 発⽣しない 発⽣する
    SERIALIZABLE 発⽣しない 発⽣しない 発⽣しない
    MySQLのREPETABLE READ

    View full-size slide

  45. 47
    分離レベル ダーティーリード ファジーリード ファントムリード
    READ UNCOMMITTED 発⽣する 発⽣する 発⽣する
    READ COMMITTED 発⽣しない 発⽣する 発⽣する
    REPETABLE READ 発⽣しない 発⽣しない 発⽣しない
    SERIALIZABLE 発⽣しない 発⽣しない 発⽣しない
    MySQLのREPETABLE READ

    View full-size slide

  46. 48
    MySQLのREPETABLE READ
    最強

    View full-size slide

  47. 49
    MySQLのREPETABLE READ
    https://dev.mysql.com/doc/refman/5.6/ja/innodb-record-level-locks.html

    View full-size slide

  48. 50
    MySQLのREPETABLE READ
    https://dev.mysql.com/doc/refman/5.6/ja/innodb-record-level-locks.html
    ロック範囲広すぎ

    View full-size slide

  49. 51
    MySQLのREPETABLE READ
    https://dev.mysql.com/doc/refman/5.6/ja/innodb-record-level-locks.html
    パフォーマンスつらい

    View full-size slide

  50. Djangoのトランザクション

    View full-size slide

  51. ようやく
    Djangoの話

    View full-size slide

  52. 54
    Djangoのトランザクションのデフォルト
    デフォルトは AutoCommit + DBのデフォルト分離レベル

    View full-size slide

  53. 55
    Djangoのトランザクションのデフォルト
    AutoCommit + REPETABLE READ
    AutoCommit + READ COMMITTED

    View full-size slide

  54. 56
    Djangoのトランザクションのデフォルト
    AutoCommit + REPETABLE READ
    AutoCommit + READ COMMITTED

    View full-size slide

  55. 57
    Djangoのトランザクションのデフォルト
    AutoCommit + REPETABLE READ
    AutoCommit + READ COMMITTED
    AutoCommit + READ COMMITTED

    View full-size slide

  56. 58
    1.11までのMySQLの初期化
    # django.db.backends.mysql.base.DatabaseWrapper#get_connection_params
    def get_connection_params(self):
    :
    options = settings_dict['OPTIONS'].copy()
    isolation_level = options.pop('isolation_level', None)
    if isolation_level:
    :
    :
    isolation_level = isolation_level.replace(' ', '-')
    self.isolation_level = isolation_level
    kwargs.update(options)
    return kwargs
    指定されてなければ何も指定しない
    -> DB側のデフォルト値になる

    View full-size slide

  57. 59
    2.0からのMySQLの初期化
    # django.db.backends.mysql.base.DatabaseWrapper#get_connection_params
    def get_connection_params(self):
    :
    options = settings_dict['OPTIONS'].copy()
    isolation_level = options.pop('isolation_level', 'read_committed')
    if isolation_level:
    :
    :
    isolation_level = isolation_level.replace(' ', '-')
    self.isolation_level = isolation_level
    kwargs.update(options)
    return kwargs
    指定されてなければ
    read_committedを設定する

    View full-size slide

  58. 60
    2.0からのMySQLの初期化
    https://docs.djangoproject.com/en/2.2/ref/databases/#mysql-isolation-level

    View full-size slide

  59. 61
    2.0からのMySQLの初期化
    https://docs.djangoproject.com/en/2.2/ref/databases/#mysql-isolation-level

    View full-size slide

  60. 62
    PostgreSQLの初期化
    # django.db.backends.postgresql.base.DatabaseWrapper#get_connection_params
    def get_new_connection(self, conn_params):
    connection = Database.connect(**conn_params)
    :
    options = self.settings_dict['OPTIONS']
    try:
    self.isolation_level = options['isolation_level']
    except KeyError:
    self.isolation_level = connection.isolation_level
    else:
    # Set the isolation level to the value from OPTIONS.
    if self.isolation_level != connection.isolation_level:
    connection.set_session(isolation_level=self.isolation_level)
    return connection
    指定されてなければ
    サーバ⾃体のデフォルト値を使う

    View full-size slide

  61. 63
    ちなみにOPTIONS
    DATABASES = {
    'default': {
    'ENGINE': 'django.db.backends.postgresql',
    'NAME': 'docker',
    'USER': 'docker',
    'PASSWORD': 'docker',
    'HOST': 'db',
    'PORT': '5432',
    'OPTIONS': {
    'isolation_level': psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE,
    }
    }
    }

    View full-size slide

  62. Djangoでの
    トランザクション管理

    View full-size slide

  63. 65
    Djangoでのトランザクション管理
    def start_blog(request):
    author = Author.objects.create(name='test')
    blog = Blog.objects.create(
    title='test_title',
    author=author,
    )
    return HttpResponse(f'success create blog {blog.id}')

    View full-size slide

  64. 66
    Djangoでのトランザクション管理
    def start_blog(request):
    author = Author.objects.create(name='test')
    blog = Blog.objects.create(
    title='test_title',
    author=author,
    )
    return HttpResponse(f'success create blog {blog.id}')

    View full-size slide

  65. 67
    Djangoでのトランザクション管理
    LOG: duration: 1.111 ms statement: INSERT INTO "blog_author" ("name") VALUES ('test') RETURNING "blog_author"."id"
    LOG: duration: 0.517 ms statement: INSERT INTO "blog_blog" ("author_id", "title") VALUES (2, 'test_title') RETURNING
    "blog_blog"."id"
    BEGIN/COMMITがないので各SQL毎にCOMMIT

    View full-size slide

  66. 68
    Djangoでのトランザクション管理
    def start_blog_with_transaction(request):
    with transaction.atomic():
    author = Author.objects.create(name='test')
    blog = Blog.objects.create(
    title='test_title',
    author=author,
    )
    return HttpResponse(f'success create blog {blog.id}')

    View full-size slide

  67. 69
    Djangoでのトランザクション管理
    LOG: duration: 0.099 ms statement: BEGIN
    LOG: duration: 1.111 ms statement: INSERT INTO "blog_author" ("name") VALUES ('test') RETURNING "blog_author"."id"
    LOG: duration: 0.517 ms statement: INSERT INTO "blog_blog" ("author_id", "title") VALUES (2, 'test_title') RETURNING
    "blog_blog"."id"
    LOG: duration: 4.009 ms statement: COMMIT
    2つのSQLがBEGIN/COMMITで囲まれており
    トランザクションとして処理されている

    View full-size slide

  68. transaction.atomic()

    View full-size slide

  69. 71
    transaction.atomic()
    # django.db.transaction.atomic
    def atomic(using=None, savepoint=True):
    if callable(using):
    return Atomic(DEFAULT_DB_ALIAS, savepoint)(using)
    else:
    return Atomic(using, savepoint)

    View full-size slide

  70. 72
    transaction.atomic()
    # django.db.transaction.atomic
    def atomic(using=None, savepoint=True):
    if callable(using):
    return Atomic(DEFAULT_DB_ALIAS, savepoint)(using)
    else:
    return Atomic(using, savepoint)
    @transaction.atomicの場合
    @transaction.atomic()
    or
    普通に呼び出した場合

    View full-size slide

  71. 73
    transaction.atomic()
    @transaction.atomic
    def start_blog(request):
    author = Author.objects.create(name='test')
    blog = Blog.objects.create(
    title='test_title',
    author=author,
    )
    return HttpResponse(f'success create blog {blog.id}')

    View full-size slide

  72. 74
    transaction.atomic()
    # django.db.transaction.Atomic
    class Atomic(ContextDecorator):
    def __enter__(self):
    connection = get_connection(self.using)
    if not connection.in_atomic_block:
    # Reset state when entering an outermost atomic block.
    connection.commit_on_exit = True
    connection.needs_rollback = False
    if not connection.get_autocommit():
    connection.in_atomic_block = True
    connection.commit_on_exit = False
    if connection.in_atomic_block:
    if self.savepoint and not connection.needs_rollback:
    sid = connection.savepoint()
    connection.savepoint_ids.append(sid)
    else:
    connection.savepoint_ids.append(None)
    else:
    connection.set_autocommit(False, force_begin_transaction_with_broken_autocommit=True)
    connection.in_atomic_block = True

    View full-size slide

  73. 75
    transaction.atomic()
    # django.db.transaction.Atomic
    class Atomic(ContextDecorator):
    def __enter__(self):
    connection = get_connection(self.using)
    if not connection.in_atomic_block:
    # Reset state when entering an outermost atomic block.
    connection.commit_on_exit = True
    connection.needs_rollback = False
    if not connection.get_autocommit():
    connection.in_atomic_block = True
    connection.commit_on_exit = False
    if connection.in_atomic_block:
    if self.savepoint and not connection.needs_rollback:
    sid = connection.savepoint()
    connection.savepoint_ids.append(sid)
    else:
    connection.savepoint_ids.append(None)
    else:
    connection.set_autocommit(False, force_begin_transaction_with_broken_autocommit=True)
    connection.in_atomic_block = True
    DBとのコネクションをラップした
    DatabaseWrapperにin_atomic_blockフラグを⽴て
    autocommitをFalseにする

    View full-size slide

  74. 76
    transaction.atomic()
    DATABASES = {
    'default': {
    'ENGINE': 'django.db.backends.postgresql_psycopg2',
    'NAME': 'djangodb',
    'USER': 'hoge',
    'PASSWORD': 'password',
    'HOST': 'localhost',
    'PORT': '5432',
    'ATOMIC_REQUESTS': True,
    }
    }

    View full-size slide

  75. 77
    transaction.atomic()
    DATABASES = {
    'default': {
    'ENGINE': 'django.db.backends.postgresql_psycopg2',
    'NAME': 'djangodb',
    'USER': 'hoge',
    'PASSWORD': 'password',
    'HOST': 'localhost',
    'PORT': '5432',
    'ATOMIC_REQUESTS': True,
    }
    }

    View full-size slide

  76. 78
    transaction.atomic()
    # django.core.handlers.base.BaseHandler#make_view_atomic
    def make_view_atomic(self, view):
    non_atomic_requests = getattr(view, '_non_atomic_requests', set())
    for db in connections.all():
    if db.settings_dict['ATOMIC_REQUESTS'] and db.alias not in non_atomic_requests:
    view = transaction.atomic(using=db.alias)(view)
    return view
    non_atomic_requestsに指定してないViewは
    transaction.atomic()で全体をラップして処理

    View full-size slide

  77. 79
    Djangoでのトランザクション管理
    def start_blog_with_nested_transaction(request):
    with transaction.atomic():
    author = Author.objects.create(name='test')
    with transaction.atomic():
    blog = Blog.objects.create(
    title='test_title',
    author=author,
    )
    return HttpResponse(f'success create blog {blog.id}')

    View full-size slide

  78. 80
    Djangoでのトランザクション管理
    def start_blog_with_nested_transaction(request):
    with transaction.atomic():
    author = Author.objects.create(name='test')
    with transaction.atomic():
    blog = Blog.objects.create(
    title='test_title',
    author=author,
    )
    return HttpResponse(f'success create blog {blog.id}')

    View full-size slide

  79. 81
    Djangoでのトランザクション管理
    LOG: duration: 0.072 ms statement: BEGIN
    LOG: duration: 1.367 ms statement: INSERT INTO "blog_author" ("name") VALUES ('test') RETURNING
    "blog_author"."id"
    LOG: duration: 0.070 ms statement: SAVEPOINT "s140129009637120_x1"
    LOG: duration: 1.071 ms statement: INSERT INTO "blog_blog" ("author_id", "title") VALUES (4, 'test_title') RETURNING
    "blog_blog"."id"
    LOG: duration: 0.052 ms statement: RELEASE SAVEPOINT "s140129009637120_x1"
    LOG: duration: 2.085 ms statement: COMMIT

    View full-size slide

  80. 82
    Djangoでのトランザクション管理
    LOG: duration: 0.072 ms statement: BEGIN
    LOG: duration: 1.367 ms statement: INSERT INTO "blog_author" ("name") VALUES ('test') RETURNING
    "blog_author"."id"
    LOG: duration: 0.070 ms statement: SAVEPOINT "s140129009637120_x1"
    LOG: duration: 1.071 ms statement: INSERT INTO "blog_blog" ("author_id", "title") VALUES (4, 'test_title') RETURNING
    "blog_blog"."id"
    LOG: duration: 0.052 ms statement: RELEASE SAVEPOINT "s140129009637120_x1"
    LOG: duration: 2.085 ms statement: COMMIT

    View full-size slide

  81. 84
    Savepoint
    https://www.postgresql.jp/document/11/html/sql-savepoint.html
    SAVEPOINTは、現在のトランザクション内に
    新しいセーブポイントを設定します。
    セーブポイントとはトランザクション内に付ける特別な印です。
    セーブポイントを設定しておくと、それ以降に実⾏されたコマンドを全て
    ロールバックし、トランザクションを
    設定時の状態に戻すことができます。

    View full-size slide

  82. 85
    Savepoint
    https://www.postgresql.jp/document/11/html/sql-savepoint.html
    SAVEPOINTは、現在のトランザクション内に
    新しいセーブポイントを設定します。
    セーブポイントとはトランザクション内に付ける特別な印です。
    セーブポイントを設定しておくと、それ以降に実⾏されたコマンドを全て
    ロールバックし、トランザクションを
    設定時の状態に戻すことができます。
    トランザクションの中につくる⼦トランザクション
    みたいなもの

    View full-size slide

  83. 86
    Savepoint
    # django.db.transaction.Atomic
    class Atomic(ContextDecorator):
    def __enter__(self):
    connection = get_connection(self.using)
    if not connection.in_atomic_block:
    # Reset state when entering an outermost atomic block.
    connection.commit_on_exit = True
    connection.needs_rollback = False
    if not connection.get_autocommit():
    connection.in_atomic_block = True
    connection.commit_on_exit = False
    if connection.in_atomic_block:
    if self.savepoint and not connection.needs_rollback:
    sid = connection.savepoint()
    connection.savepoint_ids.append(sid)
    else:
    connection.savepoint_ids.append(None)
    else:
    connection.set_autocommit(False, force_begin_transaction_with_broken_autocommit=True)
    connection.in_atomic_block = True
    2回⽬以降に呼ばれたなら、savepointを発⾏
    その識別⼦をリスト(スタック)に保持していく

    View full-size slide

  84. 87
    Savepoint
    # django.db.backends.base.base.BaseDatabaseWrapper#savepoint
    def savepoint(self):
    if not self._savepoint_allowed():
    return
    thread_ident = _thread.get_ident()
    tid = str(thread_ident).replace('-', '')
    self.savepoint_state += 1
    sid = "s%s_x%d" % (tid, self.savepoint_state)
    self.validate_thread_sharing()
    self._savepoint(sid)
    return sid
    実⾏スレッドIDをもとにSavepointのIDを決める

    View full-size slide

  85. 88
    Savepoint
    # django.db.backends.base.base.BaseDatabaseWrapper#savepoint
    def savepoint(self):
    if not self._savepoint_allowed():
    return
    thread_ident = _thread.get_ident()
    tid = str(thread_ident).replace('-', '')
    self.savepoint_state += 1
    sid = "s%s_x%d" % (tid, self.savepoint_state)
    self.validate_thread_sharing()
    self._savepoint(sid)
    return sid
    savepointコマンドを発⾏する

    View full-size slide

  86. 89
    Djangoでのトランザクション管理
    def start_blog_with_nested_transaction_with_error(request):
    with transaction.atomic():
    author = Author.objects.create(name='test')
    with transaction.atomic():
    blog = Blog.objects.create(
    title='test_title',
    author=author,
    )
    raise Exception('my error')
    return HttpResponse(f'success create blog {blog.id}')

    View full-size slide

  87. 90
    Djangoでのトランザクション管理
    def start_blog_with_nested_transaction_with_error(request):
    with transaction.atomic():
    author = Author.objects.create(name='test')
    with transaction.atomic():
    blog = Blog.objects.create(
    title='test_title',
    author=author,
    )
    raise Exception('my error')
    return HttpResponse(f'success create blog {blog.id}')

    View full-size slide

  88. 91
    Djangoでのトランザクション管理
    def start_blog_with_nested_transaction_with_error(request):
    with transaction.atomic():
    author = Author.objects.create(name='test')
    with transaction.atomic():
    blog = Blog.objects.create(
    title='test_title',
    author=author,
    )
    raise Exception('my error')
    return HttpResponse(f'success create blog {blog.id}')

    View full-size slide

  89. 92
    Djangoでのトランザクション管理
    LOG: duration: 0.130 ms statement: BEGIN
    LOG: duration: 2.886 ms statement: INSERT INTO "blog_author" ("name") VALUES ('test') RETURNING
    "blog_author"."id"
    LOG: duration: 0.054 ms statement: SAVEPOINT "s139681396160256_x1"
    LOG: duration: 0.546 ms statement: INSERT INTO "blog_blog" ("author_id", "title") VALUES (5, 'test_title') RETURNING
    "blog_blog"."id"
    LOG: duration: 1.250 ms statement: ROLLBACK TO SAVEPOINT "s139681396160256_x1"
    LOG: duration: 0.045 ms statement: RELEASE SAVEPOINT "s139681396160256_x1"
    LOG: duration: 0.915 ms statement: ROLLBACK

    View full-size slide

  90. 93
    Djangoでのトランザクション管理
    LOG: duration: 0.130 ms statement: BEGIN
    LOG: duration: 2.886 ms statement: INSERT INTO "blog_author" ("name") VALUES ('test') RETURNING
    "blog_author"."id"
    LOG: duration: 0.054 ms statement: SAVEPOINT "s139681396160256_x1"
    LOG: duration: 0.546 ms statement: INSERT INTO "blog_blog" ("author_id", "title") VALUES (5, 'test_title') RETURNING
    "blog_blog"."id"
    LOG: duration: 1.250 ms statement: ROLLBACK TO SAVEPOINT "s139681396160256_x1"
    LOG: duration: 0.045 ms statement: RELEASE SAVEPOINT "s139681396160256_x1"
    LOG: duration: 0.915 ms statement: ROLLBACK

    View full-size slide

  91. 94
    Djangoでのトランザクション管理
    def start_blog_with_nested_transaction_with_error2(request):
    with transaction.atomic():
    author = Author.objects.create(name='test')
    try:
    with transaction.atomic():
    blog = Blog.objects.create(
    title='test_title',
    author=author,
    )
    raise Exception('my error')
    except:
    pass
    return HttpResponse(f'success create blog {blog.id}')

    View full-size slide

  92. 95
    Djangoでのトランザクション管理
    def start_blog_with_nested_transaction_with_error2(request):
    with transaction.atomic():
    author = Author.objects.create(name='test')
    try:
    with transaction.atomic():
    blog = Blog.objects.create(
    title='test_title',
    author=author,
    )
    raise Exception('my error')
    except:
    pass
    return HttpResponse(f'success create blog {blog.id}')

    View full-size slide

  93. 96
    Djangoでのトランザクション管理
    def start_blog_with_nested_transaction_with_error2(request):
    with transaction.atomic():
    author = Author.objects.create(name='test')
    try:
    with transaction.atomic():
    blog = Blog.objects.create(
    title='test_title',
    author=author,
    )
    raise Exception('my error')
    except:
    pass
    return HttpResponse(f'success create blog {blog.id}')
    try/exceptを追加

    View full-size slide

  94. 97
    Djangoでのトランザクション管理
    LOG: duration: 0.079 ms statement: BEGIN
    LOG: duration: 1.264 ms statement: INSERT INTO "blog_author" ("name") VALUES ('test') RETURNING
    "blog_author"."id"
    LOG: duration: 0.039 ms statement: SAVEPOINT "s140051465017088_x1"
    LOG: duration: 2.193 ms statement: INSERT INTO "blog_blog" ("author_id", "title") VALUES (6, 'test_title') RETURNING
    "blog_blog"."id"
    LOG: duration: 0.086 ms statement: ROLLBACK TO SAVEPOINT "s140051465017088_x1"
    LOG: duration: 0.038 ms statement: RELEASE SAVEPOINT "s140051465017088_x1"
    LOG: duration: 3.101 ms statement: COMMIT

    View full-size slide

  95. 98
    Djangoでのトランザクション管理
    LOG: duration: 0.079 ms statement: BEGIN
    LOG: duration: 1.264 ms statement: INSERT INTO "blog_author" ("name") VALUES ('test') RETURNING
    "blog_author"."id"
    LOG: duration: 0.039 ms statement: SAVEPOINT "s140051465017088_x1"
    LOG: duration: 2.193 ms statement: INSERT INTO "blog_blog" ("author_id", "title") VALUES (6, 'test_title') RETURNING
    "blog_blog"."id"
    LOG: duration: 0.086 ms statement: ROLLBACK TO SAVEPOINT "s140051465017088_x1"
    LOG: duration: 0.038 ms statement: RELEASE SAVEPOINT "s140051465017088_x1"
    LOG: duration: 3.101 ms statement: COMMIT
    ⼦トランザクションはROLLBACK
    全体はCOMMIT

    View full-size slide

  96. 99
    Djangoでのトランザクション管理
    # django.db.transaction.Atomic#__exit__
    def __exit__(self, exc_type, exc_value, traceback):
    connection = get_connection(self.using)
    :
    if connection.savepoint_ids:
    sid = connection.savepoint_ids.pop()
    :
    if connection.closed_in_transaction:
    .....
    elif exc_type is None and not connection.needs_rollback:
    .....
    else:
    connection.needs_rollback = False
    if connection.in_atomic_block:
    if sid is None:
    connection.needs_rollback = True
    else:
    :
    connection.savepoint_rollback(sid)

    View full-size slide

  97. 100
    Djangoでのトランザクション管理
    # django.db.transaction.Atomic#__exit__
    def __exit__(self, exc_type, exc_value, traceback):
    connection = get_connection(self.using)
    :
    if connection.savepoint_ids:
    sid = connection.savepoint_ids.pop()
    :
    if connection.closed_in_transaction:
    .....
    elif exc_type is None and not connection.needs_rollback:
    .....
    else:
    connection.needs_rollback = False
    if connection.in_atomic_block:
    if sid is None:
    connection.needs_rollback = True
    else:
    :
    connection.savepoint_rollback(sid)

    View full-size slide

  98. Djangoのトランザクション
    完全に理解した

    View full-size slide

  99. トランザクション周りの
    つらかった話(Extraステージ)

    View full-size slide

  100. MySQL バージョンアップの罠

    View full-size slide

  101. 104
    MySQLバージョンアップの罠
    ➡ Django 1.11
    ➡ ATOMIC_REQUEST: Trueで運⽤
    ➡ バックエンドはMySQL 5.6

    View full-size slide

  102. 105
    MySQLバージョンアップの罠
    ➡ Django 1.11
    ➡ ATOMIC_REQUEST: Trueで運⽤
    ➡ バックエンドはMySQL 5.6
    ロック周りがきつい

    View full-size slide

  103. 106
    MySQLバージョンアップの罠
    ➡ Django 1.11
    ➡ ATOMIC_REQUEST: Trueで運⽤
    ➡ バックエンドはMySQL 5.6
    ➡ READ COMMITTEDに変更

    View full-size slide

  104. 107
    MySQLバージョンアップの罠
    ➡ Django 1.11
    ➡ ATOMIC_REQUEST: Trueで運⽤
    ➡ バックエンドはMySQL 5.6
    ➡ READ COMMITTEDに変更
    ロックがだいぶましになった

    View full-size slide

  105. 108
    MySQLバージョンアップの罠
    ➡ Django 1.11
    ➡ ATOMIC_REQUEST: Trueで運⽤
    ➡ バックエンドはMySQL 5.7
    ➡ READ COMMITTEDに変更
    何も問題はない

    View full-size slide

  106. 109
    MySQLバージョンアップの罠
    ➡ Django 1.11
    ➡ ATOMIC_REQUEST: Trueで運⽤
    ➡ バックエンドはMySQL 8.0
    ➡ READ COMMITTEDに変更

    View full-size slide

  107. django.db.utils.OperationalError: (1193, "Unknown system variable 'TX_ISOLATION'")

    View full-size slide

  108. 突然の死

    View full-size slide

  109. 112
    MySQLバージョンアップの罠
    https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_tx_isolation

    View full-size slide

  110. 113
    MySQLバージョンアップの罠
    https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_tx_isolation
    トランザクションの変更パラメタが変わってた

    View full-size slide

  111. 114
    Djangoでのトランザクション管理
    # https://github.com/django/django/blob/stable/1.11.x/django/db/backends/mysql/base.py
    def init_connection_state(self):
    assignments = []
    :
    if self.isolation_level:
    assignments.append("TX_ISOLATION = '%s'" % self.isolation_level)
    if assignments:
    with self.cursor() as cursor:
    cursor.execute('SET ' + ', '.join(assignments))
    1.11でのMySQLの初期化

    View full-size slide

  112. 115
    Djangoでのトランザクション管理
    # https://github.com/django/django/blob/stable/2.2.x/django/db/backends/mysql/base.py
    def init_connection_state(self):
    assignments = []
    :
    if self.isolation_level:
    assignments.append('SET SESSION TRANSACTION ISOLATION LEVEL %s' % self.isolation_level.upper())
    if assignments:
    with self.cursor() as cursor:
    cursor.execute('; '.join(assignments))
    2.xでのMySQLの初期化

    View full-size slide

  113. 116
    MySQLバージョンアップの罠
    ➡ Django 2.1
    ➡ ATOMIC_REQUEST: Trueで運⽤
    ➡ バックエンドはMySQL 5.7
    ➡ READ COMMITTEDに変更

    View full-size slide

  114. 117
    MySQLバージョンアップの罠
    ➡ Django 2.1
    ➡ ATOMIC_REQUEST: Trueで運⽤
    ➡ バックエンドはMySQL 8.0
    ➡ READ COMMITTEDに変更

    View full-size slide

  115. ようやくDBの⼈権
    (with句, Window関数)
    確保

    View full-size slide

  116. Celeryとトランザクション

    View full-size slide

  117. 120
    Celeryとトランザクション
    @shared_task
    def heavy_celery_task(hoge_id):
    hoge = Hoge.objects.get(hoge_id)
    hoge.attr += 1
    hoge.save()
    def tasK_view(request):
    with transaction.atomic():
    hoge = Hoge.objects.create()
    heavy_celery_task.delay(hoge.id)
    do_something()
    return HttpResponse('success')

    View full-size slide

  118. 121
    Celeryとトランザクション
    @shared_task
    def heavy_celery_task(hoge_id):
    hoge = Hoge.objects.get(hoge_id)
    hoge.attr += 1
    hoge.save()
    def tasK_view(request):
    with transaction.atomic():
    hoge = Hoge.objects.create()
    heavy_celery_task.delay(hoge.id)
    do_something()
    return HttpResponse('success')
    celeryタスク
    View

    View full-size slide

  119. 何が起きるでしょう?

    View full-size slide

  120. 123
    Celeryとトランザクション
    DoesNotExist: Hoge matching query does not exist
    が、ランダムに発⽣する

    View full-size slide

  121. 124
    Celeryとトランザクション
    DoesNotExist: Hoge matching query does not exist
    でも、エラー後に探すとその⾏はある

    View full-size slide

  122. 126
    Celeryとトランザクション
    def tasK_view(request):
    with transaction.atomic():
    hoge = Hoge.objects.create()
    heavy_celery_task.delay(hoge.id)
    do_something()
    return HttpResponse('success')
    @shared_task
    def heavy_celery_task(hoge_id):
    hoge = Hoge.objects.get(hoge_id)
    hoge.attr += 1
    hoge.save()
    ⾮同期

    View full-size slide

  123. 127
    Celeryとトランザクション
    def tasK_view(request):
    with transaction.atomic():
    hoge = Hoge.objects.create()
    heavy_celery_task.delay(hoge.id)
    do_something()
    return HttpResponse('success')
    @shared_task
    def heavy_celery_task(hoge_id):
    hoge = Hoge.objects.get(hoge_id)
    hoge.attr += 1
    hoge.save()
    これなら問題ない

    View full-size slide

  124. 128
    Celeryとトランザクション
    def tasK_view(request):
    with transaction.atomic():
    hoge = Hoge.objects.create()
    heavy_celery_task.delay(hoge.id)
    @shared_task
    def heavy_celery_task(hoge_id):
    hoge = Hoge.objects.get(hoge_id)
    hoge.attr += 1
    hoge.save()
    do_something()
    return HttpResponse('success') ワーストケースで
    呼び出し元トランザクションが
    終わる前にCeleryTaskが動く

    View full-size slide

  125. transaction.on_commitを使う

    View full-size slide

  126. 130
    Celeryとトランザクション
    @shared_task
    def heavy_celery_task(hoge_id):
    hoge = Hoge.objects.get(hoge_id)
    hoge.attr += 1
    hoge.save()
    def tasK_view(request):
    with transaction.atomic():
    hoge = Hoge.objects.create()
    # heavy_celery_task.delay(hoge.id)
    transaction.on_commit(lambda: heavy_celery_task.delay(hoge.id))
    return HttpResponse(f'success')

    View full-size slide

  127. 131
    Celeryとトランザクション
    @shared_task
    def heavy_celery_task(hoge_id):
    hoge = Hoge.objects.get(hoge_id)
    hoge.attr += 1
    hoge.save()
    def tasK_view(request):
    with transaction.atomic():
    hoge = Hoge.objects.create()
    # heavy_celery_task.delay(hoge.id)
    transaction.on_commit(lambda: heavy_celery_task.delay(hoge.id))
    return HttpResponse(f'success')
    transactionのcommit時にceleryに
    タスクを投げることが保証される

    View full-size slide

  128. 132
    Celeryとトランザクション
    # django.db.backends.base.base.BaseDatabaseWrapper#on_commit
    def on_commit(self, func):
    if self.in_atomic_block:
    # Transaction in progress; save for execution on commit.
    self.run_on_commit.append((set(self.savepoint_ids), func))
    elif not self.get_autocommit():
    raise TransactionManagementError('on_commit() cannot be used in manual transaction management')
    else:
    # No transaction in progress and in autocommit mode; execute
    # immediately.
    func()

    View full-size slide

  129. 133
    Celeryとトランザクション
    # django.db.backends.base.base.BaseDatabaseWrapper#on_commit
    def on_commit(self, func):
    if self.in_atomic_block:
    # Transaction in progress; save for execution on commit.
    self.run_on_commit.append((set(self.savepoint_ids), func))
    elif not self.get_autocommit():
    raise TransactionManagementError('on_commit() cannot be used in manual transaction management')
    else:
    # No transaction in progress and in autocommit mode; execute
    # immediately.
    func()

    View full-size slide

  130. 135
    ➡ トランザクションの話
    ➡ 分離レベルとRDBMS毎の差の話
    ➡ transaction.atomic()
    ➡ トランザクション関連で⾟かった話

    View full-size slide