Python | DB・SQL:ACID

Python
スポンサーリンク

概要(ACID は「ちゃんとしたトランザクション」の 4 つの約束)

ACID は、データベースのトランザクションが「ちゃんとしている」ことを表す 4 つの性質の頭文字です。
Atomicity(原子性)、Consistency(一貫性)、Isolation(分離性)、Durability(永続性)の 4 つで ACID です。

一言でいうと、
「途中で壊れた状態を残さない」「ルールを守る」「同時に動いても変にならない」「確定したら消えない」
という約束のセットです。

言葉だけだと抽象的なので、ここでは「銀行の振り込み」や「在庫管理」を例にしながら、
一つずつ、初心者目線でかみ砕いていきます。


A:Atomicity(原子性)=「全部やるか、全部やらないか」

送金処理でイメージする原子性

一番イメージしやすいのが Atomicity(原子性)です。
これは、「トランザクションの中の処理は、全部成功するか、全部なかったことになるかのどちらか」という性質です。

銀行の振り込みを考えます。
A さんから B さんに 1000 円送るとき、データベースではだいたい次のような処理になります。

A さんの残高から 1000 円引く。
B さんの残高に 1000 円足す。

この 2 つはセットで意味があります。
もし「A から引くだけ成功して、B に足すところでエラーになった」らどうなるか。
A さんからは 1000 円消えたのに、B さんには届いていない、という「お金が消える」状態になります。

原子性は、これを絶対に許さないための性質です。
トランザクションの中で 2 つの UPDATE を実行し、最後に COMMIT しようとしたとき、
途中でエラーが起きたら、両方とも ROLLBACK されて「最初から何もなかったこと」に戻ります。

ここで重要なのは、「部分的な成功を残さない」ということです。
トランザクションの中の処理は、「運命共同体」になります。
全部通ったら反映、どれか一つでもダメなら全部取り消し。
これが Atomicity です。

Python でのイメージ(atomic ブロック)

Python(特に Django)だと、transaction.atomic() でこの原子性を表現できます。

例えば Django なら、次のようなコードになります。

from django.db import transaction

with transaction.atomic():
    a.balance -= 1000
    a.save()
    b.balance += 1000
    b.save()
Python

このブロックの中で例外が起きると、
このブロック内の変更は全部 ROLLBACK されます。
「一連の処理をひとまとめにして、全部やるか全部やらないか」にしているわけです。


C:Consistency(一貫性)=「ルールが壊れた状態を確定させない」

「データのルール」が守られている状態とは

Consistency(一貫性)は、「データベースに決めたルールが壊れないようにする」性質です。
ここでいうルールとは、例えば次のようなものです。

残高はマイナスになってはいけない。
メールアドレスは重複してはいけない。
外部キーで参照している先のレコードが必ず存在する。

これらは、データベースの制約(NOT NULL、UNIQUE、CHECK、外部キー制約など)やアプリ側のロジックで定義されます。

一貫性とは、「トランザクションの前後で、これらのルールが守られていること」を意味します。
トランザクションの途中で一時的にルールが崩れても、最終的に COMMIT されるときには、
ルールが守られた状態に戻っている必要があります。

ルールが壊れたらどうなるか

例えば、「残高は 0 以上」という CHECK 制約があるとします。
トランザクションの中で、残高をマイナスにしようとすると、
データベースはエラーを返し、そのトランザクションは ROLLBACK されます。

これにより、「ルールが壊れた状態」が確定されることはありません。
一貫性は、「データベースが持つルールを破る変更は、そもそも確定できない」という性質です。

ここで大事なのは、Consistency は「トランザクションだけの話」ではなく、
制約やアプリのロジックとセットで成り立つ、ということです。
トランザクションは、「ルールを破る変更が途中で紛れ込んでも、最終的に COMMIT されないようにする」役割を担っています。


I:Isolation(分離性)=「同時に動いても、お互いの中途半端な状態を見ない」

同時実行で何が問題になるか

Isolation(分離性)は、複数のトランザクションが同時に動いているときの話です。
現実のアプリでは、ユーザーは一人ではありません。
同時に何人もが操作します。

例えば、次のような状況を考えます。

トランザクション T1:A さんが B さんに 1000 円送金中。
トランザクション T2:B さんが自分の残高を確認中。

T1 の中では、
A から 1000 円引く UPDATE
B に 1000 円足す UPDATE
が順番に行われます。

もし T2 が、「A からは引かれたけど B にはまだ入っていない」という中途半端なタイミングで残高を読んでしまったら、
B さんの残高が「本来とは違う値」に見えてしまいます。

Isolation は、こうした「中途半端な状態」を他のトランザクションから見えにくくする性質です。

「まるで一人ずつ順番に実行されたかのように見せる」

理想的には、複数のトランザクションが同時に動いていても、
外から見ると「どれか一つずつ順番に実行されたように見える」状態が望ましいです。

実際のデータベースでは、
ロックや分離レベル(READ COMMITTED、REPEATABLE READ、SERIALIZABLE など)を使って、
どこまで分離するかを調整します。

