Message Queues & Database Locking
How a queue decouples services and absorbs traffic spikes — RabbitMQ, Kafka, and Amazon SQS compared, with Kafka's partition-ordering caveat. Plus the full lock taxonomy (shared, exclusive, update, schema), row vs table granularity, optimistic vs pessimistic concurrency, and MVCC.
Assumes you know
After this you should be able to
- Explain the core idea in Message Queues & Database Locking
- Apply Message Queues & Database Locking in a representative design
Two building blocks that keep a large system responsive under load: a buffer that lets services talk without waiting on each other, and a set of rules that stop two writers from corrupting the same row. This note covers both, in the order they were taught, with the interview framing the instructor kept returning to.
A message queue decouples services so a slow or dead consumer can't stall the user — the message waits in the queue instead of failing the request. A lock does the opposite job on data: it serializes access to a row so two updates can't quietly overwrite each other. In an interview, neither choice is "right" or "wrong" — you win by naming the trade-off out loud and justifying the fit.
Revision Sheet#
Message Queue — purpose#
- MQ = async buffer between services. Producer → queue → consumer.
- Without MQ:
svc1 → svc2 → svc3synchronous → user waits for whole chain; one service down → cascading 500. - With MQ: order svc saves to DB, publishes event, returns success immediately. Downstream pulls at own pace.
- Doodle: user → [Order] → (queue barrel) → Inventory / Payment / Email hanging off it.
- ⚠ Queue rescues a down downstream — NOT a down entry service (no producer → nothing to enqueue).
- Use when → microservices comms · task / background queues · event-driven workflows · app-to-app data exchange.
MQ — 4 advantages#
- Decoupling → loose coupling; producer doesn't wait on consumer.
- Scalability → add consumers / add queue instances → horizontal.
- Async → faster response, lower latency.
- Load leveling → queue = buffer during traffic spike; consumers drain at steady rate.
MQ — message persists in queue until processed (not lost on consumer restart).#
Technologies#
| Tech | Model | Note |
|---|---|---|
| Kafka | pub/sub | high throughput, disk-first, persistent, low RAM |
| RabbitMQ | queue | RAM-heavy, priority queue built-in |
| SQS | queue | AWS managed, pay-per-use, 256KB msg cap |
- ⭐ Same message → many consumers = Kafka. One message → one consumer = SQS / RabbitMQ.
- Kafka ordering: per-partition only, not global. One partition → strict order but no scale.
- Kafka priority: not built-in → simulate via topics/partitions by priority.
- Kafka ack = offset commit, not per-message ack.
- ⭐ Same-order-per-user → route by key so a user's msgs hit the same partition.
Database Locking — why#
- Prevents lost updates, dirty reads, inconsistent data. Upholds ACID.
- Two users update same row at once → one overwrites the other → wrong value.
Lock types#
| Lock | Allows | Blocks |
|---|---|---|
| Shared (read) | many readers | writers wait |
| Exclusive (write) | one writer | reads + writes wait |
| Update | read → maybe upgrade to write | prevents deadlock on upgrade |
| Schema | protects structure | blocks ALTER/DROP during use |
Granularity (coarse ↓ = less concurrency)#
- Row → highest concurrency, best OLTP / banking / e-commerce.
- Page → group of rows, bulk update on a page.
- Table → bulk update, ALTER TABLE, maintenance.
- Database → backup / restore / migration.
- Doodle: nested boxes — row inside page inside table inside DB; lock the smallest that works.
Concurrency strategies#
| Strategy | Assumes | Mechanism |
|---|---|---|
| Optimistic | conflicts rare | version/timestamp check at update |
| Pessimistic | conflicts common | lock row immediately |
| MVCC | — | keep versions; readers see last commit |
- ⭐ MVCC: writers don't block readers. Read = old committed version, write = still exclusive.
- ⚠ MVCC cost = extra storage + must clean up old versions.
- Master→slave replication = async → eventual consistency. MVCC keeps slave readable while it applies writes.
- 💡 Interview move: state your pick → its downside → the better option that removes it (link-list answering).
§ 2 — Detailed Study Reference
The Lesson, In Full#
The hook. You tap "Place order." Behind that button, the order service calls inventory, which calls payment, which calls email, which calls notifications — each waiting on the one before it. Now payment is slow for two seconds. Your spinner keeps turning, because every service in the chain is holding the line open. Worse: if payment is down, you get a 500 and the order evaporates. The whole design has one flaw — everything is waiting on everything else. The two topics in this note are the standard fixes: a queue to break the waiting, and locks to keep concurrent writers honest.
2.1What a message queue is, and why it exists#
A message queue is a buffer that lets one service hand off work to another without waiting for it to finish. The sender (the producer) drops a message in; the receiver (the consumer) picks it up whenever it has capacity. The two never talk directly.
A waiter doesn't stand at the kitchen pass waiting for each dish to cook. They clip the order to a rail and walk away. The line cooks pull tickets when a station frees up. The rail is the queue: it lets the waiter keep serving other tables (low latency for the "user"), and if a cook steps away, the ticket just waits — it isn't lost.
Return to the e-commerce flow. Without a queue, the order service calls each downstream service synchronously — one API request/response after another — and the user sits through the sum of all of them. That violates the low-latency requirement, and it's fragile: if the inventory service goes down, payment never gets called, the order service gets no response, and the failure ripples all the way back to the customer as a 500 error.
Drop a queue in between and the order service does three things and returns: save the order to the database, publish an order-created event to the queue, respond "success" to the user. The downstream services pull that event whenever their own load allows. If payment is at 90% CPU, it waits until it isn't. If payment is down, its replica reads the still-waiting message once it recovers. The customer never sees the failure — at worst, a little extra latency.
A student asked the sharp question: what if the first service — the one the client hits directly — is itself down? The queue can't save you there; there's nothing to receive the request and publish it. The queue protects downstream hops, not the front door. The instructor's point: know which failure a component actually covers, and be ready to say so in an interview.
2.2The four advantages, stated the way an interviewer wants them#
Every reason to reach for a queue reduces to one of these four. The instructor was explicit: when you place a queue in a design, justify it by naming which of these it buys you.
Decoupling#
The producer sends without waiting for the consumer; the consumer processes at its own pace. Services are loosely coupled — order service no longer depends directly on payment service. New consumers can be added later without redrawing the architecture.
Scalability#
Horizontal by design. Add more consumer replicas to drain faster; run multiple queue instances across the system. More consumers can process messages concurrently as load grows.
Asynchronous#
Producer and consumer run independently, so the producer never blocks on processing. Lower latency, more responsive system — the user gets a fast reply.
Load leveling#
The queue is a shock absorber during traffic spikes. Messages pile up during peak; consumers drain them at a steady rate. Backends never get flooded, so performance stays stable.
Load leveling is a reservoir. A flash flood — your kickoff-time traffic spike — fills the reservoir instead of drowning the town downstream, and the dam releases water at a steady, safe rate. The queue is the reservoir; consumers draining at their own pace are the controlled release. Nothing is thrown away, it is just spread over time.
One property underlies all of it: a message stays in the queue until a consumer processes it. A consumer restart doesn't lose work — the message is still there when it comes back up.
Where queues actually show up: communication between microservices; task and background queues (batch jobs, scheduled work); event-driven systems (a match kicks off at 4pm and every subscriber is notified); and general application-to-application data exchange. In each case the justification is one of the four above — you want decoupling, buffering, or independent scaling — so name which one when you place a queue in a design.
2.3Kafka, RabbitMQ, SQS — and the one line that separates them#
Three technologies came up. They differ most in who can consume a message and where messages live.
RabbitMQ and SQS are a ticket queue: the next person takes the top ticket, and it's gone. Kafka is a bulletin board: a notice is pinned up, everyone who cares reads it, and it stays pinned until it expires. That single difference — consumed-and-gone vs. read-by-many-and-retained — drives almost every choice below.
| Property | Kafka | RabbitMQ | Amazon SQS |
|---|---|---|---|
| Model | Publisher/subscriber (log) | Queue-based | Queue-based |
| Same msg to many consumers | Yes (fan-out native) | No by default (exchange config can) | No |
| Storage | Disk-first, persistent (retain days–year) | RAM-first for queuing/routing | Managed by AWS |
| Throughput | Very high, millions writes/sec | Lower | Managed, higher latency vs self-hosted |
| Priority queue | Not built-in | Built-in | No |
| Ops | Complex setup | Complex setup, high RAM under load | Fully managed, pay-per-use, 256KB cap |
Kafka is the default for serious designs because it collects the distributed-systems wish list in one place: very high throughput and low latency, horizontal scalability across many consumers, replication for fault tolerance, and — unusually — persistent storage of messages on disk. Because it writes to disk immediately, its RAM use is low and predictable. RabbitMQ keeps messages in RAM first, so it's memory-heavy and less durable across restarts, but it ships a real priority queue. SQS is the managed, low-effort choice: no infrastructure, pay-per-use, integrates with the rest of AWS, capped at 256KB per message and with higher latency than a self-hosted broker.
Ask: does the same message need to reach many consumers, or exactly one? Fan-out to many (broadcast, pub/sub) → Kafka. One message consumed once and then gone → SQS or RabbitMQ. In the e-commerce flow, one customer's order only needs to be handled once, so SQS fits; when an event must be broadcast to 50 consumers, Kafka fits.
No. In SQS/RabbitMQ, many consumers can listen on the same queue, but each message is delivered to only one of them — once it's acknowledged, it's removed. That's not pub/sub. Kafka is the one where the same message stays available to consumer 3, 4, and 5. Don't assume "message queue" implies broadcast.
The framing the instructor pressed hardest: in an interview it "hardly matters" whether you pick SQS, RabbitMQ, IBM MQ, or a cloud provider's own queue — what matters is that you can justify the choice against the functional and non-functional requirements. RabbitMQ can fan out with exchange/routing-key config; Kafka can restrict to one consumer per partition. Neither is the default behavior, so if you want it, say you'll implement it and how.
2.4Kafka internals: partitions, ordering, priority, acks#
Kafka organizes messages into topics, and each topic is split into partitions. Consumers subscribe to topics of interest. Partitioning is what buys Kafka its throughput — but it's also the source of its sharpest gotchas.
Ordering#
Guaranteed per partition, not globally. Read sequentially from one partition and you see messages in insertion order. Across partitions, parallel processing means you might see m2 before m1. Want strict global order? Use a single partition — simple, but no longer scalable.
Priority#
No native priority queue (RabbitMQ has one). Kafka is a strict append-only log. To fake priority: separate topics/partitions by priority level, and have consumers read high-priority topics first, buffering and sorting locally.
Acknowledgement#
Not per-message like a traditional broker. Kafka uses offset commits: the consumer reads from a partition and commits the offset once processed. A committed offset means "handled up to here."
One consumer per message#
Achievable in a consumer group by assigning each partition to exactly one consumer (P0→C1, P1→C2…). It's not Kafka's default; you enforce it with the partition-to-consumer mapping.
You rarely need global order — you need order per user (a user's events applied in sequence). Route by a key: send all messages for the same user to the same partition. Now that user's messages stay ordered while other partitions still run in parallel. You keep throughput and get the ordering that actually matters.
2.5Database locking: why a row needs a bouncer #
A database lock is a mechanism that controls concurrent access to data so two transactions can't corrupt the same record. It's what enforces consistency, isolation, and the rest of ACID when many users hit the same row at once.
Picture a paper ledger with a single pen chained to it. To change a balance you must hold the pen. If two clerks could scribble at the same time, one would write over the other and the final number would be nonsense. The lock is the chained pen: it forces changes to happen one at a time.
The canonical failure is the lost update: user A reads a balance and writes a new value; user B does the same simultaneously; whoever saves last silently overwrites the other, and the final figure is wrong. Locks exist to make that impossible.
2.6The four lock types#
Shared / read lock#
- Taken when a transaction reads.
- Many readers can hold it at once.
- No one may modify until all shared locks release.
- Used by SELECT, dashboards, reports.
- Cost: writers wait.
Exclusive / write lock#
- Taken when a transaction modifies (INSERT/UPDATE/DELETE).
- Only one holder; others can't read or write.*
- Maximum consistency, released on commit/rollback.
- Cost: reduced concurrency, blocking, latency.
Update lock#
A temporary lock for the "read now, maybe write in a moment" pattern. It reads the row, holds an update lock, and upgrades to a write lock only if the write actually happens. Its whole job is to prevent the deadlock that occurs when two transactions each hold a shared lock and both try to upgrade to exclusive at once. SQL Server uses it internally.
Schema lock#
Protects the structure of the database, not the data. Held during ALTER TABLE, CREATE INDEX, DROP, TRUNCATE. It blocks concurrent schema changes so metadata stays consistent. DDL can block other DDL/DML while it's held.
The deadlock it prevents is subtle: two transactions both read a row (each gets a shared lock), then both decide to update (each needs to upgrade to exclusive) — and each is stuck waiting for the other to release its shared lock. The update lock breaks the symmetry: only one transaction can hold it, so only one can be first in line to upgrade.
2.7Lock granularity: lock the smallest thing that works#
Granularity is how much data one lock covers. Finer locks (a row) allow more concurrency but more bookkeeping; coarser locks (a table) are cheap to manage but block more users.
| Granularity | Concurrency | Best for |
|---|---|---|
| Row | Highest | OLTP, banking, e-commerce — one row locked, rest free |
| Page | Lower | Bulk update of the rows on one page; less lock management |
| Table | Low | Bulk updates across most rows, ALTER TABLE, maintenance |
| Database | None | Backup, restore, migration |
An UPDATE tbl SET ... WHERE user_id = 100 takes row-level exclusive locks
only on the rows that match the WHERE clause (assuming the DB supports row locking) —
not the whole table, and not a fixed "page size." Match 10,000 rows and 10,000 rows lock; the rest stay
free and updatable by other transactions. A single-row INSERT locks a row, not the table. The common
wrong mental model is "a multi-row update is internally a table lock" — it isn't.
When you do need a table lock (say, ALTER TABLE adding a column to a million-row table), the instructor's practical advice: schedule it as a batch job during your lowest-traffic window — check the traffic dashboard first — because a table lock blocks reads too, unless you're on MVCC.
2.8Optimistic, pessimistic, and MVCC#
Given the concurrent-update problem, databases offer strategies that differ in one assumption: how often do conflicts actually happen?
Optimistic locking#
Assumes conflicts are rare. Doesn't lock on read. Records a version number or timestamp, and at update time checks whether it still matches. If it changed, another transaction got there first — the update fails and the app retries or asks the user to refresh. High concurrency, no blocking; cost is retry logic and occasional conflicts. Good for read-heavy apps, social media, catalogs, microservices.
Pessimistic locking#
Assumes conflicts are common. Locks the record immediately, before modifying; other transactions wait until commit/rollback. Strong consistency, no version conflicts; cost is blocking, possible deadlock, lower scalability. Good for banking, payments, inventory, flight/hotel booking — anywhere the right value must win.
Optimistic = everyone edits freely; on save, the system warns "this changed since you opened it" and makes you reconcile. Pessimistic = the doc is checked out and read-only for everyone else until you're done. Optimistic bets that clashes are rare; pessimistic refuses to gamble.
MVCC (Multi-Version Concurrency Control) is the third path: instead of locking data away from readers, the database keeps multiple versions of a row. While a writer updates a row, readers see the last committed version — they never wait. Writes still take an exclusive lock (two writers can't touch the same row at once), but reads are never blocked by writes.
A plain exclusive lock blocks both readers and writers; MVCC blocks only writers and lets readers keep reading the last committed version. That's the whole reason to reach for it — availability of reads during writes.
Every write creates a new version, so MVCC needs extra storage and the database must clean up old versions (vacuum/maintenance). On a write-heavy system, versions pile up fast and that cleanup becomes real operational cost. In an interview, if you favor a plain write lock over MVCC, name this as MVCC's downside.
Where MVCC meets replication. When a master DB copies writes to a slave, the slave continuously reads the commit log and replays each write. That copy takes time — so the system is eventually consistent, not instantly. A reader hitting the slave mid-replay sees slightly stale data, then the two converge. MVCC is what keeps the slave readable during the replay: writers apply the log while readers still get the last committed version. For Instagram-style feeds, eventual consistency is fine; for banking, you'd choose pessimistic/write locking so reads never return stale money. This is the CAP trade-off in miniature: pick availability (MVCC, maybe-stale reads) or strong consistency (locked, unavailable until released) — you can't have both at once.
2.9Interview & practice Q&A#
Q1 Why not just use Kafka everywhere instead of SQS?
Because for one-to-one workflows, SQS's simpler behavior is exactly what you want. In the order flow, a single order needs to be processed once, not broadcast — SQS's "consumed and gone" model fits, with no need for Kafka's pub/sub, retention, or partition management. Kafka shines when the same message must fan out to many consumers or you need very high throughput with retention. Match the tool to whether the message is one-shot or broadcast.
Q2 How does Kafka guarantee I process messages in order?
Only within a single partition. Messages in partition 0 are read in insertion order; across partitions, parallel processing means no global order. For strict global order, use one partition (loses scalability). For per-entity order (e.g., per user), route by a key so all of that user's messages land in the same partition — you keep ordering where it matters and stay scalable.
Q3 Kafka has no priority queue. How do I get priority behavior?
You implement it yourself, because Kafka is a strict append-only log. Separate topics or partitions by priority level, have consumers always read the high-priority topic first, and buffer/sort locally if needed. RabbitMQ, by contrast, has a native priority queue — so if priority is central, that's a point in RabbitMQ's favor.
Q4 A message queue protects against a service going down — but what if the very first service is down?
Then the queue can't help — there's no receiver to accept the request and publish it. Queues protect downstream hops from failures and slowness; they don't protect the entry point. You'd address a dead front door with replicas, load balancing, and health checks, not a queue. The honest answer in an interview is to name that boundary explicitly.
Q5 What exactly does UPDATE ... WHERE user_id = 100 lock?
Row-level exclusive locks on just the rows matching the WHERE clause, assuming row locking is supported. If 10,000 rows match, those 10,000 lock; unmatched rows stay free for other transactions. Another transaction touching a matched row waits, since UPDATE is a write. It is not a table lock and not tied to a fixed page size.
Q6 My app is write-heavy and distributed. Which concurrency strategy?
MVCC is usually the strong default, even when writes dominate. Writes still take an exclusive lock (so writers serialize), but reads never block — and even a write-heavy app almost always still serves reads. A plain write lock is also correct if you truly have near-zero readers; in that edge case MVCC and a write lock behave the same. Just remember MVCC's cost: version cleanup.
Q7 Does MVCC give strong consistency or only eventual consistency?
MVCC itself supports strong consistency on writes — two writers can't modify the same row simultaneously; the second waits. The "staleness" people associate with it is on the read path: a reader may see the last committed version rather than an in-flight write. Eventual consistency comes from replication (master→slave copy lag), not from MVCC as such.
Q8 Do I need to decide Kafka partition count up front?
No — Kafka is built to scale. If your app takes off, you can add topics and increase partitions later to spread load across more consumers. Design for a reasonable starting count, and grow it as throughput demands. (Note: repartitioning can affect key-based ordering, so plan keyed topics with that in mind.)
Q9 How should I structure a system-design answer about these choices?
Use "link-list" answering: state your pick, name its main disadvantage, then offer the better option that removes that disadvantage — and explain how. Example: "I'll use a write lock; its downside is that it blocks readers too, so I'd move to MVCC, which keeps reads available at the cost of version cleanup." Each answer leaves a pointer to the next question, which keeps you driving the interview.
Q10 Why is RabbitMQ memory-heavy while Kafka stays light?
They store messages in opposite places. RabbitMQ keeps messages in RAM for queuing and routing, which is quick to reach but expensive and risky — a broker restart can lose in-RAM messages. Kafka is an append-only log that writes to disk the moment a message arrives, so RAM use stays low and predictable, and the same design is what gives it persistent storage and durability. For large volumes, or when messages must survive a restart, that's the argument for Kafka.
Q11 Why does a read on a replica sometimes return stale data?
Because master-to-replica replication is asynchronous. The master commits and appends to its log; the replica tails that log and replays each write afterwards. A read that lands on the replica before the replay finishes sees the older value — that's eventual consistency, and the lag is roughly how long the replay takes. MVCC is what lets the replica keep answering reads with the last committed version while the new writes are being applied.
2.10Key takeaways#
- A queue trades a little latency for decoupling, fault tolerance, scalability, and load leveling — and the message survives a consumer restart.
- Broker choice reduces to one question: same message to many consumers (Kafka) or exactly one (SQS/RabbitMQ)?
- Kafka orders per partition, not globally; route by key for per-entity order without losing scale.
- Locks serialize writers to prevent lost updates; lock the smallest granularity that does the job.
- Optimistic bets conflicts are rare (version check); pessimistic assumes they're common (lock now); MVCC lets readers through while writers work.
- Replication lag → eventual consistency; the availability-vs-consistency call is CAP in practice.
§ 3 — Microexercises
Now Do It Yourself#
Two drills. The first is a design decision you'll make out loud in an interview; the second turns the granularity pitfall into a reflex.
3.1Exercise 1 — Pick the broker (design drill)#
You're designing notifications for a live sports app. When a match goes live, a single match-started event must reach every subscribed service:
push-notifications, the live-scores cache, and the analytics pipeline — all three act on the same event
independently.
Decide the broker and justify it against the requirements:
- The same event must be consumed by 3+ independent services (fan-out).
- Traffic spikes hard at kickoff (load leveling).
- State the exact answer: which broker, the one property that decides it, and one thing you must configure for ordering per match.
Show the full walkthrough
Step 1 — Identify the shape. The decisive fact is "the same event must reach every subscribed service." That's fan-out / broadcast, not one-shot consumption.
Step 2 — Apply the picking question. "Same message to many consumers → Kafka. One message consumed once → SQS/RabbitMQ." Three independent services need the same event, so this is Kafka's native pub/sub territory. SQS would hand the event to just one consumer and remove it — wrong model here.
Step 3 — Check the other requirements. Kickoff spikes are handled by the queue as a buffer (load leveling); Kafka's high throughput and disk-first retention cover the burst and let a lagging consumer catch up.
Step 4 — Handle ordering per match. If a match's events (started → goal →
ended) must stay ordered, route by a key — match_id — so all events for
one match land in the same partition. Across matches, partitions run in parallel; within a match,
order holds.
// Kafka: key by match_id so one match's events stay ordered
ProducerRecord<String, String> rec =
new ProducerRecord<>("match-events", matchId, "match-started");
producer.send(rec);
// push-notify, scores-cache, analytics each subscribe as
// their OWN consumer group -> every group gets the event.Expected answer: Kafka; decided by the need for fan-out (same message to many
consumers); configure ordering by using match_id as the partition key,
and give each service its own consumer group.
Why: the one-vs-many-consumers question picks the broker, and keyed partitioning is how you keep the ordering that matters without sacrificing scale.
3.2Exercise 2 — Predict the lock (diagnose drill)#
A teammate claims: "Our nightly job runs one big UPDATE to recompute loyalty
points for a segment of users, so it must be taking a table lock — that's why the app
feels frozen at 2am." The table has 5 million rows; the segment matches 40,000 rows.
UPDATE members
SET points = points + monthly_bonus
WHERE segment = 'gold'; -- matches ~40,000 of 5,000,000 rows- Predict what the statement actually locks (assume row-level locking is supported).
- Explain whether the teammate's "table lock" diagnosis is correct.
- Say what a concurrent transaction touching a
segment = 'silver'row would experience. - Name a fix that reduces the 2am blocking without a table lock.
Show the full walkthrough
Root cause of the misconception: the belief that "a multi-row UPDATE is internally a table lock." It isn't. A row-locking database takes exclusive locks only on the rows matching the WHERE clause.
Step 1 — What actually locks. The 40,000 segment = 'gold' rows get row-level exclusive locks. The other ~4,960,000
rows are untouched and fully available.
Step 2 — Is the diagnosis right? No. It's 40,000 row locks, not a table
lock. A true table lock would come from something like ALTER TABLE, not
this UPDATE.
Step 3 — Predict the concurrent 'silver' transaction. It succeeds without waiting — its rows don't match the WHERE clause, so they're never locked. (A transaction touching a gold row would block until the job commits.) So the "whole app frozen" story doesn't fit a row-lock UPDATE; if the app really freezes for everyone, look elsewhere — a long single transaction holding locks too long, lock escalation under memory pressure, or a genuine table-level operation running alongside.
Step 4 — A fix. Batch the update into chunks so each transaction holds fewer locks for less time, and run it during the lowest-traffic window (check the traffic dashboard). If reads must never block, ensure the DB uses MVCC so readers see the last committed version while the job writes.
-- Commit in chunks so locks release often instead of
-- holding 40,000 row locks in one long transaction.
UPDATE members
SET points = points + monthly_bonus
WHERE segment = 'gold'
AND member_id BETWEEN :lo AND :hi; -- loop the range, commit each batchWhy: row-level UPDATE locks only matched rows — knowing that stops you from blaming a "table lock," and points you at lock-hold time and MVCC as the real levers.