Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

https://startpython.connpass.com/

Slide 7

Slide 7 text

Python と Django と

Slide 8

Slide 8 text

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)

Slide 9

Slide 9 text

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)

Slide 10

Slide 10 text

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)

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

No content

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

20 本題

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

22 このコードに潜りたい

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

RDMSでのトランザクション

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

RDMSごとの実装の違い

Slide 42

Slide 42 text

42 RDBMSの種類は様々

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

48 MySQLのREPETABLE READ 最強

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

Djangoのトランザクション

Slide 53

Slide 53 text

ようやく Djangoの話

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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側のデフォルト値になる

Slide 59

Slide 59 text

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を設定する

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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 指定されてなければ サーバ⾃体のデフォルト値を使う

Slide 63

Slide 63 text

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, } } }

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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}')

Slide 66

Slide 66 text

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}')

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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}')

Slide 69

Slide 69 text

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で囲まれており トランザクションとして処理されている

Slide 70

Slide 70 text

transaction.atomic()

Slide 71

Slide 71 text

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)

Slide 72

Slide 72 text

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 普通に呼び出した場合

Slide 73

Slide 73 text

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}')

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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にする

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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()で全体をラップして処理

Slide 79

Slide 79 text

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}')

Slide 80

Slide 80 text

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}')

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

savepoint?

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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を発⾏ その識別⼦をリスト(スタック)に保持していく

Slide 87

Slide 87 text

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を決める

Slide 88

Slide 88 text

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コマンドを発⾏する

Slide 89

Slide 89 text

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}')

Slide 90

Slide 90 text

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}')

Slide 91

Slide 91 text

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}')

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

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}')

Slide 95

Slide 95 text

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}')

Slide 96

Slide 96 text

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を追加

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

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)

Slide 100

Slide 100 text

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)

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

MySQL バージョンアップの罠

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

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

Slide 107

Slide 107 text

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

Slide 108

Slide 108 text

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

Slide 109

Slide 109 text

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

Slide 110

Slide 110 text

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

Slide 111

Slide 111 text

突然の死

Slide 112

Slide 112 text

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

Slide 113

Slide 113 text

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

Slide 114

Slide 114 text

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の初期化

Slide 115

Slide 115 text

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の初期化

Slide 116

Slide 116 text

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

Slide 117

Slide 117 text

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

Slide 118

Slide 118 text

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

Slide 119

Slide 119 text

Celeryとトランザクション

Slide 120

Slide 120 text

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')

Slide 121

Slide 121 text

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

Slide 122

Slide 122 text

何が起きるでしょう?

Slide 123

Slide 123 text

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

Slide 124

Slide 124 text

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

Slide 125

Slide 125 text

No content

Slide 126

Slide 126 text

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() ⾮同期

Slide 127

Slide 127 text

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() これなら問題ない

Slide 128

Slide 128 text

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が動く

Slide 129

Slide 129 text

transaction.on_commitを使う

Slide 130

Slide 130 text

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')

Slide 131

Slide 131 text

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に タスクを投げることが保証される

Slide 132

Slide 132 text

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()

Slide 133

Slide 133 text

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()

Slide 134

Slide 134 text

まとめ

Slide 135

Slide 135 text

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