Loading content...
Black Friday, 2023. An e-commerce platform shows a hot product with '1 unit left in stock.' Fifty thousand users click 'Buy Now' within the same second.
Without proper concurrency control, the result would be chaos:
This scenario illustrates why concurrent access control is critical. Modern databases serve thousands of simultaneous users, all reading and writing shared data. Without sophisticated mechanisms to coordinate this access, data corruption, lost updates, and inconsistent reads would be constant.
Concurrent access is the ability of a DBMS to handle multiple users or applications accessing the database simultaneously while maintaining data consistency and integrity. It's one of the most technically complex—and most essential—capabilities of modern database systems.
By the end of this page, you will understand concurrency problems, locking mechanisms, isolation levels, and how DBMS coordinates simultaneous access. You'll learn why concurrent access was impossible with file-based systems and how modern databases achieve it transparently.
When multiple transactions access shared data simultaneously, several problems can occur if access isn't properly coordinated. These concurrency anomalies can corrupt data or provide incorrect results to users.
Why does this happen?
Database operations aren't instantaneous. Reading a row, computing a value, and writing back takes time—microseconds to milliseconds. In that window, another transaction can interfere. Without coordination, both transactions operate on stale data, and one overwrites the other's changes.
| Anomaly | Description | Example | Consequence |
|---|---|---|---|
| Lost Update | Two transactions read same value; both update; one overwrites the other | Two clerks read balance $100, both add $50. Final: $150 not $200. | Data permanently incorrect. Money lost. |
| Dirty Read | Reading uncommitted data that may be rolled back | Read balance $150 during transfer, but transfer fails and rolls back to $100. | Decisions based on data that never existed. |
| Non-Repeatable Read | Same query returns different values within one transaction | Read price $99. Someone updates to $129. Read price again: $129. | Inconsistent logic within single operation. |
| Phantom Read | Query result set changes as other transactions insert/delete | Count orders: 100. Another insert happens. Count again: 101. | Aggregates and reports inconsistent. |
Concurrency bugs are notoriously difficult to detect. They depend on precise timing—millisecond differences in when operations execute. A system might work perfectly for months, then fail under peak load when timing windows align. This is why built-in DBMS concurrency control is essential; you can't reliably test your way to correctness.
The traditional approach to concurrency control is locking. Before a transaction can access data, it must acquire a lock. The lock prevents conflicting access by other transactions until the holding transaction completes.
Lock Types:
| Requesting → | S (Shared) | X (Exclusive) |
|---|---|---|
| S (Shared) held | ✅ Granted | ❌ Wait |
| X (Exclusive) held | ❌ Wait | ❌ Wait |
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
-- Explicit locking to prevent lost update -- SESSION 1: Teller A deposits $200BEGIN TRANSACTION; -- Acquire exclusive lock on the rowSELECT Balance FROM Accounts WHERE AccountID = 1001 FOR UPDATE;-- Returns: Balance = $1,000-- Row is now exclusively locked - Teller B must wait UPDATE Accounts SET Balance = Balance + 200 WHERE AccountID = 1001; COMMIT;-- Lock released. Teller B can now proceed. -- SESSION 2: Teller B deposits $300 (executes concurrently)BEGIN TRANSACTION; -- Attempt to acquire lock... WAITS here until Session 1 commitsSELECT Balance FROM Accounts WHERE AccountID = 1001 FOR UPDATE;-- After Session 1 commits, returns: Balance = $1,200 (the updated value!) UPDATE Accounts SET Balance = Balance + 300 WHERE AccountID = 1001; COMMIT;-- Final balance: $1,500 (correct!) -- Different lock levels: -- Shared lock (for read-only access)SELECT * FROM Accounts WHERE AccountID = 1001 FOR SHARE;-- Others can also read, but no one can write -- Exclusive lock (for read-then-write)SELECT * FROM Accounts WHERE AccountID = 1001 FOR UPDATE;-- No one else can read or write -- Skip locked rows (don't wait)SELECT * FROM Accounts FOR UPDATE SKIP LOCKED;-- Process only unlocked rows - useful for job queues -- No wait (fail immediately if locked)SELECT * FROM Accounts WHERE AccountID = 1001 FOR UPDATE NOWAIT;-- Throws error immediately if row is lockedLocks can be acquired at different granularities: row-level (finest, most concurrent), page-level, table-level (coarsest, least concurrent). Modern DBMS typically use row-level locking by default, escalating to page or table locks if too many individual row locks are held.
Locking introduces a new problem: deadlock. A deadlock occurs when two or more transactions are each waiting for locks held by the other, creating a circular dependency where none can proceed.
Example:
| Strategy | How It Works | Trade-offs |
|---|---|---|
| Deadlock Detection | DBMS periodically checks for cycles. Kills one transaction to break cycle. | Standard approach. Victim transaction must retry. |
| Deadlock Prevention | Enforce ordering: all transactions must lock resources in same order. | No deadlocks possible, but requires careful programming. |
| Lock Timeouts | If lock not acquired within timeout, abort transaction. | Simple but may abort transactions that would succeed. |
| Wait-Die / Wound-Wait | Older transactions get priority. Younger ones abort/wait based on scheme. | Avoids indefinite waiting but may abort many transactions. |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
-- DEADLOCK SCENARIO -- SESSION 1BEGIN TRANSACTION;UPDATE Accounts SET Balance = Balance - 100 WHERE AccountID = 1001;-- Acquired X-lock on Account 1001 -- SESSION 2 (concurrent)BEGIN TRANSACTION;UPDATE Accounts SET Balance = Balance - 200 WHERE AccountID = 1002;-- Acquired X-lock on Account 1002 -- SESSION 1 (continues)UPDATE Accounts SET Balance = Balance + 100 WHERE AccountID = 1002;-- WAITS: Session 2 holds lock on Account 1002 -- SESSION 2 (continues)UPDATE Accounts SET Balance = Balance + 200 WHERE AccountID = 1001;-- WAITS: Session 1 holds lock on Account 1001-- DEADLOCK! Both waiting for each other. -- DBMS detects deadlock and kills one transaction:-- "ERROR: deadlock detected-- DETAIL: Process 12345 waits for ShareLock on transaction 12346;-- Process 12346 waits for ShareLock on transaction 12345.-- HINT: See server log for query details." -- PREVENTION: Access resources in consistent order-- Always lock accounts in ascending AccountID order -- SESSION 1 (correct)BEGIN TRANSACTION;SELECT * FROM Accounts WHERE AccountID IN (1001, 1002) ORDER BY AccountID FOR UPDATE; -- Locks both in orderUPDATE Accounts SET Balance = Balance - 100 WHERE AccountID = 1001;UPDATE Accounts SET Balance = Balance + 100 WHERE AccountID = 1002;COMMIT; -- SESSION 2 (correct)BEGIN TRANSACTION;SELECT * FROM Accounts WHERE AccountID IN (1001, 1002) ORDER BY AccountID FOR UPDATE; -- Waits for Session 1 to completeUPDATE Accounts SET Balance = Balance - 200 WHERE AccountID = 1002;UPDATE Accounts SET Balance = Balance + 200 WHERE AccountID = 1001;COMMIT; -- No deadlock possible: consistent ordering prevents cyclesDeadlocks are normal in concurrent systems—not bugs per se. The DBMS handles them automatically by killing a victim transaction. Your application must be prepared to retry aborted transactions. Design for idempotency and make retry logic part of your error handling.
Full isolation (where every transaction appears to run alone) is expensive. It requires extensive locking, reducing concurrency and throughput. Isolation levels provide a spectrum of trade-offs between consistency and performance.
SQL standard defines four isolation levels, from least to most restrictive:
| Level | Dirty Read | Non-Repeatable Read | Phantom Read | Performance |
|---|---|---|---|---|
| Read Uncommitted | ❌ Possible | ❌ Possible | ❌ Possible | Fastest |
| Read Committed | ✅ Prevented | ❌ Possible | ❌ Possible | Fast |
| Repeatable Read | ✅ Prevented | ✅ Prevented | ❌ Possible | Moderate |
| Serializable | ✅ Prevented | ✅ Prevented | ✅ Prevented | Slowest |
12345678910111213141516171819202122232425262728293031323334353637383940414243
-- Setting isolation levels -- Per-transaction (PostgreSQL)BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;-- orSET TRANSACTION ISOLATION LEVEL READ COMMITTED; -- Session-wide (PostgreSQL)SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL REPEATABLE READ; -- READ COMMITTED behavior (default)BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED;SELECT salary FROM Employees WHERE id = 100; -- Returns $50,000 -- Meanwhile, another transaction updates and commits:-- UPDATE Employees SET salary = 55000 WHERE id = 100; COMMIT; SELECT salary FROM Employees WHERE id = 100; -- Returns $55,000 (new value!)COMMIT; -- REPEATABLE READ behaviorBEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;SELECT salary FROM Employees WHERE id = 100; -- Returns $50,000 -- Meanwhile, another transaction updates and commits:-- UPDATE Employees SET salary = 55000 WHERE id = 100; COMMIT; SELECT salary FROM Employees WHERE id = 100; -- Still returns $50,000!COMMIT; -- SERIALIZABLE example: Preventing lost updates automaticallyBEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;SELECT balance INTO @bal FROM Accounts WHERE id = 1001; -- $1,000-- Application computes: @bal + 200 -- Meanwhile, another SERIALIZABLE transaction also reads $1,000 and -- tries to update based on that... DBMS will abort one transaction! UPDATE Accounts SET balance = @bal + 200 WHERE id = 1001;COMMIT; -- May fail if conflict detected: "could not serialize access"Start with Read Committed (the default). Move to Repeatable Read for reports or algorithms requiring internal consistency. Use Serializable only for critical operations where you need absolute correctness. Never use Read Uncommitted unless you fully understand the implications.
Modern databases like PostgreSQL, MySQL (InnoDB), Oracle, and SQL Server implement Multi-Version Concurrency Control (MVCC) as an alternative to pure locking. MVCC provides readers don't block writers, writers don't block readers—dramatically improving concurrency.
How MVCC Works:
Instead of overwriting data in place, MVCC creates new versions of rows. Each transaction sees a consistent snapshot based on when it started. Older versions are retained until no transaction needs them.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
-- MVCC in action: Readers don't block writers -- SESSION 1 (long-running report)BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;SELECT SUM(balance) FROM Accounts; -- Returns $1,000,000-- Takes 30 seconds to process... -- SESSION 2 (concurrent update)BEGIN TRANSACTION;UPDATE Accounts SET balance = balance + 1000 WHERE AccountID = 1001;COMMIT; -- Succeeds immediately! Not blocked by Session 1. -- SESSION 1 (continues)SELECT SUM(balance) FROM Accounts; -- Still returns $1,000,000!-- MVCC provides consistent snapshot from transaction startCOMMIT; -- SESSION 3 (new transaction)BEGIN TRANSACTION;SELECT SUM(balance) FROM Accounts; -- Returns $1,001,000 (sees Session 2's update)COMMIT; -- MVCC and update conflicts-- When two writers touch the same row: -- SESSION ABEGIN TRANSACTION;UPDATE Accounts SET balance = 1000 WHERE AccountID = 5; -- SESSION B BEGIN TRANSACTION;UPDATE Accounts SET balance = 2000 WHERE AccountID = 5; -- WAITS: Session A has uncommitted update to same row -- SESSION ACOMMIT; -- SESSION B (now proceeds)-- Depending on isolation level:-- READ COMMITTED: Sees Session A's commit, updates to 2000-- SERIALIZABLE: May abort with serialization failure -- PostgreSQL: Viewing row versions (ctid is physical row location)SELECT ctid, xmin, xmax, * FROM Accounts WHERE AccountID = 1001;-- ctid: physical location (page, tuple)-- xmin: transaction ID that created this version-- xmax: transaction ID that deleted/updated (0 = current version)MVCC is so successful that virtually all modern relational databases use it. PostgreSQL, Oracle, MySQL InnoDB, SQL Server (with READ_COMMITTED_SNAPSHOT), and even non-relational databases like MongoDB and CockroachDB implement MVCC. It's the foundation of scalable concurrent database access.
File-based systems had primitive concurrency support, if any. The challenges that DBMS solves automatically were enormous manual burdens in file-based architectures.
The inability of file-based systems to handle concurrent access was a primary driver of DBMS adoption. Modern databases handle thousands of concurrent transactions per second with fine-grained locking, automatic deadlock resolution, and configurable isolation—all transparently.
Understanding concurrency control theory is important, but applying it correctly in practice requires awareness of common patterns and pitfalls.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
-- OPTIMISTIC LOCKING: No upfront locks, check before commit CREATE TABLE Products ( ProductID INT PRIMARY KEY, Name VARCHAR(100), Price DECIMAL(10,2), Stock INT, Version INT DEFAULT 1 -- Version counter for optimistic locking); -- Read product (no lock)SELECT ProductID, Name, Price, Stock, Version FROM Products WHERE ProductID = 100;-- Returns: ProductID=100, Name='Widget', Price=29.99, Stock=50, Version=7 -- Application processes... (could be long, no lock held) -- Update with version checkUPDATE Products SET Stock = 45, Version = Version + 1WHERE ProductID = 100 AND Version = 7; -- The version we read earlier -- Check if update succeeded-- If 0 rows affected: someone else modified while we worked-- Retry: re-read, re-process, re-update -- TIMESTAMP-BASED OPTIMISTIC LOCKINGCREATE TABLE Documents ( DocID INT PRIMARY KEY, Content TEXT, ModifiedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP); -- ReadSELECT DocID, Content, ModifiedAt FROM Documents WHERE DocID = 50;-- Returns: ModifiedAt = '2024-01-15 10:30:00' -- Update with timestamp check UPDATE DocumentsSET Content = 'New content', ModifiedAt = CURRENT_TIMESTAMPWHERE DocID = 50 AND ModifiedAt = '2024-01-15 10:30:00'; -- If 0 rows: conflict detected. Refresh and retry. -- APPLICATION PATTERN (pseudocode):-- function updateProduct(id, newStock):-- MAX_RETRIES = 3-- for attempt in range(MAX_RETRIES):-- product = SELECT ... WHERE id = :id-- result = UPDATE ... WHERE id = :id AND version = product.version-- if result.rowsAffected == 1:-- return SUCCESS-- if attempt < MAX_RETRIES - 1:-- sleep(random(50, 100) * attempt) # Backoff-- return CONFLICT_ERRORPessimistic locking (SELECT FOR UPDATE): Best when conflicts are common. Lock immediately, work, release. Optimistic locking (version checks): Best when conflicts are rare. Read freely, check on write, retry if conflict. Most web applications benefit from optimistic locking because reads far exceed writes.
Concurrent access is what transforms a database from a personal data store into a multi-user system. DBMS provides sophisticated mechanisms to coordinate simultaneous access while maintaining data consistency. Let's consolidate the key concepts:
What's Next:
We've explored how DBMS maintains correctness during concurrent access. But what about failures—power outages, disk crashes, hardware failures? The next page examines Security and Backup—how DBMS protects data from loss, unauthorized access, and disasters.
You now understand how DBMS enables safe concurrent access—the foundation of multi-user database systems. This capability transformed databases from single-user data stores into the shared infrastructure powering modern applications serving millions of users.