ACID Isolation & Lock Contention: Mitigating Deadlocks, Dirty Reads, and Phantom Reads with SELECT … FOR UPDATE

Introduction

Every application that writes to a relational database is, whether the team realizes it or not, in a constant negotiation with concurrency. Two checkout requests hit the same inventory row at the same millisecond. A reporting job reads a table while an order pipeline updates it. A batch job and an API request both try to lock the same customer record. None of this is a bug — it's simply what happens when a database serves more than one client at a time.

The tools relational databases give you to manage this negotiation are transaction isolation levels and explicit row locking, most commonly expressed through SELECT ... FOR UPDATE. Used well, they eliminate entire categories of bugs — dirty reads, lost updates, phantom rows — before they ever reach production. Used carelessly, they produce the exact opposite: deadlocks, timeouts, and mysterious "works on my machine" data corruption that only appears under real traffic.

This article walks through how isolation levels and row-level locking actually behave under the hood in PostgreSQL and MySQL/InnoDB, why the classic concurrency anomalies happen, and how to design transactions that avoid them — with concrete SELECT ... FOR UPDATE patterns you can apply directly to your schema.

A Quick Refresher: What "Isolation" Actually Means in ACID