BUILDING TO SCALE
FR | EN
Parler de votre projet

Les transactions de base de données dans Rails : ce que signifie vraiment ACID pour votre code

ActiveRecord rend les transactions très faciles à utiliser. Ce que promet réellement chacune des quatre garanties ACID, et où les développeurs Rails se trompent sur l'isolation.

Publié le 17 octobre 2021 · 5 min de lecture

ActiveRecord rend les transactions si faciles à utiliser qu'il vaut la peine de prendre du recul et d'être précis sur ce qu'une transaction garantit réellement, et sur ce qu'elle ne garantit très délibérément pas.

Quatre lettres, quatre promesses distinctes

ATOMIQUE Toutes les instructions de la transaction sont validées, ou aucune ne l'est. COHÉRENT Les contraintes sont vraies avant et après, jamais en cours de transaction. ISOLÉ Les transactions concurrentes ne voient pas les écritures non validées des autres. DURABLE Une fois validée, une écriture survit à un crash survenant juste après.
Les quatre garanties que votre base de données vous donne réellement

Où les développeurs Rails se trompent sur l'isolation

Le niveau d'isolation par défaut de Postgres, read committed, est plus faible que ce que la plupart des ingénieurs supposent. Deux transactions peuvent lire la même ligne, décider toutes les deux qu'il est sûr de la mettre à jour, et valider toutes les deux, la seconde écriture écrasant silencieusement l'intention de la première. Cela se manifeste comme une race condition dans des cas comme la décrémentation d'un stock ou le crédit du solde d'un portefeuille.

# Racy: two requests can both pass the check before either updates
if account.balance_cents >= amount_cents
  account.update!(balance_cents: account.balance_cents - amount_cents)
end

# Safe: the row lock serializes concurrent updates
ActiveRecord::Base.transaction do
  account = Account.lock.find(account.id)
  raise InsufficientFunds if account.balance_cents < amount_cents
  account.update!(balance_cents: account.balance_cents - amount_cents)
end

La cohérence est aussi le travail de vos callbacks, pas seulement celui de la base de données

Le « C » de ACID renvoie aux contraintes au niveau de la base de données, pas aux règles métier de votre application. Une validation numérique contournée par une mise à jour brute d'une colonne laisse vos données techniquement validées et pratiquement fausses. Appuyez-vous sur les contraintes de la base, NOT NULL, clés étrangères, check constraints, pour tout ce qui ne doit jamais être violé, et considérez les validations de modèle comme une seconde couche, pas comme la seule.

La durabilité a un coût qu'il vaut mieux connaître

Chaque commit qui retourne avec succès à votre processus Rails a, par défaut, été écrit sur disque de manière à survivre à un crash. Cette garantie est ce qui rend relativement coûteux le fait de valider à chaque écriture à fort volume, et c'est pourquoi regrouper les écritures en moins de transactions, plus grosses, est souvent la première vraie amélioration de performance sur une table très sollicitée, bien avant d'ajouter du matériel.