Practice: transactions and concurrency
A review of this module. Skip it if you feel confident; the next module does not need it.
Fix the mistakes
The slips that cost marks in this module. Spot each one, then check the fix.
Most transaction bugs come from four habits: committing without checking what each statement did, writing values computed from an earlier read, choosing an isolation level without knowing what it allows, and locking rows in a different order in different places. Each one has a short, reliable fix.
Mistake 1: committing without checking each step
The affected row count is the number of rows a statement changed, which ROW_COUNT() returns. An UPDATE that matches no row changes 0 rows and is not an error, so the app must check the count before COMMIT.
A debit that finds no money changes zero rows, and MySQL does not complain. If the app commits anyway, half a payment is saved.
Example 1 · A wallet payment that checks every step
DROP TABLE IF EXISTS wallets;CREATE TABLE wallets ( user_id INT PRIMARY KEY, name VARCHAR(40) NOT NULL, balance DECIMAL(10,2) NOT NULL CHECK (balance >= 0));INSERT INTO wallets (user_id, name, balance) VALUES (1, 'Sneha Patil', 650.00), (2, 'ShopLite', 0.00);
-- Sneha pays the shop Rs. 120: every statement's row count is checkedSTART TRANSACTION;UPDATE wallets SET balance = balance - 120 WHERE user_id = 1 AND balance >= 120;SELECT ROW_COUNT() AS debited; -- must be 1UPDATE wallets SET balance = balance + 120 WHERE user_id = 2;SELECT ROW_COUNT() AS credited; -- must be 1COMMIT; -- the app commits only after two 1s
-- Sneha tries to pay Rs. 600: the debit finds no row with enough moneySTART TRANSACTION;UPDATE wallets SET balance = balance - 600 WHERE user_id = 1 AND balance >= 600;SELECT ROW_COUNT() AS debited; -- 0: stop hereROLLBACK;
SELECT user_id, name, balance FROM wallets ORDER BY user_id;+---------+ | debited | +---------+ | 1 | +---------+ +----------+ | credited | +----------+ | 1 | +----------+ +---------+ | debited | +---------+ | 0 | +---------+ +---------+-------------+---------+ | user_id | name | balance | +---------+-------------+---------+ | 1 | Sneha Patil | 530.00 | | 2 | ShopLite | 120.00 | +---------+-------------+---------+
What this code does
- The first payment gets row counts 1 and 1, so the app commits. The second debit asks for
balance >= 600while Sneha has 530.00, so it changes no row and the app rolls back before any credit. - The CHECK constraint is a second safety net: even a buggy debit without the guard could not make the balance negative.
A transaction is only as safe as the decision to commit it. MySQL does not roll back on its own when a statement fails. The fix is a rule for every multi-step change: after each statement, check the error and the affected row count. Commit only when every step did exactly what it should; otherwise roll back.
Put the business check inside the statement where you can: WHERE balance >= 120 makes the debit itself refuse to overdraw, so the row count tells you whether the money was there.
Find the bug
Find the unchecked step
Click the line that lets half a payment be saved.
The debit finds no wallet with enough money.
Round 1 of 2Mistake 2: read, compute in the app, write back
A guarded in-place update changes a value with arithmetic inside the UPDATE and a safety check in WHERE, such as SET stock = stock - 5 WHERE stock >= 5. The app then checks the row count.
Reading a value, computing in the app and writing it back loses an update when two users act together.
Example 2 · A flash sale with a guarded in-place update
-- Step 1 · Session A: buyer 1 wants 5 iPhones (stock 8)START TRANSACTION;UPDATE products SET stock = stock - 5WHERE product_id = 3 AND stock >= 5;SELECT ROW_COUNT() AS a_bought;
-- Step 2 · Session B: buyer 2 wants 5 at the same momentSTART TRANSACTION;UPDATE products SET stock = stock - 5WHERE product_id = 3 AND stock >= 5;SELECT ROW_COUNT() AS b_bought;
-- Step 3 · Session ACOMMIT;
-- Step 4 · Session B: b_bought was 0, so roll backROLLBACK;SELECT product_id, name, stock FROM products WHERE product_id = 3;-- Step 1 · Session A +----------+ | a_bought | +----------+ | 1 | +----------+ -- Step 2 · Session B (waited until step 3) +----------+ | b_bought | +----------+ | 0 | +----------+ -- Step 4 · Session B +------------+-----------+-------+ | product_id | name | stock | +------------+-----------+-------+ | 3 | iPhone 15 | 3 | +------------+-----------+-------+
What this code does
- B's UPDATE waits for A's row lock, then checks
stock >= 5against the new stock, 3, and changes nothing. The app tells buyer 2 that only 3 are left. - No read, no computed value, no lost update: the database checks and changes the stock in one locked step.
Isolation levels do not prevent this at MySQL's default level. Two fixes cover almost every case. For a counter or a balance, update in place with a guard: SET stock = stock - 5 WHERE product_id = 3 AND stock >= 5, then check the row count. When the decision needs several reads, lock first with SELECT ... FOR UPDATE inside the transaction.
For long edits on a screen, where holding a lock is not possible, use a version column and check it when saving.
Fix the error
Replace the app-computed write
Two sessions both read stock 120 and both write 115. Pick the safe second line.
Sell 5 power banks (product 7).
Error
No error is shown: both sales succeed, but the stock drops by 5 instead of 10.
Mistake 3: choosing the wrong isolation level
An isolation level is a setting, chosen per session or per transaction, that decides what a transaction's reads can see of other transactions' work.
A report reads several numbers one after another. At the wrong level it can mix values from before and after another transaction.
Example 3 · A report at READ COMMITTED mixes two moments
-- Setup, once, in either session:DROP TABLE IF EXISTS bank_accounts;CREATE TABLE bank_accounts ( acc_no INT PRIMARY KEY, holder VARCHAR(40) NOT NULL, balance DECIMAL(10,2) NOT NULL, CONSTRAINT balance_not_negative CHECK (balance >= 0));INSERT INTO bank_accounts (acc_no, holder, balance) VALUES (101, 'Asha Rao', 5000.00), (102, 'Ravi Kumar', 1200.00);
-- Step 1 · Session A: the report reads account 101SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;START TRANSACTION;SELECT balance AS bal_101 FROM bank_accounts WHERE acc_no = 101;
-- Step 2 · Session B: a transfer of Rs. 1000 from 101 to 102START TRANSACTION;UPDATE bank_accounts SET balance = balance - 1000 WHERE acc_no = 101;UPDATE bank_accounts SET balance = balance + 1000 WHERE acc_no = 102;COMMIT;
-- Step 3 · Session A: the report reads account 102SELECT balance AS bal_102 FROM bank_accounts WHERE acc_no = 102;COMMIT;-- Step 1 · Session A +---------+ | bal_101 | +---------+ | 5000.00 | +---------+ -- Step 3 · Session A +---------+ | bal_102 | +---------+ | 2200.00 | +---------+
What this code does
- The report adds 5000.00 (before the transfer) and 2200.00 (after it) and prints Rs. 7,200 in the bank, Rs. 1,000 that never existed.
- At the default REPEATABLE READ, both reads come from one snapshot and the total is the true Rs. 6,200. Practice 5 asks you to show it.
A report that reads several numbers at READ COMMITTED can print totals that never existed together.
A simple guide for MySQL: REPEATABLE READ (default): most work. Reports in one transaction see one consistent snapshot; READ COMMITTED: each statement should see the latest data, and fewer lock waits matter more than repeatable reads; SERIALIZABLE: short, critical transactions where you cannot lock the exact rows yourself; expect waits and deadlocks; READ UNCOMMITTED: almost never; it shows changes that may be rolled back.
Mistake 4: locking in a different order in different places
A fixed lock order means every transaction locks rows in the same order, for example by ascending id. Then no circle of waiting can form.
One cart locks product 7 then 6, another locks 6 then 7. Under load, they will deadlock.
Example 4 · Two carts that lock products in id order
-- Step 1 · Session A: cart 1 holds products 7 and 6: lock in id orderSTART TRANSACTION;SELECT product_id, stock FROM productsWHERE product_id IN (6, 7) ORDER BY product_id FOR UPDATE;
-- Step 2 · Session B: cart 2 holds products 6 and 7: the same orderSTART TRANSACTION;SELECT product_id, stock FROM productsWHERE product_id IN (6, 7) ORDER BY product_id FOR UPDATE;
-- Step 3 · Session AUPDATE products SET stock = stock - 1 WHERE product_id = 7;UPDATE products SET stock = stock - 1 WHERE product_id = 6;COMMIT;
-- Step 4 · Session BUPDATE products SET stock = stock - 2 WHERE product_id = 6;UPDATE products SET stock = stock - 2 WHERE product_id = 7;COMMIT;SELECT product_id, name, stock FROM products WHERE product_id IN (6, 7) ORDER BY product_id;-- Step 1 · Session A +------------+-------+ | product_id | stock | +------------+-------+ | 6 | 120 | | 7 | 60 | +------------+-------+ -- Step 2 · Session B (waited until step 3) +------------+-------+ | product_id | stock | +------------+-------+ | 6 | 119 | | 7 | 59 | +------------+-------+ -- Step 4 · Session B +------------+------------------------+-------+ | product_id | name | stock | +------------+------------------------+-------+ | 6 | boAt Airdopes 141 | 117 | | 7 | Mi Power Bank 20000mAh | 57 | +------------+------------------------+-------+
What this code does
- Both carts lock 6 before 7, whatever order the customer added them in. Cart 2 waits in step 2 and then reads the stock cart 1 left behind.
- The final stock is 120 - 1 - 2 = 117 earbuds and 60 - 1 - 2 = 57 power banks: both checkouts counted, no deadlock.
The fix is a fixed order: sort the ids and lock them all at the start with one ... WHERE product_id IN (...) ORDER BY product_id FOR UPDATE. Keep the transaction short, and still retry the whole transaction on error 1213, because some deadlocks come from places you did not plan for.
Module quiz
Questions from the whole module. Your first answer is your score.
Problems
Graded easy to hard, using everything from this module.
Try each one yourself first. Use the hint if you get stuck. Mark it as done when it works.
Exercise 1· Easy
In one transaction, add 10 bonus marks to Gaurav Mishra's (student 7) Database Systems mark (course 2) and set a savepoint. Then by mistake add 10 to all his courses; undo only that, commit, and show his course_id, marks ordered by course.
+-----------+-------+ | course_id | marks | +-----------+-------+ | 1 | 64 | | 2 | 48 | +-----------+-------+
Exercise 2· Easy
Sell 3 iPhones (product 3, stock 8) with one guarded in-place UPDATE and show the row count as sold_3. Then try to sell 6 more the same way and show sold_6. Finish with the stock.
+--------+ | sold_3 | +--------+ | 1 | +--------+ +--------+ | sold_6 | +--------+ | 0 | +--------+ +------------+-----------+-------+ | product_id | name | stock | +------------+-----------+-------+ | 3 | iPhone 15 | 5 | +------------+-----------+-------+
Exercise 3· Medium
Create the wallets table from Example 1. Sneha pays the shop Rs. 650 (her whole balance). Write the safe version: guarded debit, debited row count, credit, credited row count, commit. Show both wallets.
+---------+ | debited | +---------+ | 1 | +---------+ +----------+ | credited | +----------+ | 1 | +----------+ +---------+-------------+---------+ | user_id | name | balance | +---------+-------------+---------+ | 1 | Sneha Patil | 0.00 | | 2 | ShopLite | 650.00 | +---------+-------------+---------+
Exercise 4· Medium
Predict, then run: Session B (default level) starts a transaction and sums the HR salaries (dept 3) as hr_total. Session A gives Rohan Das (emp_id 10) a raise of 10000. B sums again, commits and sums once more. What are B's three totals?
-- Step 1 · Session B +-----------+ | hr_total | +-----------+ | 150000.00 | +-----------+ -- Step 3 · Session B +-----------+ | hr_total | +-----------+ | 150000.00 | +-----------+ +-----------+ | hr_total | +-----------+ | 160000.00 | +-----------+
Exercise 5· Medium
Fix the report of Example 3: run it at the default REPEATABLE READ, starting with START TRANSACTION WITH CONSISTENT SNAPSHOT, with the same transfer in the middle. Show bal_101 and bal_102; their sum must be Rs. 6,200.
-- Step 1 · Session A +---------+ | bal_101 | +---------+ | 5000.00 | +---------+ -- Step 3 · Session A +---------+ | bal_102 | +---------+ | 1200.00 | +---------+
Exercise 6· Medium
A cancel button can be clicked twice. Cancel order 14 (already CANCELLED) safely: in a transaction, run the guarded status UPDATE (AND status <> 'CANCELLED') and show ROW_COUNT() as cancelled_now. It is 0, so roll back instead of restocking. Show the order and the stock of its product (4).
+---------------+ | cancelled_now | +---------------+ | 0 | +---------------+ +----------+-----------+ | order_id | status | +----------+-----------+ | 14 | CANCELLED | +----------+-----------+ +------------+-----------------------+-------+ | product_id | name | stock | +------------+-----------------------+-------+ | 4 | Lenovo IdeaPad Slim 3 | 12 | +------------+-----------------------+-------+
Exercise 7· Hard
Flash sale with locks: buyer 1 (Session A) locks the iPhone row with FOR UPDATE, sees stock 8 and buys 5. Buyer 2 (Session B) also wants 5: its locking read waits, then shows 3, so it rolls back. Show the final stock.
-- Step 1 · Session A +-------+ | stock | +-------+ | 8 | +-------+ -- Step 2 · Session B (waited until step 3) +-------+ | stock | +-------+ | 3 | +-------+ -- Step 4 · Session B +------------+-----------+-------+ | product_id | name | stock | +------------+-----------+-------+ | 3 | iPhone 15 | 3 | +------------+-----------+-------+
Exercise 8· Hard
Reproduce the cart deadlock and the retry. Cart 1 (A) lowers product 7 then 6 by 1; cart 2 (B) lowers product 6 then 7 by 2, interleaved, so B becomes the victim. A commits. Then B retries its whole transaction and commits. Show the final stock of both products.
-- Step 3 · Session A (waited until step 4) -- Step 4 · Session B ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction -- Step 6 · Session B +------------+------------------------+-------+ | product_id | name | stock | +------------+------------------------+-------+ | 6 | boAt Airdopes 141 | 117 | | 7 | Mi Power Bank 20000mAh | 57 | +------------+------------------------+-------+
Interview questions
Questions interviewers ask on this topic.
Finish up
Mark complete & continue
Next: Mini project: cinema seat booking