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

7d46f2037fed74f249a4c85e8635da7d?s=47 Denzow
May 18, 2019

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

Django Congress2019 での資料です。

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

7d46f2037fed74f249a4c85e8635da7d?s=128

Denzow

May 18, 2019
Tweet

Transcript

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

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

  3. 3 ➡ でんぞう (@denzowill, denzow) ➡ Python歴 6年 (仕事で1.5年) ➡

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

    scouty,inc シニアエンジニア(前職はDBサポートエンジニア) ➡ DBスペシャリスト(PostgreSQLが好き、•racleは得意だけど苦⼿) お前誰よ? LAPRAS
  5. ͋ΒΏΔࣄ৅ΛඞવԽ͠ɺ ੈͷதͷϛεϚονΛͳ͘͢ ݸਓ͕࣋ͭ͞·͟·ͳ࠽ೳͱɺੈքͱͷ઀఺͸ແݶʹ͋Γ·͢ɻ ఱ৬ͱͷग़͍͋ͷΑ͏ʹɺʮϛεϚονͷ઀఺ʯΛഉআ͠ɺʮ࠷దͳ઀఺ʯͱ઀ଓͰ͖Ε͹ɺ ਓʑ͸ྗΛ࠷େݶʹൃش͠ɺΑΓ͍͖͍͖ͱͨ͠ঢ়ଶΛ࣮ݱͰ͖Δ͸ͣͰ͢ɻ ͋ͳͨͷݸੑΛٻΊΔਓ͕ݱΕΔͷ΋ɺ-"13"4ͳΒܾͯ͠ۮવͰ͸͋Γ·ͤΜɻ -"13"4͸"*ٕज़Λ΋ͱʹɺ͋ΒΏΔਓΛج఺ʹ৘ใΛ࠶ߏங͠ɺ ৽͍͠αʔϏεΛ૑଄͠·͢ɻ M I

    S S I O N
  6. https://startpython.connpass.com/

  7. Python と Django と

  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)
  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)
  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)
  11. 11 Django と scouty(2018/5) ➡ Django 1.11 ➡ Django Celery

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

    ➡ Django でDDD的な設計 ➡ Django Channels でのWebsocket ➡ daphneでの本番運⽤
  13. https://speakerdeck.com/denzow/imasarazhen-rifan-ru-django-migration

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

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

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

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

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

  20. 20 本題

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

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

  23. 今⽇話すこと 23 ➡Djangoのトランザクション周りのソースを
 読んで知的好奇⼼を満たす ➡そのcommit誰に⾒えるの?でやらかさないために

  24. 24 ➡ そもそもトランザクション ➡ RDBMSでのトランザクション ➡ RDBMS毎の違い ➡ Djangoのトランザクション ➡

    トランザクション周りの⾟かった話 アジェンダ
  25. そもそもトランザクション

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

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

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

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

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

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

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

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

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

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

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

    発⽣する READ COMMITTED 発⽣しない 発⽣する 発⽣する REPETABLE READ 発⽣しない 発⽣しない 発⽣する SERIALIZABLE 発⽣しない 発⽣しない 発⽣しない
  37. 37 ダーティーリード BEGIN; BEGIN; INSERT INTO A...; SELECT ... FROM

    A; 未commitの INSERT結果が⾒える
  38. 38 ファジーリード BEGIN; BEGIN; UPDATE A... WHER ID=1; SELECT ...

    FROM A; COMMIT; SELECT ... FROM A; さっきとレコードの 内容が違う
  39. 39 ファントムリード BEGIN; BEGIN; INSERT INTO A...; SELECT ... FROM

    A; COMMIT; SELECT ... FROM A; ⾏が増えてる
  40. 40 トランザクション分離レベル 分離レベル ダーティーリード ファジーリード ファントムリード READ UNCOMMITTED 発⽣する 発⽣する

    発⽣する READ COMMITTED 発⽣しない 発⽣する 発⽣する REPETABLE READ 発⽣しない 発⽣しない 発⽣する SERIALIZABLE 発⽣しない 発⽣しない 発⽣しない
  41. RDMSごとの実装の違い

  42. 42 RDBMSの種類は様々

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

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

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

  46. 46 分離レベル ダーティーリード ファジーリード ファントムリード READ UNCOMMITTED 発⽣する 発⽣する 発⽣する

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

    READ COMMITTED 発⽣しない 発⽣する 発⽣する REPETABLE READ 発⽣しない 発⽣しない 発⽣しない SERIALIZABLE 発⽣しない 発⽣しない 発⽣しない MySQLのREPETABLE READ
  48. 48 MySQLのREPETABLE READ 最強

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

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

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

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

  53. ようやく Djangoの話

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

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

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

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

    AutoCommit + READ COMMITTED
  58. 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側のデフォルト値になる
  59. 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を設定する
  60. 60 2.0からのMySQLの初期化 https://docs.djangoproject.com/en/2.2/ref/databases/#mysql-isolation-level

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

  62. 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 指定されてなければ サーバ⾃体のデフォルト値を使う
  63. 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, } } }
  64. Djangoでの トランザクション管理

  65. 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}')
  66. 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}')
  67. 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
  68. 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}')
  69. 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で囲まれており トランザクションとして処理されている
  70. transaction.atomic()

  71. 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)
  72. 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 普通に呼び出した場合
  73. 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}')
  74. 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
  75. 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にする
  76. 76 transaction.atomic() DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql_psycopg2', 'NAME':

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

    'djangodb', 'USER': 'hoge', 'PASSWORD': 'password', 'HOST': 'localhost', 'PORT': '5432', 'ATOMIC_REQUESTS': True, } }
  78. 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()で全体をラップして処理
  79. 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}')
  80. 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}')
  81. 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
  82. 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
  83. savepoint?

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

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

    みたいなもの
  86. 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を発⾏ その識別⼦をリスト(スタック)に保持していく
  87. 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を決める
  88. 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コマンドを発⾏する
  89. 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}')
  90. 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}')
  91. 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}')
  92. 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
  93. 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
  94. 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}')
  95. 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}')
  96. 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を追加
  97. 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
  98. 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
  99. 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)
  100. 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)
  101. Djangoのトランザクション 完全に理解した

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

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

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

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

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

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

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

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

    8.0 ➡ READ COMMITTEDに変更
  110. django.db.utils.OperationalError: (1193, "Unknown system variable 'TX_ISOLATION'")

  111. 突然の死

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

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

  114. 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の初期化
  115. 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の初期化
  116. 116 MySQLバージョンアップの罠 ➡ Django 2.1 ➡ ATOMIC_REQUEST: Trueで運⽤ ➡ バックエンドはMySQL

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

    8.0 ➡ READ COMMITTEDに変更
  118. ようやくDBの⼈権 (with句, Window関数) 確保

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

  120. 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')
  121. 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
  122. 何が起きるでしょう?

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

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

  125. None
  126. 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() ⾮同期
  127. 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() これなら問題ない
  128. 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が動く
  129. transaction.on_commitを使う

  130. 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')
  131. 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に タスクを投げることが保証される
  132. 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()
  133. 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()
  134. まとめ

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