[{"data":1,"prerenderedAt":1784},["ShallowReactive",2],{"article-en-\u002Fdatabases\u002Ftwo-phase-locking":3,"article-sibling-en-\u002Fdatabases\u002Ftwo-phase-locking":929,"surround-en-\u002Fdatabases\u002Ftwo-phase-locking":1744,"related-en-\u002Fdatabases\u002Ftwo-phase-locking":1751},{"id":4,"title":5,"body":6,"date":908,"description":909,"draft":910,"extension":911,"img":912,"meta":913,"navigation":914,"path":915,"seo":916,"slug":917,"stem":918,"tags":919,"topics":925,"__hash__":928},"content\u002Fdatabases\u002F1.two-phase-locking.md","How Two-Phase Locking Keeps Concurrent Bank Transactions From Corrupting Your Data",{"type":7,"value":8,"toc":896},"minimark",[9,13,16,21,24,124,132,136,164,175,185,188,202,206,209,225,249,255,270,281,295,298,310,342,352,356,362,377,384,424,431,435,446,460,471,477,480,488,505,511,525,528,532,539,546,550,564,567,589,592,596,603,655,658,683,693,699,710,714,717,727,844,851,865,876,882,886,889,892],[10,11,12],"p",{},"A customer taps \"send\" on a 100 transfer. In the same millisecond, on the other side of the world, a second transfer lands on the same account. Both read the balance, both add their amount, both write back. If the database lets those two operations interleave naively, one of the two payments is silently erased. At the scale of a payment processor that settles millions of transactions an hour, that is not a bug, it is a financial hole.",[10,14,15],{},"This is the sharp end of a deep idea: concurrency control. This article grew out of a technical interview with Djamo, a mobile-first bank, where exactly this class of question came up: how do you keep a ledger correct when many transfers hit the same account at once? So the framing here is money, where correctness is not optional. Collaborative document editing has a similar flavor, but that is usually coordinated at the application layer by protocols like WOPI, not by the database. So we go straight to where concurrency is really solved: inside the database engine, and concretely inside PostgreSQL.",[17,18,20],"h2",{"id":19},"the-nightmare-of-simultaneous-access","The nightmare of simultaneous access",[10,22,23],{},"Account X starts with a balance of 1000. Transaction T1 deposits 100. In the same instant, T2 deposits 200. Each transfer is really three steps: read the balance, compute balance + amount, write it back. Interleave them badly and watch what happens:",[25,26,27,46],"table",{},[28,29,30],"thead",{},[31,32,33,37,40,43],"tr",{},[34,35,36],"th",{},"Time",[34,38,39],{},"T1 (deposit 100)",[34,41,42],{},"T2 (deposit 200)",[34,44,45],{},"Balance in DB",[47,48,49,63,74,86,98,111],"tbody",{},[31,50,51,55,58,60],{},[52,53,54],"td",{},"t1",[52,56,57],{},"read X (1000)",[52,59],{},[52,61,62],{},"1000",[31,64,65,68,70,72],{},[52,66,67],{},"t2",[52,69],{},[52,71,57],{},[52,73,62],{},[31,75,76,79,82,84],{},[52,77,78],{},"t3",[52,80,81],{},"compute 1000 + 100",[52,83],{},[52,85,62],{},[31,87,88,91,93,96],{},[52,89,90],{},"t4",[52,92],{},[52,94,95],{},"compute 1000 + 200",[52,97,62],{},[31,99,100,103,106,108],{},[52,101,102],{},"t5",[52,104,105],{},"write X = 1100",[52,107],{},[52,109,110],{},"1100",[31,112,113,116,118,121],{},[52,114,115],{},"t6",[52,117],{},[52,119,120],{},"write X = 1200",[52,122,123],{},"1200",[10,125,126,127,131],{},"The final balance is 1200. It should be 1300. T1's deposit evaporated because T2 read the balance before T1 wrote its result, so T2 overwrote it. This is the ",[128,129,130],"strong",{},"lost update",". Neither transaction is wrong on its own. The interleaving is the bug, and no amount of careful application code fixes it, because the two requests may run on different servers with no knowledge of each other. Only the shared component underneath, the database, can arbitrate.",[17,133,135],{"id":134},"the-abstraction-the-transaction-model","The abstraction: the transaction model",[10,137,138,139,142,143,146,147,150,151,154,155,159,160,163],{},"To reason about this without drowning in disk pages and B-trees, we use the ",[128,140,141],{},"transaction model",". The system treats each piece of data as an opaque ",[128,144,145],{},"entity"," (a row, a page, a key) and reduces every transaction to two primitive actions: ",[128,148,149],{},"read"," and ",[128,152,153],{},"write",". We write ",[156,157,158],"code",{},"r1[X]"," for \"T1 reads X\" and ",[156,161,162],{},"w2[X]"," for \"T2 writes X\".",[10,165,166,167,170,171,174],{},"A transaction Ti is then just an ordered sequence of such operations, ending in a commit ",[156,168,169],{},"c_i"," or an abort ",[156,172,173],{},"a_i",". Our lost update becomes a compact string:",[176,177,182],"pre",{"className":178,"code":180,"language":181},[179],"language-text","r1[X]  r2[X]  w1[X]  w2[X]\n","text",[156,183,180],{"__ignoreMap":184},"",[10,186,187],{},"That is the entire problem, reduced to its skeleton. Two reads of X precede both writes, so the second write clobbers the first. Concurrency control is the discipline that decides which such interleavings are legal.",[10,189,190,191,194,195,197,198,201],{},"Formally, a ",[128,192,193],{},"schedule"," S over a set of transactions is an interleaving of all their operations that preserves the internal order of each transaction. If T1 does ",[156,196,158],{}," before ",[156,199,200],{},"w1[X]",", every schedule must keep that order. What it may choose is where T2's operations slot in between.",[17,203,205],{"id":204},"the-math-conflict-serializability","The math: conflict-serializability",[10,207,208],{},"We need a precise definition of \"correct\", and force-brute serial execution gives us the reference point.",[10,210,211,212,215,216,220,221,224],{},"A ",[128,213,214],{},"serial schedule"," runs each transaction to completion before starting the next: T1 fully, then T2, then T3. It is trivially correct, because nothing overlaps, so no anomaly is possible. The price is zero parallelism, which no payment system can afford. So we want a schedule that runs concurrently yet behaves ",[217,218,219],"em",{},"as if"," it were serial. That property is ",[128,222,223],{},"serializability",", and here is how we make it checkable.",[10,226,227,228,231,232,235,236,239,240,235,242,150,244,235,246,248],{},"Two operations ",[128,229,230],{},"conflict"," when they belong to different transactions, touch the same entity, and at least one is a write. So ",[156,233,234],{},"r","\u002F",[156,237,238],{},"w",", ",[156,241,238],{},[156,243,234],{},[156,245,238],{},[156,247,238],{}," on the same entity conflict, but two reads never do. Formally, for a schedule S:",[176,250,253],{"className":251,"code":252,"language":181},[179],"conflict(op_i, op_j)  ⇔  entity(op_i) = entity(op_j)\n                          ∧ Ti ≠ Tj\n                          ∧ (op_i is a write ∨ op_j is a write)\n",[156,254,252],{"__ignoreMap":184},[10,256,257,258,261,262,265,266,269],{},"Two schedules are ",[128,259,260],{},"conflict-equivalent"," when they contain the same operations and order every pair of conflicting operations the same way. A schedule is ",[128,263,264],{},"conflict-serializable"," when it is conflict-equivalent to ",[217,267,268],{},"some"," serial schedule. Non-conflicting operations may be freely reordered, so we only care about the relative order of the conflicting pairs.",[10,271,272,273,276,277,280],{},"The tool that decides this is the ",[128,274,275],{},"precedence graph"," (also called the serialization graph) ",[156,278,279],{},"SG(S)",":",[282,283,284,288],"ul",{},[285,286,287],"li",{},"one node per committed transaction,",[285,289,290,291,294],{},"a directed edge ",[156,292,293],{},"Ti → Tj"," whenever an operation of Ti conflicts with, and comes before, an operation of Tj in S.",[10,296,297],{},"Now the foundational result of the whole field:",[299,300,301],"blockquote",{},[10,302,303,306,307,309],{},[128,304,305],{},"Serializability theorem."," A schedule S is conflict-serializable if and only if its precedence graph ",[156,308,279],{}," is acyclic.",[10,311,312,313,315,316,319,320,323,324,327,328,197,330,332,333,327,336,197,339,341],{},"The intuition is clean. An edge ",[156,314,293],{}," means \"Ti must come before Tj in any equivalent serial order\". If the graph is acyclic, a ",[128,317,318],{},"topological sort"," of it produces exactly that serial order. If there is a cycle ",[156,321,322],{},"Ti → Tj → ... → Ti",", then Ti must come strictly before itself, which is impossible, so no equivalent serial order exists. Our lost update has edges ",[156,325,326],{},"T1 → T2"," (from ",[156,329,158],{},[156,331,162],{},") and ",[156,334,335],{},"T2 → T1",[156,337,338],{},"r2[X]",[156,340,200],{},"), forming a 2-cycle. Cycle means not serializable, which is the formal fingerprint of the bug.",[10,343,344,345,347,348,351],{},"Checking acyclicity of a graph is cheap. The catch is that building ",[156,346,279],{}," requires knowing the whole schedule, and a busy database runs thousands of transactions per second with no idea what arrives next. We need a rule that guarantees acyclicity ",[217,349,350],{},"while the schedule is still being produced",", without ever constructing the graph. That rule is locking.",[17,353,355],{"id":354},"locks-and-their-two-modes","Locks and their two modes",[10,357,211,358,361],{},[128,359,360],{},"lock"," is a claim a transaction places on an entity before touching it. Crucially there are two modes, and the distinction is what allows any parallelism at all.",[282,363,364,370],{},[285,365,211,366,369],{},[128,367,368],{},"shared lock (S)",", or read lock: \"I am reading this, others may read it too, but nobody may write it.\" Many transactions can hold it on the same entity at once.",[285,371,372,373,376],{},"An ",[128,374,375],{},"exclusive lock (X)",", or write lock: \"I am writing this, nobody else may read or write it.\" Only one holder, and no shared lock may coexist.",[10,378,379,380,383],{},"This collapses into a ",[128,381,382],{},"compatibility matrix",". A lock request is granted only if it is compatible with every lock already held on that entity:",[25,385,386,400],{},[28,387,388],{},[31,389,390,393,397],{},[34,391,392],{},"Held \\ Requested",[34,394,396],{"align":395},"center","Shared (S)",[34,398,399],{"align":395},"Exclusive (X)",[47,401,402,414],{},[31,403,404,408,411],{},[52,405,406],{},[128,407,396],{},[52,409,410],{"align":395},"compatible",[52,412,413],{"align":395},"wait",[31,415,416,420,422],{},[52,417,418],{},[128,419,399],{},[52,421,413],{"align":395},[52,423,413],{"align":395},[10,425,426,427,430],{},"Two readers coexist, a writer excludes everyone. This is why read-heavy workloads scale under locking while write contention does not. An incompatible request ",[128,428,429],{},"blocks"," until the conflicting lock is released.",[17,432,434],{"id":433},"the-golden-rule-two-phase-locking","The golden rule: two-phase locking",[10,436,437,438,441,442,445],{},"Locks alone do not guarantee serializability. You can acquire and release them in an order that still produces the lost update. The discipline that fixes it is ",[128,439,440],{},"two-phase locking (2PL)",", a rule about ",[217,443,444],{},"when"," you may acquire and release, splitting every transaction into two phases:",[282,447,448,454],{},[285,449,450,453],{},[128,451,452],{},"Growing phase (acquisition)."," The transaction may acquire locks in any order, but may not release any.",[285,455,456,459],{},[128,457,458],{},"Shrinking phase (release)."," The instant it releases its first lock, it flips to shrinking. From then on it may only release, never acquire.",[10,461,462,463,466,467,470],{},"Plot the number of locks a transaction holds over time. It rises monotonically to a peak, then falls monotonically. That peak is the ",[128,464,465],{},"lock point"," ",[156,468,469],{},"LP(Ti)",", the instant the transaction owns everything it will ever need.",[176,472,475],{"className":473,"code":474,"language":181},[179],"locks\nheld  |          \u002F\\\n      |         \u002F  \\\n      |        \u002F    \\\n      |       \u002F      \\___\n      |______\u002F           \\____\n      +--------------------------> time\n        growing | lock  | shrinking\n                | point |\n",[156,476,474],{"__ignoreMap":184},[10,478,479],{},"Why accept a rule that clearly holds locks longer than needed? Because of the payoff:",[299,481,482],{},[10,483,484,487],{},[128,485,486],{},"2PL theorem."," If every transaction obeys the two-phase rule, every schedule the system produces is conflict-serializable.",[10,489,490,491,494,495,497,498,500,501,504],{},"Here is the proof sketch, and it is worth following because it shows ",[217,492,493],{},"why"," the rule works. Suppose the precedence graph had an edge ",[156,496,293],{},". That edge exists because some operation of Ti conflicted with a later operation of Tj on a shared entity. For both to touch that entity in conflict, Ti must have released its lock and Tj must have acquired one afterward. Releasing means Ti was already in its shrinking phase, so ",[156,499,469],{}," came before Tj's acquisition. Acquiring means Tj was still growing, so that acquisition came before ",[156,502,503],{},"LP(Tj)",". Chaining these:",[176,506,509],{"className":507,"code":508,"language":181},[179],"Ti → Tj  in SG(S)   ⇒   LP(Ti) \u003C LP(Tj)\n",[156,510,508],{"__ignoreMap":184},[10,512,513,514,517,518,521,522,524],{},"Every edge in the precedence graph strictly increases the lock point. A cycle ",[156,515,516],{},"Ti → ... → Ti"," would therefore force ",[156,519,520],{},"LP(Ti) \u003C LP(Ti)",", a contradiction. So the graph has no cycles, and by the serializability theorem the schedule is conflict-serializable. The lock points themselves give the equivalent serial order: sort the transactions by ",[156,523,469],{},".",[10,526,527],{},"That is the whole magic. We replaced an impossible runtime graph analysis with a purely local rule, \"grow then shrink\", and got global serializability as a theorem.",[17,529,531],{"id":530},"pessimistic-versus-optimistic","Pessimistic versus optimistic",[10,533,534,535,538],{},"2PL is the flagship of the ",[128,536,537],{},"pessimistic"," strategy: assume conflict is likely, so lock the data before touching it and make everyone else wait. It is the right default when contention is high, exactly the case of a hot account that many transfers hit at once.",[10,540,541,542,545],{},"The opposite philosophy is ",[128,543,544],{},"optimistic",": assume conflict is rare, let transactions run without blocking, and check for conflicts only at commit time, aborting the loser if two genuinely collided. It wins when contention is low, because it pays no locking overhead on the common path. Real databases offer both, and PostgreSQL is the clearest example, as we will see. First we have to fix a flaw in plain 2PL.",[17,547,549],{"id":548},"why-plain-2pl-is-not-enough-strict-2pl","Why plain 2PL is not enough: strict 2PL",[10,551,552,553,150,556,559,560,563],{},"Basic 2PL guarantees serializability but still permits ",[128,554,555],{},"dirty reads",[128,557,558],{},"cascading aborts",". Suppose T1 enters its shrinking phase, releases the lock on X, and only ",[217,561,562],{},"later"," aborts. In the gap, T2 read the uncommitted value of X. When T1 rolls back, T2 is built on data that never officially existed, so T2 must abort too, and anything that read from T2 aborts in turn. One failure cascades.",[10,565,566],{},"For a bank ledger this is intolerable, so real engines delay releasing locks:",[282,568,569,579],{},[285,570,571,574,575,578],{},[128,572,573],{},"Strict 2PL (S2PL)."," Hold every ",[128,576,577],{},"exclusive"," lock until commit or abort. Shared locks may still drop during shrinking. This alone kills cascading aborts: nobody can read your writes until you have committed them.",[285,580,581,584,585,588],{},[128,582,583],{},"Rigorous 2PL."," Hold ",[128,586,587],{},"all"," locks, shared and exclusive, until commit. Slightly less concurrency, simpler to reason about.",[10,590,591],{},"When a database says \"two-phase locking\", it almost always means strict or rigorous 2PL. The commit becomes the single instant where all writes become visible and all locks drop, which is exactly the \"all or nothing\" visibility you expect from a transaction.",[17,593,595],{"id":594},"the-price-deadlocks","The price: deadlocks",[10,597,598,599,602],{},"Holding locks until commit introduces the ",[128,600,601],{},"deadlock",". Two transfers touching two accounts in opposite order is enough:",[25,604,605,617],{},[28,606,607],{},[31,608,609,611,614],{},[34,610,36],{},[34,612,613],{},"T1",[34,615,616],{},"T2",[47,618,619,628,637,646],{},[31,620,621,623,626],{},[52,622,54],{},[52,624,625],{},"lock A (granted)",[52,627],{},[31,629,630,632,634],{},[52,631,67],{},[52,633],{},[52,635,636],{},"lock B (granted)",[31,638,639,641,644],{},[52,640,78],{},[52,642,643],{},"request B ... waits",[52,645],{},[31,647,648,650,652],{},[52,649,90],{},[52,651],{},[52,653,654],{},"request A ... waits",[10,656,657],{},"T1 holds A and wants B, T2 holds B and wants A. Neither yields. There are two responses.",[10,659,660,663,664,667,668,670,671,674,675,678,679,682],{},[128,661,662],{},"Detection."," The engine keeps a ",[128,665,666],{},"wait-for graph",": an edge ",[156,669,293],{}," means Ti is waiting on a lock Tj holds. A ",[128,672,673],{},"cycle"," is a deadlock. The engine picks a ",[128,676,677],{},"victim"," (typically the cheapest to undo), aborts it, releases its locks, and lets the survivor finish. The victim retries later. PostgreSQL does exactly this and surfaces a ",[156,680,681],{},"deadlock detected"," error, so your application code must be ready to retry it.",[10,684,685,688,689,692],{},[128,686,687],{},"Prevention."," Timestamp schemes stop cycles from forming. Give each transaction a birth timestamp ",[156,690,691],{},"TS(Ti)",", where older means smaller. When Ti requests a lock held by Tj:",[176,694,697],{"className":695,"code":696,"language":181},[179],"wait-die:    if TS(Ti) \u003C TS(Tj)  then Ti waits      (older waits)\n             else                     Ti aborts     (younger dies)\n\nwound-wait:  if TS(Ti) \u003C TS(Tj)  then Tj aborts     (older wounds younger)\n             else                     Ti waits       (younger waits)\n",[156,698,696],{"__ignoreMap":184},[10,700,701,702,705,706,709],{},"Both are deadlock-free because every waiting edge points consistently from older to younger (wait-die) or younger to older (wound-wait), which can never form a cycle. Because a restarted transaction keeps its original ",[156,703,704],{},"TS",", it grows older over time and eventually wins, so no transaction starves. A cruder cousin is the plain ",[128,707,708],{},"lock timeout",": abort after waiting N seconds. Imprecise, but it needs neither graph nor timestamps.",[17,711,713],{"id":712},"postgresql-in-the-real-world","PostgreSQL in the real world",[10,715,716],{},"PostgreSQL is worth grounding in because it offers the pessimistic and the optimistic path in one engine.",[10,718,719,722,723,726],{},[128,720,721],{},"The pessimistic path."," You take locks explicitly. ",[156,724,725],{},"SELECT ... FOR UPDATE"," acquires a row-level exclusive lock, which is precisely the tool that fixes our opening transfer bug:",[176,728,732],{"className":729,"code":730,"language":731,"meta":184,"style":184},"language-sql shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","BEGIN;\nSELECT balance FROM accounts WHERE id = 'X' FOR UPDATE;  -- exclusive row lock\nUPDATE accounts SET balance = balance + 100 WHERE id = 'X';\nCOMMIT;  -- strict-2PL: lock released only here\n","sql",[156,733,734,747,795,833],{"__ignoreMap":184},[735,736,739,743],"span",{"class":737,"line":738},"line",1,[735,740,742],{"class":741},"sbssI","BEGIN",[735,744,746],{"class":745},"sTEyZ",";\n",[735,748,750,753,756,759,762,765,768,772,775,779,782,785,788,791],{"class":737,"line":749},2,[735,751,752],{"class":741},"SELECT",[735,754,755],{"class":745}," balance ",[735,757,758],{"class":741},"FROM",[735,760,761],{"class":745}," accounts ",[735,763,764],{"class":741},"WHERE",[735,766,767],{"class":745}," id ",[735,769,771],{"class":770},"sMK4o","=",[735,773,774],{"class":770}," '",[735,776,778],{"class":777},"sfazB","X",[735,780,781],{"class":770},"'",[735,783,784],{"class":741}," FOR",[735,786,787],{"class":741}," UPDATE",[735,789,790],{"class":745},";  ",[735,792,794],{"class":793},"sHwdD","-- exclusive row lock\n",[735,796,798,801,803,806,808,810,812,815,818,821,823,825,827,829,831],{"class":737,"line":797},3,[735,799,800],{"class":741},"UPDATE",[735,802,761],{"class":745},[735,804,805],{"class":741},"SET",[735,807,755],{"class":745},[735,809,771],{"class":770},[735,811,755],{"class":745},[735,813,814],{"class":770},"+",[735,816,817],{"class":741}," 100",[735,819,820],{"class":741}," WHERE",[735,822,767],{"class":745},[735,824,771],{"class":770},[735,826,774],{"class":770},[735,828,778],{"class":777},[735,830,781],{"class":770},[735,832,746],{"class":745},[735,834,836,839,841],{"class":737,"line":835},4,[735,837,838],{"class":741},"COMMIT",[735,840,790],{"class":745},[735,842,843],{"class":793},"-- strict-2PL: lock released only here\n",[10,845,846,847,850],{},"With ",[156,848,849],{},"FOR UPDATE",", T2 cannot even read X until T1 commits, so the lost update is impossible. This is textbook strict 2PL, applied by hand where you know contention is high.",[10,852,853,856,857,860,861,864],{},[128,854,855],{},"The optimistic path."," For everyday reads, PostgreSQL does not take read locks at all. It uses ",[128,858,859],{},"multiversion concurrency control (MVCC)",": a writer creates a ",[217,862,863],{},"new version"," of a row rather than overwriting it, so readers see a consistent snapshot and never block on writers, and writers never block on readers. Writes still take exclusive row locks, so write-write conflicts remain serialized, but the common read path is lock-free.",[10,866,867,868,871,872,875],{},"At the ",[156,869,870],{},"SERIALIZABLE"," isolation level, PostgreSQL layers ",[128,873,874],{},"serializable snapshot isolation (SSI)"," on top. Rather than holding long read locks like 2PL, SSI lets transactions run optimistically, watches for the dangerous read-write patterns that would create a cycle in the precedence graph, and aborts one transaction with a serialization failure if it detects one. It is the same acyclicity guarantee from our theorem, enforced optimistically instead of pessimistically. Your code retries the aborted transaction, exactly as it retries a deadlock victim.",[10,877,878,879,881],{},"The four SQL isolation levels (read uncommitted, read committed, repeatable read, serializable) are really a dial for how much of this machinery is engaged. ",[156,880,870],{}," is the only one that promises true conflict-serializable behavior, and you can reach it in PostgreSQL either pessimistically with explicit locks or optimistically with SSI. Lock-based engines like MySQL InnoDB and SQL Server lean harder on the 2PL side of that spectrum.",[17,883,885],{"id":884},"conclusion","Conclusion",[10,887,888],{},"Concurrency control is invisible to the end user and absolutely central to any system where the data is money. Two-phase locking is the idea that made it tractable: it replaces an impossible runtime check, \"is the precedence graph acyclic\", with a simple local rule, \"grow then shrink\", and the theorem proves the two are equivalent. Strict 2PL hardens it against cascading aborts, deadlock detection and timestamp prevention keep it from freezing, and MVCC with SSI offers the optimistic alternative for the read-heavy common case.",[10,890,891],{},"Thanks to this machinery, the 100 transfer that vanished in our first example never actually vanishes. A rule you never see, enforced pessimistically by a lock or optimistically by a version check, refuses to let it.",[893,894,895],"style",{},"html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":184,"searchDepth":749,"depth":749,"links":897},[898,899,900,901,902,903,904,905,906,907],{"id":19,"depth":749,"text":20},{"id":134,"depth":749,"text":135},{"id":204,"depth":749,"text":205},{"id":354,"depth":749,"text":355},{"id":433,"depth":749,"text":434},{"id":530,"depth":749,"text":531},{"id":548,"depth":749,"text":549},{"id":594,"depth":749,"text":595},{"id":712,"depth":749,"text":713},{"id":884,"depth":749,"text":885},"2026-07-22","Two transfers hit the same account in the same millisecond and one payment vanishes. This is the lost update, and it is why every payment ledger runs on a concurrency control layer. This article builds the theory from a fintech example up to two-phase locking (2PL): the transaction model, the math of conflict-serializability and the precedence graph, why the 2PL theorem guarantees correctness, pessimistic versus optimistic control in PostgreSQL, strict 2PL, and how deadlocks are detected and prevented.",false,"md","https:\u002F\u002Fres.cloudinary.com\u002Fdpdwhd6ka\u002Fimage\u002Fupload\u002Ff_auto,q_auto\u002Fv1\u002FBlog\u002Fimages\u002Fhbcudyxllyjvbkjxvs7g",{},true,"\u002Fdatabases\u002Ftwo-phase-locking",{"title":5,"description":909},"two-phase-locking-concurrency-control","databases\u002F1.two-phase-locking",[920,921,922,923,924],"Databases","Concurrency","Transactions","PostgreSQL","Distributed-Systems",[926,927],"databases","backend","rhcR7gsBbPPRhLX7aIvobFE-qLAMsImaPCECSk2PxmY",{"id":930,"title":931,"body":932,"date":908,"description":1736,"draft":910,"extension":911,"img":912,"meta":1737,"navigation":914,"path":1738,"seo":1739,"slug":917,"stem":1740,"tags":1741,"topics":1742,"__hash__":1743},"content\u002Fdatabases\u002F1.two-phase-locking.fr.md","Comment le verrouillage à deux phases empêche vos transactions bancaires concurrentes de corrompre vos données",{"type":7,"value":933,"toc":1724},[934,937,940,944,947,1032,1039,1043,1068,1077,1082,1085,1098,1102,1105,1120,1141,1147,1162,1172,1183,1186,1197,1225,1235,1239,1245,1259,1266,1304,1311,1315,1326,1340,1349,1355,1358,1366,1382,1388,1399,1402,1406,1413,1420,1424,1439,1442,1464,1467,1471,1478,1528,1531,1554,1563,1569,1579,1583,1586,1595,1678,1684,1698,1708,1714,1716,1719,1722],[10,935,936],{},"Un client appuie sur « envoyer » pour un virement de 100. À la même milliseconde, à l'autre bout du monde, un second virement arrive sur le même compte. Les deux lisent le solde, les deux ajoutent leur montant, les deux réécrivent. Si la base de données laisse ces deux opérations s'entrelacer naïvement, un des deux paiements est silencieusement effacé. À l'échelle d'un processeur de paiement qui règle des millions de transactions par heure, ce n'est pas un bug, c'est un trou financier.",[10,938,939],{},"C'est la pointe acérée d'une idée profonde : le contrôle de concurrence. Cet article est né d'un entretien technique avec Djamo, une banque mobile, où exactement cette classe de question s'est posée : comment garder un registre correct quand de nombreux virements frappent le même compte à la fois ? Le cadrage ici est donc l'argent, là où la correction n'est pas optionnelle. L'édition collaborative de documents a une saveur voisine, mais elle est généralement coordonnée au niveau applicatif par des protocoles comme WOPI, pas par la base. On va donc droit là où la concurrence est réellement résolue : au cœur du moteur de base de données, et concrètement dans PostgreSQL.",[17,941,943],{"id":942},"le-cauchemar-des-accès-simultanés","Le cauchemar des accès simultanés",[10,945,946],{},"Le compte X démarre avec un solde de 1000. La transaction T1 dépose 100. Au même instant, T2 dépose 200. Chaque virement se fait en réalité en trois étapes : lire le solde, calculer solde + montant, réécrire. Entrelacez-les maladroitement et observez :",[25,948,949,965],{},[28,950,951],{},[31,952,953,956,959,962],{},[34,954,955],{},"Temps",[34,957,958],{},"T1 (dépôt 100)",[34,960,961],{},"T2 (dépôt 200)",[34,963,964],{},"Solde en base",[47,966,967,978,988,999,1010,1021],{},[31,968,969,971,974,976],{},[52,970,54],{},[52,972,973],{},"lire X (1000)",[52,975],{},[52,977,62],{},[31,979,980,982,984,986],{},[52,981,67],{},[52,983],{},[52,985,973],{},[52,987,62],{},[31,989,990,992,995,997],{},[52,991,78],{},[52,993,994],{},"calculer 1000 + 100",[52,996],{},[52,998,62],{},[31,1000,1001,1003,1005,1008],{},[52,1002,90],{},[52,1004],{},[52,1006,1007],{},"calculer 1000 + 200",[52,1009,62],{},[31,1011,1012,1014,1017,1019],{},[52,1013,102],{},[52,1015,1016],{},"écrire X = 1100",[52,1018],{},[52,1020,110],{},[31,1022,1023,1025,1027,1030],{},[52,1024,115],{},[52,1026],{},[52,1028,1029],{},"écrire X = 1200",[52,1031,123],{},[10,1033,1034,1035,1038],{},"Le solde final est 1200. Il devrait être 1300. Le dépôt de T1 s'est évaporé parce que T2 a lu le solde avant que T1 n'écrive son résultat, donc T2 l'a écrasé. C'est la ",[128,1036,1037],{},"mise à jour perdue"," (lost update). Aucune des deux transactions n'est fausse en soi. C'est l'entrelacement qui est le bug, et aucun code applicatif soigné ne le corrige, car les deux requêtes peuvent tourner sur des serveurs différents qui s'ignorent. Seul le composant partagé en dessous, la base de données, peut arbitrer.",[17,1040,1042],{"id":1041},"labstraction-le-modèle-de-transaction","L'abstraction : le modèle de transaction",[10,1044,1045,1046,1049,1050,1053,1054,1057,1058,1061,1062,1064,1065,1067],{},"Pour raisonner là-dessus sans se noyer dans les pages disque et les B-trees, on utilise le ",[128,1047,1048],{},"modèle de transaction",". Le système traite chaque donnée comme une ",[128,1051,1052],{},"entité"," opaque (une ligne, une page, une clé) et réduit toute transaction à deux actions primitives : ",[128,1055,1056],{},"lire"," (read) et ",[128,1059,1060],{},"écrire"," (write). On note ",[156,1063,158],{}," pour « T1 lit X » et ",[156,1066,162],{}," pour « T2 écrit X ».",[10,1069,1070,1071,1073,1074,1076],{},"Une transaction Ti n'est alors qu'une séquence ordonnée de telles opérations, terminée par un commit ",[156,1072,169],{}," ou un abandon ",[156,1075,173],{},". Notre mise à jour perdue devient une chaîne compacte :",[176,1078,1080],{"className":1079,"code":180,"language":181},[179],[156,1081,180],{"__ignoreMap":184},[10,1083,1084],{},"Voilà tout le problème, réduit à son squelette. Deux lectures de X précèdent les deux écritures, donc la seconde écriture écrase la première. Le contrôle de concurrence est la discipline qui décide quels entrelacements sont légaux.",[10,1086,1087,1088,1091,1092,1094,1095,1097],{},"Formellement, un ",[128,1089,1090],{},"ordonnancement"," (schedule) S sur un ensemble de transactions est un entrelacement de toutes leurs opérations qui préserve l'ordre interne de chaque transaction. Si T1 fait ",[156,1093,158],{}," avant ",[156,1096,200],{},", tout ordonnancement doit garder cet ordre. Ce qu'il peut choisir, c'est où insérer les opérations de T2 entre les deux.",[17,1099,1101],{"id":1100},"les-maths-la-conflit-sérialisabilité","Les maths : la conflit-sérialisabilité",[10,1103,1104],{},"Il nous faut une définition précise de « correct », et l'exécution sérielle en force brute donne le point de référence.",[10,1106,1107,1108,1111,1112,1115,1116,1119],{},"Un ",[128,1109,1110],{},"ordonnancement sériel"," exécute chaque transaction jusqu'au bout avant de démarrer la suivante : T1 en entier, puis T2, puis T3. Il est trivialement correct, car rien ne se chevauche, donc aucune anomalie n'est possible. Le prix est un parallélisme nul, qu'aucun système de paiement ne peut se permettre. On veut donc un ordonnancement qui s'exécute en parallèle tout en se comportant ",[217,1113,1114],{},"comme si"," il était sériel. Cette propriété est la ",[128,1117,1118],{},"sérialisabilité",", et voici comment la rendre vérifiable.",[10,1121,1122,1123,1126,1127,235,1129,239,1131,235,1133,1135,1136,235,1138,1140],{},"Deux opérations sont ",[128,1124,1125],{},"en conflit"," quand elles appartiennent à des transactions différentes, touchent la même entité, et qu'au moins l'une est une écriture. Donc ",[156,1128,234],{},[156,1130,238],{},[156,1132,238],{},[156,1134,234],{}," et ",[156,1137,238],{},[156,1139,238],{}," sur la même entité sont en conflit, mais deux lectures ne le sont jamais. Formellement, pour un ordonnancement S :",[176,1142,1145],{"className":1143,"code":1144,"language":181},[179],"conflit(op_i, op_j)  ⇔  entité(op_i) = entité(op_j)\n                         ∧ Ti ≠ Tj\n                         ∧ (op_i est une écriture ∨ op_j est une écriture)\n",[156,1146,1144],{"__ignoreMap":184},[10,1148,1149,1150,1153,1154,1157,1158,1161],{},"Deux ordonnancements sont ",[128,1151,1152],{},"conflit-équivalents"," quand ils contiennent les mêmes opérations et ordonnent de la même façon chaque paire d'opérations en conflit. Un ordonnancement est ",[128,1155,1156],{},"conflit-sérialisable"," quand il est conflit-équivalent à ",[217,1159,1160],{},"un"," ordonnancement sériel. Les opérations sans conflit peuvent être librement réordonnées, donc seul compte l'ordre relatif des paires en conflit.",[10,1163,1164,1165,1168,1169,1171],{},"L'outil qui tranche cela est le ",[128,1166,1167],{},"graphe de précédence"," (aussi appelé graphe de sérialisation) ",[156,1170,279],{}," :",[282,1173,1174,1177],{},[285,1175,1176],{},"un nœud par transaction commitée,",[285,1178,1179,1180,1182],{},"une arête orientée ",[156,1181,293],{}," dès qu'une opération de Ti est en conflit avec, et précède, une opération de Tj dans S.",[10,1184,1185],{},"Voici maintenant le résultat fondateur de tout le domaine :",[299,1187,1188],{},[10,1189,1190,1193,1194,1196],{},[128,1191,1192],{},"Théorème de sérialisabilité."," Un ordonnancement S est conflit-sérialisable si et seulement si son graphe de précédence ",[156,1195,279],{}," est acyclique.",[10,1198,1199,1200,1202,1203,1206,1207,1209,1210,1212,1213,1094,1215,1217,1218,1212,1220,1094,1222,1224],{},"L'intuition est limpide. Une arête ",[156,1201,293],{}," signifie « Ti doit précéder Tj dans tout ordre sériel équivalent ». Si le graphe est acyclique, un ",[128,1204,1205],{},"tri topologique"," produit exactement cet ordre sériel. S'il y a un cycle ",[156,1208,322],{},", alors Ti doit précéder strictement lui-même, ce qui est impossible, donc aucun ordre sériel équivalent n'existe. Notre mise à jour perdue a les arêtes ",[156,1211,326],{}," (via ",[156,1214,158],{},[156,1216,162],{},") et ",[156,1219,335],{},[156,1221,338],{},[156,1223,200],{},"), formant un cycle de longueur 2. Cycle signifie non sérialisable, ce qui est l'empreinte formelle du bug.",[10,1226,1227,1228,1230,1231,1234],{},"Vérifier l'acyclicité d'un graphe est peu coûteux. Le hic, c'est que construire ",[156,1229,279],{}," exige de connaître l'ordonnancement entier, or une base chargée exécute des milliers de transactions par seconde sans savoir ce qui arrive ensuite. Il nous faut une règle qui garantisse l'acyclicité ",[217,1232,1233],{},"pendant que l'ordonnancement se produit encore",", sans jamais construire le graphe. Cette règle, c'est le verrouillage.",[17,1236,1238],{"id":1237},"les-verrous-et-leurs-deux-modes","Les verrous et leurs deux modes",[10,1240,1107,1241,1244],{},[128,1242,1243],{},"verrou"," est une revendication qu'une transaction pose sur une entité avant d'y toucher. Il y a crucialement deux modes, et la distinction est ce qui permet le moindre parallélisme.",[282,1246,1247,1253],{},[285,1248,1107,1249,1252],{},[128,1250,1251],{},"verrou partagé (S)",", ou verrou de lecture : « je lis ceci, d'autres peuvent le lire aussi, mais personne ne peut l'écrire ». Plusieurs transactions peuvent le détenir sur la même entité à la fois.",[285,1254,1107,1255,1258],{},[128,1256,1257],{},"verrou exclusif (X)",", ou verrou d'écriture : « j'écris ceci, personne d'autre ne peut le lire ni l'écrire ». Un seul détenteur, et aucun verrou partagé ne peut coexister.",[10,1260,1261,1262,1265],{},"Cela se résume à une ",[128,1263,1264],{},"matrice de compatibilité",". Une demande de verrou n'est accordée que si elle est compatible avec tous les verrous déjà détenus sur cette entité :",[25,1267,1268,1281],{},[28,1269,1270],{},[31,1271,1272,1275,1278],{},[34,1273,1274],{},"Détenu \\ Demandé",[34,1276,1277],{"align":395},"Partagé (S)",[34,1279,1280],{"align":395},"Exclusif (X)",[47,1282,1283,1294],{},[31,1284,1285,1289,1291],{},[52,1286,1287],{},[128,1288,1277],{},[52,1290,410],{"align":395},[52,1292,1293],{"align":395},"attente",[31,1295,1296,1300,1302],{},[52,1297,1298],{},[128,1299,1280],{},[52,1301,1293],{"align":395},[52,1303,1293],{"align":395},[10,1305,1306,1307,1310],{},"Deux lecteurs coexistent, un rédacteur exclut tout le monde. C'est pourquoi les charges à dominante lecture passent à l'échelle sous verrouillage, et pas la contention en écriture. Une demande incompatible se ",[128,1308,1309],{},"bloque"," jusqu'à la libération du verrou conflictuel.",[17,1312,1314],{"id":1313},"la-règle-dor-le-verrouillage-à-deux-phases","La règle d'or : le verrouillage à deux phases",[10,1316,1317,1318,1321,1322,1325],{},"Les verrous seuls ne garantissent pas la sérialisabilité. On peut les acquérir et les relâcher dans un ordre qui produit encore la mise à jour perdue. La discipline qui corrige cela est le ",[128,1319,1320],{},"verrouillage à deux phases (2PL)",", une règle sur le ",[217,1323,1324],{},"moment"," où l'on peut acquérir et relâcher, qui scinde chaque transaction en deux phases :",[282,1327,1328,1334],{},[285,1329,1330,1333],{},[128,1331,1332],{},"Phase de croissance (acquisition)."," La transaction peut acquérir des verrous dans n'importe quel ordre, mais n'en relâche aucun.",[285,1335,1336,1339],{},[128,1337,1338],{},"Phase de décroissance (libération)."," À l'instant où elle relâche son premier verrou, elle bascule en décroissance. Dès lors, elle ne peut plus que relâcher, jamais acquérir.",[10,1341,1342,1343,466,1346,1348],{},"Traçez le nombre de verrous détenus au fil du temps. Il monte de façon monotone jusqu'à un sommet, puis redescend de façon monotone. Ce sommet est le ",[128,1344,1345],{},"point de verrou",[156,1347,469],{},", l'instant où la transaction possède tout ce dont elle aura besoin.",[176,1350,1353],{"className":1351,"code":1352,"language":181},[179],"verrous\ndétenus |          \u002F\\\n        |         \u002F  \\\n        |        \u002F    \\\n        |       \u002F      \\___\n        |______\u002F           \\____\n        +--------------------------> temps\n         croissance | point | décroissance\n                     | verrou|\n",[156,1354,1352],{"__ignoreMap":184},[10,1356,1357],{},"Pourquoi accepter une règle qui tient clairement les verrous plus longtemps que nécessaire ? À cause du gain :",[299,1359,1360],{},[10,1361,1362,1365],{},[128,1363,1364],{},"Théorème du 2PL."," Si toute transaction respecte la règle des deux phases, tout ordonnancement produit par le système est conflit-sérialisable.",[10,1367,1368,1369,1372,1373,1375,1376,1378,1379,1381],{},"Voici l'esquisse de preuve, qui vaut le détour car elle montre ",[217,1370,1371],{},"pourquoi"," la règle marche. Supposons que le graphe de précédence ait une arête ",[156,1374,293],{},". Cette arête existe parce qu'une opération de Ti est entrée en conflit avec une opération ultérieure de Tj sur une entité partagée. Pour que les deux touchent cette entité en conflit, Ti doit avoir relâché son verrou et Tj en avoir acquis un après. Relâcher signifie que Ti était déjà en décroissance, donc ",[156,1377,469],{}," a précédé l'acquisition de Tj. Acquérir signifie que Tj était encore en croissance, donc cette acquisition a précédé ",[156,1380,503],{},". En chaînant :",[176,1383,1386],{"className":1384,"code":1385,"language":181},[179],"Ti → Tj  dans SG(S)   ⇒   LP(Ti) \u003C LP(Tj)\n",[156,1387,1385],{"__ignoreMap":184},[10,1389,1390,1391,1393,1394,1396,1397,524],{},"Chaque arête du graphe de précédence fait strictement croître le point de verrou. Un cycle ",[156,1392,516],{}," forcerait donc ",[156,1395,520],{},", une contradiction. Le graphe est donc sans cycle, et par le théorème de sérialisabilité, l'ordonnancement est conflit-sérialisable. Les points de verrou eux-mêmes donnent l'ordre sériel équivalent : trions les transactions par ",[156,1398,469],{},[10,1400,1401],{},"Voilà toute la magie. On a remplacé une analyse de graphe impossible à l'exécution par une règle purement locale, « croître puis décroître », et obtenu la sérialisabilité globale comme théorème.",[17,1403,1405],{"id":1404},"pessimiste-contre-optimiste","Pessimiste contre optimiste",[10,1407,1408,1409,1412],{},"Le 2PL est le porte-drapeau de la stratégie ",[128,1410,1411],{},"pessimiste"," : supposer que le conflit est probable, donc verrouiller la donnée avant d'y toucher et faire attendre tous les autres. C'est le bon défaut quand la contention est forte, exactement le cas d'un compte chaud que de nombreux virements frappent en même temps.",[10,1414,1415,1416,1419],{},"La philosophie opposée est ",[128,1417,1418],{},"optimiste"," : supposer que le conflit est rare, laisser les transactions tourner sans blocage, et ne vérifier les conflits qu'au moment du commit, en abandonnant le perdant si deux se sont réellement heurtées. Elle gagne quand la contention est faible, car elle ne paie aucun coût de verrouillage sur le chemin courant. Les vraies bases offrent les deux, et PostgreSQL en est l'exemple le plus clair, comme on va le voir. Il faut d'abord corriger un défaut du 2PL simple.",[17,1421,1423],{"id":1422},"pourquoi-le-2pl-simple-ne-suffit-pas-le-2pl-strict","Pourquoi le 2PL simple ne suffit pas : le 2PL strict",[10,1425,1426,1427,1430,1431,1434,1435,1438],{},"Le 2PL de base garantit la sérialisabilité mais autorise encore les ",[128,1428,1429],{},"lectures sales"," (dirty reads) et les ",[128,1432,1433],{},"abandons en cascade",". Supposons que T1 entre en décroissance, relâche le verrou sur X, puis abandonne ",[217,1436,1437],{},"plus tard"," seulement. Dans l'intervalle, T2 a lu la valeur non commitée de X. Quand T1 est annulée, T2 est bâtie sur une donnée qui n'a jamais officiellement existé, T2 doit donc abandonner aussi, et tout ce qui a lu depuis T2 abandonne à son tour. Un seul échec se propage en cascade.",[10,1440,1441],{},"Pour un registre bancaire c'est intolérable, donc les moteurs réels retardent la libération des verrous :",[282,1443,1444,1454],{},[285,1445,1446,1449,1450,1453],{},[128,1447,1448],{},"2PL strict (S2PL)."," Garder tout verrou ",[128,1451,1452],{},"exclusif"," jusqu'au commit ou à l'abandon. Les verrous partagés peuvent encore tomber en décroissance. Cela suffit à tuer les abandons en cascade : personne ne peut lire vos écritures tant que vous ne les avez pas commitées.",[285,1455,1456,1459,1460,1463],{},[128,1457,1458],{},"2PL rigoureux."," Garder ",[128,1461,1462],{},"tous"," les verrous, partagés et exclusifs, jusqu'au commit. Un peu moins de concurrence, mais plus simple à raisonner.",[10,1465,1466],{},"Quand une base dit « verrouillage à deux phases », elle désigne presque toujours le 2PL strict ou rigoureux. Le commit devient le seul instant où toutes les écritures deviennent visibles et où tous les verrous tombent, ce qui est exactement la visibilité « tout ou rien » attendue d'une transaction.",[17,1468,1470],{"id":1469},"le-prix-les-interblocages","Le prix : les interblocages",[10,1472,1473,1474,1477],{},"Garder les verrous jusqu'au commit introduit l'",[128,1475,1476],{},"interblocage"," (deadlock). Deux virements touchant deux comptes dans l'ordre inverse suffisent :",[25,1479,1480,1490],{},[28,1481,1482],{},[31,1483,1484,1486,1488],{},[34,1485,955],{},[34,1487,613],{},[34,1489,616],{},[47,1491,1492,1501,1510,1519],{},[31,1493,1494,1496,1499],{},[52,1495,54],{},[52,1497,1498],{},"verrou A (accordé)",[52,1500],{},[31,1502,1503,1505,1507],{},[52,1504,67],{},[52,1506],{},[52,1508,1509],{},"verrou B (accordé)",[31,1511,1512,1514,1517],{},[52,1513,78],{},[52,1515,1516],{},"demande B ... attend",[52,1518],{},[31,1520,1521,1523,1525],{},[52,1522,90],{},[52,1524],{},[52,1526,1527],{},"demande A ... attend",[10,1529,1530],{},"T1 détient A et veut B, T2 détient B et veut A. Aucune ne cède. Il y a deux réponses.",[10,1532,1533,1536,1537,1540,1541,1543,1544,1546,1547,1550,1551,1553],{},[128,1534,1535],{},"Détection."," Le moteur tient un ",[128,1538,1539],{},"graphe d'attente"," (wait-for graph) : une arête ",[156,1542,293],{}," signifie que Ti attend un verrou détenu par Tj. Un ",[128,1545,673],{}," est un interblocage. Le moteur choisit une ",[128,1548,1549],{},"victime"," (typiquement la moins coûteuse à défaire), l'abandonne, relâche ses verrous, et laisse la survivante finir. La victime réessaie plus tard. PostgreSQL fait exactement cela et remonte une erreur ",[156,1552,681],{},", donc votre code applicatif doit être prêt à la réessayer.",[10,1555,1556,1559,1560,1562],{},[128,1557,1558],{},"Prévention."," Les schémas à timestamps empêchent les cycles de se former. Donnez à chaque transaction un timestamp de naissance ",[156,1561,691],{},", où plus ancien signifie plus petit. Quand Ti demande un verrou détenu par Tj :",[176,1564,1567],{"className":1565,"code":1566,"language":181},[179],"wait-die :    si TS(Ti) \u003C TS(Tj)  alors Ti attend      (l'ancien attend)\n              sinon                    Ti abandonne     (le jeune meurt)\n\nwound-wait :  si TS(Ti) \u003C TS(Tj)  alors Tj abandonne    (l'ancien blesse le jeune)\n              sinon                    Ti attend         (le jeune attend)\n",[156,1568,1566],{"__ignoreMap":184},[10,1570,1571,1572,1574,1575,1578],{},"Les deux sont sans interblocage car chaque arête d'attente pointe de façon cohérente de l'ancien vers le jeune (wait-die) ou du jeune vers l'ancien (wound-wait), ce qui ne peut jamais former de cycle. Comme une transaction relancée garde son ",[156,1573,704],{}," d'origine, elle vieillit avec le temps et finit par gagner, donc aucune transaction ne meurt de faim. Un cousin plus grossier est le simple ",[128,1576,1577],{},"délai d'expiration"," (lock timeout) : abandonner après N secondes d'attente. Imprécis, mais il ne demande ni graphe ni timestamps.",[17,1580,1582],{"id":1581},"postgresql-dans-le-monde-réel","PostgreSQL dans le monde réel",[10,1584,1585],{},"PostgreSQL vaut la peine d'être ancré ici car il offre le chemin pessimiste et le chemin optimiste dans un seul moteur.",[10,1587,1588,1591,1592,1594],{},[128,1589,1590],{},"Le chemin pessimiste."," Vous prenez les verrous explicitement. ",[156,1593,725],{}," acquiert un verrou exclusif de ligne, ce qui est précisément l'outil qui corrige notre bug de virement du début :",[176,1596,1598],{"className":729,"code":1597,"language":731,"meta":184,"style":184},"BEGIN;\nSELECT balance FROM accounts WHERE id = 'X' FOR UPDATE;  -- verrou exclusif de ligne\nUPDATE accounts SET balance = balance + 100 WHERE id = 'X';\nCOMMIT;  -- 2PL strict : verrou relâché seulement ici\n",[156,1599,1600,1606,1637,1669],{"__ignoreMap":184},[735,1601,1602,1604],{"class":737,"line":738},[735,1603,742],{"class":741},[735,1605,746],{"class":745},[735,1607,1608,1610,1612,1614,1616,1618,1620,1622,1624,1626,1628,1630,1632,1634],{"class":737,"line":749},[735,1609,752],{"class":741},[735,1611,755],{"class":745},[735,1613,758],{"class":741},[735,1615,761],{"class":745},[735,1617,764],{"class":741},[735,1619,767],{"class":745},[735,1621,771],{"class":770},[735,1623,774],{"class":770},[735,1625,778],{"class":777},[735,1627,781],{"class":770},[735,1629,784],{"class":741},[735,1631,787],{"class":741},[735,1633,790],{"class":745},[735,1635,1636],{"class":793},"-- verrou exclusif de ligne\n",[735,1638,1639,1641,1643,1645,1647,1649,1651,1653,1655,1657,1659,1661,1663,1665,1667],{"class":737,"line":797},[735,1640,800],{"class":741},[735,1642,761],{"class":745},[735,1644,805],{"class":741},[735,1646,755],{"class":745},[735,1648,771],{"class":770},[735,1650,755],{"class":745},[735,1652,814],{"class":770},[735,1654,817],{"class":741},[735,1656,820],{"class":741},[735,1658,767],{"class":745},[735,1660,771],{"class":770},[735,1662,774],{"class":770},[735,1664,778],{"class":777},[735,1666,781],{"class":770},[735,1668,746],{"class":745},[735,1670,1671,1673,1675],{"class":737,"line":835},[735,1672,838],{"class":741},[735,1674,790],{"class":745},[735,1676,1677],{"class":793},"-- 2PL strict : verrou relâché seulement ici\n",[10,1679,1680,1681,1683],{},"Avec ",[156,1682,849],{},", T2 ne peut même pas lire X tant que T1 n'a pas commité, donc la mise à jour perdue est impossible. C'est du 2PL strict manuel, appliqué là où vous savez la contention forte.",[10,1685,1686,1689,1690,1693,1694,1697],{},[128,1687,1688],{},"Le chemin optimiste."," Pour les lectures courantes, PostgreSQL ne prend aucun verrou de lecture. Il utilise le ",[128,1691,1692],{},"contrôle de concurrence multiversion (MVCC)"," : un rédacteur crée une ",[217,1695,1696],{},"nouvelle version"," d'une ligne plutôt que d'écraser l'ancienne, donc les lecteurs voient un instantané cohérent et ne se bloquent jamais sur les rédacteurs, et les rédacteurs ne se bloquent jamais sur les lecteurs. Les écritures prennent quand même des verrous exclusifs de ligne, donc les conflits écriture-écriture restent sérialisés, mais le chemin de lecture courant est sans verrou.",[10,1699,1700,1701,1703,1704,1707],{},"Au niveau d'isolation ",[156,1702,870],{},", PostgreSQL ajoute par-dessus l'",[128,1705,1706],{},"isolation d'instantané sérialisable (SSI)",". Plutôt que tenir de longs verrous de lecture comme le 2PL, SSI laisse les transactions tourner de façon optimiste, guette les motifs lecture-écriture dangereux qui créeraient un cycle dans le graphe de précédence, et abandonne une transaction avec une erreur de sérialisation s'il en détecte un. C'est la même garantie d'acyclicité de notre théorème, imposée de façon optimiste au lieu de pessimiste. Votre code réessaie la transaction abandonnée, exactement comme il réessaie une victime d'interblocage.",[10,1709,1710,1711,1713],{},"Les quatre niveaux d'isolation SQL (read uncommitted, read committed, repeatable read, serializable) sont en réalité une molette réglant quelle part de cette mécanique est engagée. ",[156,1712,870],{}," est le seul qui promet un vrai comportement conflit-sérialisable, et vous pouvez l'atteindre dans PostgreSQL soit de façon pessimiste avec des verrous explicites, soit de façon optimiste avec SSI. Les moteurs à base de verrous comme MySQL InnoDB et SQL Server penchent plus fort du côté 2PL de ce spectre.",[17,1715,885],{"id":884},[10,1717,1718],{},"Le contrôle de concurrence est invisible pour l'utilisateur final et absolument central pour tout système où la donnée est de l'argent. Le verrouillage à deux phases est l'idée qui l'a rendu praticable : il remplace un contrôle impossible à l'exécution, « le graphe de précédence est-il acyclique », par une simple règle locale, « croître puis décroître », et le théorème prouve que les deux sont équivalents. Le 2PL strict le durcit contre les abandons en cascade, la détection et la prévention par timestamps l'empêchent de se figer, et MVCC avec SSI offrent l'alternative optimiste pour le cas courant à dominante lecture.",[10,1720,1721],{},"Grâce à cette mécanique, le virement de 100 qui s'évaporait dans notre premier exemple ne s'évapore jamais vraiment. Une règle que vous ne voyez jamais, imposée pessimistiquement par un verrou ou optimistiquement par une vérification de version, refuse de laisser faire.",[893,1723,895],{},{"title":184,"searchDepth":749,"depth":749,"links":1725},[1726,1727,1728,1729,1730,1731,1732,1733,1734,1735],{"id":942,"depth":749,"text":943},{"id":1041,"depth":749,"text":1042},{"id":1100,"depth":749,"text":1101},{"id":1237,"depth":749,"text":1238},{"id":1313,"depth":749,"text":1314},{"id":1404,"depth":749,"text":1405},{"id":1422,"depth":749,"text":1423},{"id":1469,"depth":749,"text":1470},{"id":1581,"depth":749,"text":1582},{"id":884,"depth":749,"text":885},"Deux virements touchent le même compte à la même milliseconde et un paiement s'évapore. C'est la mise à jour perdue, et c'est pour ça que tout registre de paiement repose sur une couche de contrôle de concurrence. Cet article construit la théorie depuis un exemple fintech jusqu'au verrouillage à deux phases (2PL) : le modèle de transaction, les maths de la conflit-sérialisabilité et le graphe de précédence, pourquoi le théorème du 2PL garantit la correction, le contrôle pessimiste contre optimiste dans PostgreSQL, le 2PL strict, et comment les interblocages sont détectés et évités.",{},"\u002Fdatabases\u002Ftwo-phase-locking.fr",{"title":931,"description":1736},"databases\u002F1.two-phase-locking.fr",[920,921,922,923,924],[926,927],"VOmU4hvXoWDLshCg7hX7A1Wdk7yNRqx-wMWy86b6X50",[1745,1748],{"title":1746,"path":1747},"My Journey to AWS Cloud Practitioner Certification","\u002Fcloud\u002Faws\u002Fhow-i-passed-clf-exam",{"title":1749,"path":1750},"Why I built my own MongoDB image to run a replica set in production","\u002Fdatabases\u002Fmongo\u002Fset-up-mongodb-replica-with-docker",[1752,1764,1776],{"path":1753,"title":1754,"description":1755,"date":1756,"tags":1757,"topics":1761},"\u002Fbackend\u002Fload-balancing\u002Fload-balancing-algorithms","Load Balancing Algorithms: Each One Fixes the Last One's Flaw","Spreading requests across N servers sounds like a one-liner: pick a server, send the request. Then one backend is slower, or bigger, or holds a session, and the naive choice falls apart. This walks the classic algorithms as a chain where each one exists to fix the previous one's blind spot: round robin, weighted, least connections, power of two choices, and consistent hashing, in Go, ending with the failure every tutorial forgets.","2026-07-20",[1758,1759,924,1760],"Load-Balancing","Go","Resilience",[927,1762,1763],"resilience","platform-engineering",{"path":1765,"title":1766,"description":1767,"date":1768,"tags":1769,"topics":1774},"\u002Fsecurity\u002Fauthz\u002Fzanzibar-explained","Zanzibar Demystified: How Google Answers 'Can This User Do This?'","Authorization looks like a one-line if statement until you run it ten million times a second across every product Google ships. Zanzibar is the system that made that question fast, consistent and global. This walks from the naive permission check to relationship-based access control, the tuple model, consistency with zookies, and the open-source heirs like OpenFGA you can actually run today.","2026-07-18",[1770,1771,1772,924,1773],"Authorization","ReBAC","Security","OpenFGA",[1775,927,1763],"authz",{"path":1777,"title":1778,"description":1779,"date":1780,"tags":1781,"topics":1783},"\u002Fbackend\u002Fapi-design\u002Fretry-storms","Retry Storms: How Good Clients Take Down Healthy Servers","A retry looks harmless: the request failed, so try again. Multiply that by every client, add one slow dependency, and retries turn into a self-inflicted DDoS. This walks from the naive retry loop to exponential backoff, jitter, retry budgets and circuit breakers, the caller-side half of resilience that pairs with rate limiting on the server.","2026-07-11",[1760,924,1759,1782],"Retries",[927,1763,1762],1784742134634]