初心者のうちは、細かい分離レベルの違いまでは覚えなくて大丈夫です。
まずは、「Isolation は、同時に動いても変な矛盾が起きないようにするための性質」と理解しておけば十分です。

例えば、Django のデフォルト設定では、
多くの RDBMS で READ COMMITTED という分離レベルが使われます。
これは、「他のトランザクションがまだ COMMIT していない変更は見えない」というレベルです。
つまり、「確定していない中途半端な変更」は見えないようになっています。


D:Durability(永続性)=「COMMIT されたら、ちゃんと残る」

COMMIT の「重さ」をイメージする

Durability(永続性)は、「トランザクションが COMMIT されたら、その変更はちゃんと残る」という性質です。
ここでいう「残る」とは、単にメモリ上にあるだけでなく、
ディスクなどの永続ストレージに書き込まれていて、
サーバーが落ちても消えない、という意味です。

例えば、送金処理が終わって COMMIT された直後に、
サーバーがクラッシュしたとします。
再起動したときに、「送金前の状態に戻っていたら」困りますよね。
Durability は、「COMMIT された変更はちゃんと生き残る」ことを保証します。

データベース内部で何が起きているか(ざっくりイメージ)

実際の RDBMS は、
トランザクションログ(WAL:Write-Ahead Log など)に変更内容を書き出し、
それをディスクにフラッシュしてから COMMIT を完了させる、
といった仕組みで Durability を実現しています。

初心者のうちは、内部実装の細かいところまでは気にしなくて大丈夫です。
大事なのは、「COMMIT が成功したら、その変更は“ちゃんとどこかに書かれていて、消えない”」という感覚です。


4 つをまとめて「トランザクションらしさ」として捉える

送金処理に ACID を当てはめてみる

もう一度、A さんから B さんへの送金を例にして、ACID をまとめてみます。

Atomicity(原子性)
A から引く、B に足す、という 2 つの UPDATE は「全部成功するか全部失敗するか」。
途中でエラーが起きたら、どちらもなかったことになる。

Consistency(一貫性)
残高がマイナスにならない、合計金額が合う、などのルールが壊れた状態は COMMIT されない。
制約やロジックと組み合わさって、「おかしなデータ」が確定されない。

Isolation(分離性)
他のトランザクションからは、「送金処理の途中の中途半端な状態」が見えない。
まるで送金処理が一瞬で終わったかのように見える。

Durability(永続性)
COMMIT が成功したら、その送金結果はディスクに書かれていて、サーバーが落ちても消えない。

こうやって見ると、ACID はバラバラの概念ではなく、
「トランザクションをちゃんと動かすための 4 つの側面」だと分かります。

なぜ ACID を意識する必要があるのか

実務でアプリを作るとき、
「トランザクションを使うべきかどうか」
「どこまで分離性が必要か」
「どんな制約を張るか」

といった設計をするときに、
頭の中に ACID のイメージがあるかどうかで、
判断の質が変わります。

例えば、
「ここは途中で失敗したら困るから、atomic で包もう」
「ここは同時実行で競合しそうだから、ロックや分離レベルを考えよう」
「ここは制約を張って、一貫性をデータベース側でも守ろう」

といった発想が自然に出てくるようになります。


Python(Django)で ACID を意識したコードを書くイメージ

Django の transaction.atomic と ACID

Django を例にすると、transaction.atomic() は、
ACID のうち特に Atomicity と Isolation を意識した仕組みです。

例えば、注文処理を Django で書くとします。

from django.db import transaction

def create_order(user, items):
    with transaction.atomic():
        order = Order.objects.create(user=user)
        total = 0
        for item in items:
            OrderItem.objects.create(
                order=order,
                product=item.product,
                quantity=item.quantity,
                price=item.product.price,
            )
            item.product.stock -= item.quantity
            item.product.save()
            total += item.product.price * item.quantity

        order.total_amount = total
        order.save()
Python

この中でエラーが起きたら、
Order も OrderItem も stock の更新も、全部 ROLLBACK されます。
これが Atomicity です。

データベース側には、
外部キー制約や NOT NULL、CHECK などを張っておけば、
Consistency も高められます。

Isolation と Durability は、
主にデータベースの設定や実装側の話ですが、
アプリ側は「トランザクションを適切な粒度で使う」ことで、
それらの性質を活かしやすくなります。


まとめ(ACID は「壊れた世界を残さないための 4 本柱」)

ACID を初心者目線でまとめると、こうなります。

Atomicity(原子性)は、「一連の処理は全部成功か全部失敗か。途中の中途半端な成功は残さない」という約束。
Consistency(一貫性)は、「データベースに決めたルール(制約)が壊れた状態を確定させない」という約束。
Isolation(分離性)は、「複数のトランザクションが同時に動いても、お互いの中途半端な状態を見て変な矛盾が起きないようにする」という約束。
Durability(永続性)は、「COMMIT された変更はちゃんとディスクに書かれ、サーバーが落ちても消えない」という約束。

この 4 つがそろっているからこそ、
お金、在庫、ポイント、予約などの「壊れたら困るデータ」を、
安心して RDB に任せられます。

タイトルとURLをコピーしました