[{"data":1,"prerenderedAt":3265},["ShallowReactive",2],{"content:\u002F10-databases\u002F03-transactions-isolation-anomalies":3},{"title":4,"description":5,"path":6,"body":7},"Транзакции, изоляция и аномалии","Транзакция группирует несколько операций с базой в одну логическую единицу. Для backend-разработчика это не абстрактная теория: почти каждый денежный перевод, смена статуса, списание лимита, запись прогресса и публикация события требует ответа на вопрос: \"Что будет, если в это же время придет второй запрос?\"","\u002F10-databases\u002F03-transactions-isolation-anomalies",{"type":8,"value":9,"toc":3216},"minimark",[10,14,17,20,151,160,163,168,236,239,241,245,248,271,274,337,340,445,460,623,635,637,641,646,649,652,664,666,670,673,719,722,797,812,814,817,820,828,835,841,843,846,849,855,864,866,869,872,878,881,883,887,890,896,902,962,969,972,1032,1035,1037,1041,1044,1047,1095,1098,1127,1130,1136,1139,1142,1168,1171,1242,1244,1248,1251,1254,1260,1268,1270,1274,1277,1283,1286,1302,1305,1319,1322,1325,1327,1338,1341,1344,1347,1352,1354,1365,1368,1382,1384,1388,1392,1395,1399,1405,1503,1506,1509,1568,1575,1700,1703,1707,1729,1735,1739,1742,1748,1751,1768,1771,1823,1826,1828,1832,1836,1839,1876,1879,1883,1886,1959,1965,1968,2013,2016,2151,2153,2157,2163,2212,2215,2244,2247,2289,2300,2303,2305,2309,2315,2318,2493,2496,2513,2516,2520,2523,2526,2616,2619,2708,2714,2717,2723,2730,2733,2760,2763,2765,2769,2773,2776,2782,2785,2788,2794,2797,2799,2803,2807,2810,2820,2833,2837,2852,2856,2862,2869,2875,2879,2882,2884,2888,2945,2947,2951,2979,2981,2985,3008,3010,3014,3054,3173,3175,3179,3212],[11,12,4],"h1",{"id":13},"транзакции-изоляция-и-аномалии",[15,16,5],"p",{},[15,18,19],{},"Простой пример:",[21,22,27],"pre",{"className":23,"code":24,"language":25,"meta":26,"style":26},"language-sql shiki shiki-themes github-dark","BEGIN;\n\nUPDATE accounts\nSET balance_cents = balance_cents - 1000\nWHERE id = 1;\n\nUPDATE accounts\nSET balance_cents = balance_cents + 1000\nWHERE id = 2;\n\nCOMMIT;\n","sql","",[28,29,30,43,50,59,80,96,101,108,124,138,143],"code",{"__ignoreMap":26},[31,32,35,39],"span",{"class":33,"line":34},"line",1,[31,36,38],{"class":37},"snl16","BEGIN",[31,40,42],{"class":41},"s95oV",";\n",[31,44,46],{"class":33,"line":45},2,[31,47,49],{"emptyLinePlaceholder":48},true,"\n",[31,51,53,56],{"class":33,"line":52},3,[31,54,55],{"class":37},"UPDATE",[31,57,58],{"class":41}," accounts\n",[31,60,62,65,68,71,73,76],{"class":33,"line":61},4,[31,63,64],{"class":37},"SET",[31,66,67],{"class":41}," balance_cents ",[31,69,70],{"class":37},"=",[31,72,67],{"class":41},[31,74,75],{"class":37},"-",[31,77,79],{"class":78},"sDLfK"," 1000\n",[31,81,83,86,89,91,94],{"class":33,"line":82},5,[31,84,85],{"class":37},"WHERE",[31,87,88],{"class":41}," id ",[31,90,70],{"class":37},[31,92,93],{"class":78}," 1",[31,95,42],{"class":41},[31,97,99],{"class":33,"line":98},6,[31,100,49],{"emptyLinePlaceholder":48},[31,102,104,106],{"class":33,"line":103},7,[31,105,55],{"class":37},[31,107,58],{"class":41},[31,109,111,113,115,117,119,122],{"class":33,"line":110},8,[31,112,64],{"class":37},[31,114,67],{"class":41},[31,116,70],{"class":37},[31,118,67],{"class":41},[31,120,121],{"class":37},"+",[31,123,79],{"class":78},[31,125,127,129,131,133,136],{"class":33,"line":126},9,[31,128,85],{"class":37},[31,130,88],{"class":41},[31,132,70],{"class":37},[31,134,135],{"class":78}," 2",[31,137,42],{"class":41},[31,139,141],{"class":33,"line":140},10,[31,142,49],{"emptyLinePlaceholder":48},[31,144,146,149],{"class":33,"line":145},11,[31,147,148],{"class":37},"COMMIT",[31,150,42],{"class":41},[15,152,153,154,156,157,159],{},"Если между двумя ",[28,155,55],{}," случится сбой, нельзя оставить деньги списанными с одного счета и не зачисленными на другой. Транзакция делает изменение атомарным: либо оба ",[28,158,55],{}," сохранены, либо оба отменены.",[161,162],"hr",{},[164,165,167],"h2",{"id":166},"acid","ACID",[169,170,171,188],"table",{},[172,173,174],"thead",{},[175,176,177,182,185],"tr",{},[178,179,181],"th",{"align":180},"left","Свойство",[178,183,184],{"align":180},"Смысл",[178,186,187],{"align":180},"Пример",[189,190,191,203,214,225],"tbody",{},[175,192,193,197,200],{},[194,195,196],"td",{"align":180},"Atomicity",[194,198,199],{"align":180},"Все операции транзакции применяются целиком или не применяются",[194,201,202],{"align":180},"Перевод денег не остается наполовину",[175,204,205,208,211],{},[194,206,207],{"align":180},"Consistency",[194,209,210],{"align":180},"Транзакция переводит базу из одного корректного состояния в другое",[194,212,213],{"align":180},"Constraints не нарушены",[175,215,216,219,222],{},[194,217,218],{"align":180},"Isolation",[194,220,221],{"align":180},"Параллельные транзакции не мешают друг другу сверх выбранных гарантий",[194,223,224],{"align":180},"Два запроса не ломают счетчик",[175,226,227,230,233],{},[194,228,229],{"align":180},"Durability",[194,231,232],{"align":180},"После commit данные переживают сбой",[194,234,235],{"align":180},"Запись не исчезает после ответа клиенту",[15,237,238],{},"Consistency в ACID - не то же самое, что \"eventual consistency\" в распределенных системах. Здесь речь о том, что constraints и инварианты базы не должны быть нарушены успешно завершенной транзакцией.",[161,240],{},[164,242,244],{"id":243},"transaction-control","Transaction control",[15,246,247],{},"Базовые команды:",[21,249,251],{"className":23,"code":250,"language":25,"meta":26,"style":26},"BEGIN;\n-- SQL statements\nCOMMIT;\n",[28,252,253,259,265],{"__ignoreMap":26},[31,254,255,257],{"class":33,"line":34},[31,256,38],{"class":37},[31,258,42],{"class":41},[31,260,261],{"class":33,"line":45},[31,262,264],{"class":263},"sAwPA","-- SQL statements\n",[31,266,267,269],{"class":33,"line":52},[31,268,148],{"class":37},[31,270,42],{"class":41},[15,272,273],{},"Откат:",[21,275,277],{"className":23,"code":276,"language":25,"meta":26,"style":26},"BEGIN;\n\nUPDATE orders\nSET status = 'paid'\nWHERE id = $1;\n\nROLLBACK;\n",[28,278,279,285,289,296,310,326,330],{"__ignoreMap":26},[31,280,281,283],{"class":33,"line":34},[31,282,38],{"class":37},[31,284,42],{"class":41},[31,286,287],{"class":33,"line":45},[31,288,49],{"emptyLinePlaceholder":48},[31,290,291,293],{"class":33,"line":52},[31,292,55],{"class":37},[31,294,295],{"class":41}," orders\n",[31,297,298,300,303,306],{"class":33,"line":61},[31,299,64],{"class":37},[31,301,302],{"class":37}," status",[31,304,305],{"class":37}," =",[31,307,309],{"class":308},"sU2Wk"," 'paid'\n",[31,311,312,314,316,318,321,324],{"class":33,"line":82},[31,313,85],{"class":37},[31,315,88],{"class":41},[31,317,70],{"class":37},[31,319,320],{"class":41}," $",[31,322,323],{"class":78},"1",[31,325,42],{"class":41},[31,327,328],{"class":33,"line":98},[31,329,49],{"emptyLinePlaceholder":48},[31,331,332,335],{"class":33,"line":103},[31,333,334],{"class":37},"ROLLBACK",[31,336,42],{"class":41},[15,338,339],{},"Savepoint:",[21,341,343],{"className":23,"code":342,"language":25,"meta":26,"style":26},"BEGIN;\n\nINSERT INTO users (email, name) VALUES ('a@example.com', 'Anna');\n\nSAVEPOINT before_optional_profile;\n\nINSERT INTO user_profiles (user_id, bio) VALUES (42, '...');\n\nROLLBACK TO SAVEPOINT before_optional_profile;\n\nCOMMIT;\n",[28,344,345,351,355,387,391,396,400,421,425,435,439],{"__ignoreMap":26},[31,346,347,349],{"class":33,"line":34},[31,348,38],{"class":37},[31,350,42],{"class":41},[31,352,353],{"class":33,"line":45},[31,354,49],{"emptyLinePlaceholder":48},[31,356,357,360,363,366,369,372,375,378,381,384],{"class":33,"line":52},[31,358,359],{"class":37},"INSERT INTO",[31,361,362],{"class":41}," users (email, ",[31,364,365],{"class":37},"name",[31,367,368],{"class":41},") ",[31,370,371],{"class":37},"VALUES",[31,373,374],{"class":41}," (",[31,376,377],{"class":308},"'a@example.com'",[31,379,380],{"class":41},", ",[31,382,383],{"class":308},"'Anna'",[31,385,386],{"class":41},");\n",[31,388,389],{"class":33,"line":61},[31,390,49],{"emptyLinePlaceholder":48},[31,392,393],{"class":33,"line":82},[31,394,395],{"class":41},"SAVEPOINT before_optional_profile;\n",[31,397,398],{"class":33,"line":98},[31,399,49],{"emptyLinePlaceholder":48},[31,401,402,404,407,409,411,414,416,419],{"class":33,"line":103},[31,403,359],{"class":37},[31,405,406],{"class":41}," user_profiles (user_id, bio) ",[31,408,371],{"class":37},[31,410,374],{"class":41},[31,412,413],{"class":78},"42",[31,415,380],{"class":41},[31,417,418],{"class":308},"'...'",[31,420,386],{"class":41},[31,422,423],{"class":33,"line":110},[31,424,49],{"emptyLinePlaceholder":48},[31,426,427,429,432],{"class":33,"line":126},[31,428,334],{"class":37},[31,430,431],{"class":37}," TO",[31,433,434],{"class":41}," SAVEPOINT before_optional_profile;\n",[31,436,437],{"class":33,"line":140},[31,438,49],{"emptyLinePlaceholder":48},[31,440,441,443],{"class":33,"line":145},[31,442,148],{"class":37},[31,444,42],{"class":41},[15,446,447,448,451,452,455,456,459],{},"В Go не забывайте передавать ",[28,449,450],{},"context.Context",", всегда завершать транзакцию и не смешивать ",[28,453,454],{},"tx.ExecContext"," с ",[28,457,458],{},"db.ExecContext"," внутри одного use case.",[21,461,466],{"className":462,"code":463,"language":464,"meta":465,"style":26},"language-go shiki shiki-themes github-dark","tx, err := db.BeginTx(ctx, nil)\nif err != nil {\n    return err\n}\ndefer tx.Rollback()\n\nif _, err := tx.ExecContext(ctx, debitQuery, fromID, amount); err != nil {\n    return err\n}\nif _, err := tx.ExecContext(ctx, creditQuery, toID, amount); err != nil {\n    return err\n}\n\nreturn tx.Commit()\n","go","no-run",[28,467,468,492,509,517,522,536,540,563,569,573,594,600,605,610],{"__ignoreMap":26},[31,469,470,473,476,479,483,486,489],{"class":33,"line":34},[31,471,472],{"class":41},"tx, err ",[31,474,475],{"class":37},":=",[31,477,478],{"class":41}," db.",[31,480,482],{"class":481},"svObZ","BeginTx",[31,484,485],{"class":41},"(ctx, ",[31,487,488],{"class":78},"nil",[31,490,491],{"class":41},")\n",[31,493,494,497,500,503,506],{"class":33,"line":45},[31,495,496],{"class":37},"if",[31,498,499],{"class":41}," err ",[31,501,502],{"class":37},"!=",[31,504,505],{"class":78}," nil",[31,507,508],{"class":41}," {\n",[31,510,511,514],{"class":33,"line":52},[31,512,513],{"class":37},"    return",[31,515,516],{"class":41}," err\n",[31,518,519],{"class":33,"line":61},[31,520,521],{"class":41},"}\n",[31,523,524,527,530,533],{"class":33,"line":82},[31,525,526],{"class":37},"defer",[31,528,529],{"class":41}," tx.",[31,531,532],{"class":481},"Rollback",[31,534,535],{"class":41},"()\n",[31,537,538],{"class":33,"line":98},[31,539,49],{"emptyLinePlaceholder":48},[31,541,542,544,547,549,551,554,557,559,561],{"class":33,"line":103},[31,543,496],{"class":37},[31,545,546],{"class":41}," _, err ",[31,548,475],{"class":37},[31,550,529],{"class":41},[31,552,553],{"class":481},"ExecContext",[31,555,556],{"class":41},"(ctx, debitQuery, fromID, amount); err ",[31,558,502],{"class":37},[31,560,505],{"class":78},[31,562,508],{"class":41},[31,564,565,567],{"class":33,"line":110},[31,566,513],{"class":37},[31,568,516],{"class":41},[31,570,571],{"class":33,"line":126},[31,572,521],{"class":41},[31,574,575,577,579,581,583,585,588,590,592],{"class":33,"line":140},[31,576,496],{"class":37},[31,578,546],{"class":41},[31,580,475],{"class":37},[31,582,529],{"class":41},[31,584,553],{"class":481},[31,586,587],{"class":41},"(ctx, creditQuery, toID, amount); err ",[31,589,502],{"class":37},[31,591,505],{"class":78},[31,593,508],{"class":41},[31,595,596,598],{"class":33,"line":145},[31,597,513],{"class":37},[31,599,516],{"class":41},[31,601,603],{"class":33,"line":602},12,[31,604,521],{"class":41},[31,606,608],{"class":33,"line":607},13,[31,609,49],{"emptyLinePlaceholder":48},[31,611,613,616,618,621],{"class":33,"line":612},14,[31,614,615],{"class":37},"return",[31,617,529],{"class":41},[31,619,620],{"class":481},"Commit",[31,622,535],{"class":41},[15,624,625,628,629,631,632,634],{},[28,626,627],{},"defer tx.Rollback()"," после успешного ",[28,630,620],{}," обычно вернет ошибку, которую можно игнорировать. Зато при раннем ",[28,633,615],{}," транзакция не останется открытой.",[161,636],{},[164,638,640],{"id":639},"изоляция-и-аномалии","Изоляция и аномалии",[642,643,645],"h3",{"id":644},"что-такое-изоляция","Что такое изоляция",[15,647,648],{},"Isolation отвечает на вопрос: какие изменения других транзакций видит текущая транзакция.",[15,650,651],{},"Если транзакции выполнять строго по одной, а не параллельно, аномалий не будет. Но производительность будет ужасной. Поэтому СУБД разрешают конкурентное выполнение и дают несколько уровней изоляции.",[15,653,654,655,380,657,380,660,663],{},"PostgreSQL использует MVCC: читатели обычно не блокируют писателей, а писатели не блокируют обычных читателей. Запрос видит snapshot данных, подходящий выбранному уровню изоляции. Но MVCC не отменяет блокировки полностью: ",[28,656,55],{},[28,658,659],{},"DELETE",[28,661,662],{},"SELECT FOR UPDATE",", unique checks, DDL и explicit locks все равно конфликтуют.",[161,665],{},[642,667,669],{"id":668},"ansi-phenomena","ANSI phenomena",[15,671,672],{},"Классические уровни изоляции описывают, какие явления запрещены.",[169,674,675,685],{},[172,676,677],{},[175,678,679,682],{},[178,680,681],{"align":180},"Аномалия",[178,683,684],{"align":180},"Что происходит",[189,686,687,695,703,711],{},[175,688,689,692],{},[194,690,691],{"align":180},"Dirty read",[194,693,694],{"align":180},"Транзакция читает незакоммиченные данные другой транзакции",[175,696,697,700],{},[194,698,699],{"align":180},"Non-repeatable read",[194,701,702],{"align":180},"Повторное чтение той же строки дает новое значение",[175,704,705,708],{},[194,706,707],{"align":180},"Phantom read",[194,709,710],{"align":180},"Повторный запрос по условию видит новый набор строк",[175,712,713,716],{},[194,714,715],{"align":180},"Serialization anomaly",[194,717,718],{"align":180},"Итог нельзя объяснить никаким последовательным порядком транзакций",[15,720,721],{},"Таблица для PostgreSQL:",[169,723,724,739],{},[172,725,726],{},[175,727,728,731,733,735,737],{},[178,729,730],{"align":180},"Уровень",[178,732,691],{"align":180},[178,734,699],{"align":180},[178,736,707],{"align":180},[178,738,715],{"align":180},[189,740,741,757,771,784],{},[175,742,743,746,749,752,754],{},[194,744,745],{"align":180},"Read Uncommitted",[194,747,748],{"align":180},"Не происходит в PostgreSQL",[194,750,751],{"align":180},"Возможен",[194,753,751],{"align":180},[194,755,756],{"align":180},"Возможна",[175,758,759,762,765,767,769],{},[194,760,761],{"align":180},"Read Committed",[194,763,764],{"align":180},"Не происходит",[194,766,751],{"align":180},[194,768,751],{"align":180},[194,770,756],{"align":180},[175,772,773,776,778,780,782],{},[194,774,775],{"align":180},"Repeatable Read",[194,777,764],{"align":180},[194,779,764],{"align":180},[194,781,748],{"align":180},[194,783,756],{"align":180},[175,785,786,789,791,793,795],{},[194,787,788],{"align":180},"Serializable",[194,790,764],{"align":180},[194,792,764],{"align":180},[194,794,764],{"align":180},[194,796,764],{"align":180},[15,798,799,800,803,804,807,808,811],{},"Важно для собеседований: SQL standard задает минимальные запреты, а конкретная СУБД может давать более сильные гарантии. В PostgreSQL ",[28,801,802],{},"READ UNCOMMITTED"," ведет себя как ",[28,805,806],{},"READ COMMITTED",", а ",[28,809,810],{},"REPEATABLE READ"," реализован через snapshot isolation и не допускает phantom read в обычном смысле.",[161,813],{},[642,815,691],{"id":816},"dirty-read",[15,818,819],{},"Dirty read: одна транзакция читает данные, которые другая еще не закоммитила.",[21,821,826],{"className":822,"code":824,"language":825,"meta":26},[823],"language-text","T1: BEGIN;\nT1: UPDATE accounts SET balance = 0 WHERE id = 1;\n\nT2: SELECT balance FROM accounts WHERE id = 1; -- видит 0\n\nT1: ROLLBACK;\n","text",[28,827,824],{"__ignoreMap":26},[15,829,830,831,834],{},"Если T2 приняла решение на основе ",[28,832,833],{},"0",", она использовала данные, которых никогда не было в committed-состоянии.",[15,836,837,838,840],{},"PostgreSQL не допускает dirty read даже при ",[28,839,802],{},".",[161,842],{},[642,844,699],{"id":845},"non-repeatable-read",[15,847,848],{},"Одна транзакция дважды читает одну строку и получает разные значения.",[21,850,853],{"className":851,"code":852,"language":825,"meta":26},[823],"T1: BEGIN ISOLATION LEVEL READ COMMITTED;\nT1: SELECT balance FROM accounts WHERE id = 1; -- 1000\n\nT2: UPDATE accounts SET balance = 500 WHERE id = 1;\nT2: COMMIT;\n\nT1: SELECT balance FROM accounts WHERE id = 1; -- 500\nT1: COMMIT;\n",[28,854,852],{"__ignoreMap":26},[15,856,857,858,860,861,863],{},"В ",[28,859,806],{}," каждый statement получает новый snapshot, поэтому это нормально. В ",[28,862,810],{}," T1 продолжила бы видеть старую версию строки.",[161,865],{},[642,867,707],{"id":868},"phantom-read",[15,870,871],{},"Phantom read: повторный запрос по условию видит другой набор строк.",[21,873,876],{"className":874,"code":875,"language":825,"meta":26},[823],"T1: BEGIN ISOLATION LEVEL READ COMMITTED;\nT1: SELECT count(*) FROM orders WHERE status = 'paid'; -- 10\n\nT2: INSERT INTO orders(user_id, status, total_cents)\n    VALUES (1, 'paid', 1000);\nT2: COMMIT;\n\nT1: SELECT count(*) FROM orders WHERE status = 'paid'; -- 11\nT1: COMMIT;\n",[28,877,875],{"__ignoreMap":26},[15,879,880],{},"Это отличается от non-repeatable read тем, что меняется не уже прочитанная строка, а множество строк, подходящих под условие.",[161,882],{},[642,884,886],{"id":885},"lost-update","Lost update",[15,888,889],{},"Lost update: две транзакции читают одно значение, обе считают новое, одна перезаписывает другую.",[21,891,894],{"className":892,"code":893,"language":825,"meta":26},[823],"balance = 100\n\nT1: SELECT balance FROM accounts WHERE id = 1; -- 100\nT2: SELECT balance FROM accounts WHERE id = 1; -- 100\n\nT1: UPDATE accounts SET balance = 70 WHERE id = 1;\nT2: UPDATE accounts SET balance = 50 WHERE id = 1;\n\nИтог: 50, хотя ожидали 20\n",[28,895,893],{"__ignoreMap":26},[15,897,898,899,901],{},"Лучше писать атомарный ",[28,900,55],{},", где вычисление происходит в базе:",[21,903,905],{"className":23,"code":904,"language":25,"meta":26,"style":26},"UPDATE accounts\nSET balance_cents = balance_cents - $1\nWHERE id = $2\n  AND balance_cents >= $1\nRETURNING balance_cents;\n",[28,906,907,913,930,943,957],{"__ignoreMap":26},[31,908,909,911],{"class":33,"line":34},[31,910,55],{"class":37},[31,912,58],{"class":41},[31,914,915,917,919,921,923,925,927],{"class":33,"line":45},[31,916,64],{"class":37},[31,918,67],{"class":41},[31,920,70],{"class":37},[31,922,67],{"class":41},[31,924,75],{"class":37},[31,926,320],{"class":41},[31,928,929],{"class":78},"1\n",[31,931,932,934,936,938,940],{"class":33,"line":52},[31,933,85],{"class":37},[31,935,88],{"class":41},[31,937,70],{"class":37},[31,939,320],{"class":41},[31,941,942],{"class":78},"2\n",[31,944,945,948,950,953,955],{"class":33,"line":61},[31,946,947],{"class":37},"  AND",[31,949,67],{"class":41},[31,951,952],{"class":37},">=",[31,954,320],{"class":41},[31,956,929],{"class":78},[31,958,959],{"class":33,"line":82},[31,960,961],{"class":41},"RETURNING balance_cents;\n",[15,963,964,965,968],{},"Если ",[28,966,967],{},"RETURNING"," не вернул строку, денег недостаточно или аккаунта нет. Такой запрос не требует сначала читать balance в Go, считать, а потом писать.",[15,970,971],{},"Другой вариант - optimistic locking:",[21,973,975],{"className":23,"code":974,"language":25,"meta":26,"style":26},"UPDATE accounts\nSET balance_cents = $new_balance,\n    version = version + 1\nWHERE id = $id\n  AND version = $old_version;\n",[28,976,977,983,994,1010,1021],{"__ignoreMap":26},[31,978,979,981],{"class":33,"line":34},[31,980,55],{"class":37},[31,982,58],{"class":41},[31,984,985,987,989,991],{"class":33,"line":45},[31,986,64],{"class":37},[31,988,67],{"class":41},[31,990,70],{"class":37},[31,992,993],{"class":41}," $new_balance,\n",[31,995,996,999,1001,1004,1007],{"class":33,"line":52},[31,997,998],{"class":37},"    version",[31,1000,305],{"class":37},[31,1002,1003],{"class":37}," version",[31,1005,1006],{"class":37}," +",[31,1008,1009],{"class":78}," 1\n",[31,1011,1012,1014,1016,1018],{"class":33,"line":61},[31,1013,85],{"class":37},[31,1015,88],{"class":41},[31,1017,70],{"class":37},[31,1019,1020],{"class":41}," $id\n",[31,1022,1023,1025,1027,1029],{"class":33,"line":82},[31,1024,947],{"class":37},[31,1026,1003],{"class":37},[31,1028,305],{"class":37},[31,1030,1031],{"class":41}," $old_version;\n",[15,1033,1034],{},"Если обновлено 0 строк, кто-то изменил запись раньше. Use case перечитывает данные и повторяет операцию или возвращает conflict.",[161,1036],{},[642,1038,1040],{"id":1039},"write-skew","Write skew",[15,1042,1043],{},"Write skew особенно любят на собеседованиях, потому что он показывает разницу между \"строки не конфликтуют\" и \"бизнес-инвариант нарушен\".",[15,1045,1046],{},"Инвариант: в дежурстве должна быть хотя бы одна активная смена врача.",[21,1048,1050],{"className":23,"code":1049,"language":25,"meta":26,"style":26},"CREATE TABLE shifts (\n    doctor_id bigint PRIMARY KEY,\n    on_call boolean NOT NULL\n);\n",[28,1051,1052,1066,1080,1091],{"__ignoreMap":26},[31,1053,1054,1057,1060,1063],{"class":33,"line":34},[31,1055,1056],{"class":37},"CREATE",[31,1058,1059],{"class":37}," TABLE",[31,1061,1062],{"class":481}," shifts",[31,1064,1065],{"class":41}," (\n",[31,1067,1068,1071,1074,1077],{"class":33,"line":45},[31,1069,1070],{"class":41},"    doctor_id ",[31,1072,1073],{"class":37},"bigint",[31,1075,1076],{"class":37}," PRIMARY KEY",[31,1078,1079],{"class":41},",\n",[31,1081,1082,1085,1088],{"class":33,"line":52},[31,1083,1084],{"class":41},"    on_call ",[31,1086,1087],{"class":37},"boolean",[31,1089,1090],{"class":37}," NOT NULL\n",[31,1092,1093],{"class":33,"line":61},[31,1094,386],{"class":41},[15,1096,1097],{},"Начальное состояние:",[169,1099,1100,1111],{},[172,1101,1102],{},[175,1103,1104,1108],{},[178,1105,1107],{"align":1106},"right","doctor_id",[178,1109,1110],{"align":180},"on_call",[189,1112,1113,1120],{},[175,1114,1115,1117],{},[194,1116,323],{"align":1106},[194,1118,1119],{"align":180},"true",[175,1121,1122,1125],{},[194,1123,1124],{"align":1106},"2",[194,1126,1119],{"align":180},[15,1128,1129],{},"Две транзакции:",[21,1131,1134],{"className":1132,"code":1133,"language":825,"meta":26},[823],"T1: видит, что on_call врачей = 2\nT2: видит, что on_call врачей = 2\n\nT1: UPDATE shifts SET on_call = false WHERE doctor_id = 1;\nT2: UPDATE shifts SET on_call = false WHERE doctor_id = 2;\n\nОбе коммитятся.\nИтог: on_call врачей = 0\n",[28,1135,1133],{"__ignoreMap":26},[15,1137,1138],{},"Они обновляли разные строки, поэтому row lock не конфликтовал. Но инвариант был про набор строк.",[15,1140,1141],{},"Варианты защиты:",[1143,1144,1145,1156,1162,1165],"ul",{},[1146,1147,1148,1151,1152,1155],"li",{},[28,1149,1150],{},"SERIALIZABLE"," и retry при ",[28,1153,1154],{},"SQLSTATE 40001",";",[1146,1157,1158,1159,1155],{},"explicit lock на общий ресурс, например строку ",[28,1160,1161],{},"departments",[1146,1163,1164],{},"моделировать инвариант так, чтобы конфликт был на одной строке;",[1146,1166,1167],{},"использовать constraint, если правило выражается декларативно.",[15,1169,1170],{},"Пример с общей строкой:",[21,1172,1174],{"className":23,"code":1173,"language":25,"meta":26,"style":26},"BEGIN;\n\nSELECT *\nFROM departments\nWHERE id = $department_id\nFOR UPDATE;\n\n-- Теперь проверка и изменение смен происходят под lock общего ресурса.\n\nCOMMIT;\n",[28,1175,1176,1182,1186,1194,1202,1213,1223,1227,1232,1236],{"__ignoreMap":26},[31,1177,1178,1180],{"class":33,"line":34},[31,1179,38],{"class":37},[31,1181,42],{"class":41},[31,1183,1184],{"class":33,"line":45},[31,1185,49],{"emptyLinePlaceholder":48},[31,1187,1188,1191],{"class":33,"line":52},[31,1189,1190],{"class":37},"SELECT",[31,1192,1193],{"class":37}," *\n",[31,1195,1196,1199],{"class":33,"line":61},[31,1197,1198],{"class":37},"FROM",[31,1200,1201],{"class":41}," departments\n",[31,1203,1204,1206,1208,1210],{"class":33,"line":82},[31,1205,85],{"class":37},[31,1207,88],{"class":41},[31,1209,70],{"class":37},[31,1211,1212],{"class":41}," $department_id\n",[31,1214,1215,1218,1221],{"class":33,"line":98},[31,1216,1217],{"class":37},"FOR",[31,1219,1220],{"class":37}," UPDATE",[31,1222,42],{"class":41},[31,1224,1225],{"class":33,"line":103},[31,1226,49],{"emptyLinePlaceholder":48},[31,1228,1229],{"class":33,"line":110},[31,1230,1231],{"class":263},"-- Теперь проверка и изменение смен происходят под lock общего ресурса.\n",[31,1233,1234],{"class":33,"line":126},[31,1235,49],{"emptyLinePlaceholder":48},[31,1237,1238,1240],{"class":33,"line":140},[31,1239,148],{"class":37},[31,1241,42],{"class":41},[161,1243],{},[642,1245,1247],{"id":1246},"read-skew","Read skew",[15,1249,1250],{},"Read skew: транзакция читает несколько связанных значений, но видит их из разных моментов времени.",[15,1252,1253],{},"Пример: деньги переводят между счетами, а отчет читает счета двумя запросами.",[21,1255,1258],{"className":1256,"code":1257,"language":825,"meta":26},[823],"accounts:\n  A = 100\n  B = 100\n\nT1: SELECT balance FROM accounts WHERE id = 'A'; -- 100\n\nT2: BEGIN;\nT2: UPDATE accounts SET balance = 50 WHERE id = 'A';\nT2: UPDATE accounts SET balance = 150 WHERE id = 'B';\nT2: COMMIT;\n\nT1: SELECT balance FROM accounts WHERE id = 'B'; -- 150\n\nT1 видит сумму 250, хотя committed-состояния с такой суммой не было.\n",[28,1259,1257],{"__ignoreMap":26},[15,1261,857,1262,1264,1265,1267],{},[28,1263,806],{}," это возможно, потому что каждый statement видит новый snapshot. Для консистентного отчета используйте ",[28,1266,810],{}," или один statement, который читает все нужное сразу.",[161,1269],{},[164,1271,1273],{"id":1272},"уровни-изоляции-на-практике","Уровни изоляции на практике",[642,1275,761],{"id":1276},"read-committed",[15,1278,1279,1280,1282],{},"Это default в PostgreSQL. Каждый statement видит данные, committed до начала statement. Два ",[28,1281,1190],{}," внутри одной транзакции могут увидеть разные snapshots.",[15,1284,1285],{},"Подходит:",[1143,1287,1288,1291,1294,1297],{},[1146,1289,1290],{},"простые CRUD-операции;",[1146,1292,1293],{},"update конкретной строки;",[1146,1295,1296],{},"операции, где каждый statement самодостаточен;",[1146,1298,1299,1300,840],{},"большинство web-request'ов при правильных constraints и атомарных ",[28,1301,55],{},[15,1303,1304],{},"Опасно:",[1143,1306,1307,1310,1313,1316],{},[1146,1308,1309],{},"сложные проверки по набору строк;",[1146,1311,1312],{},"отчеты из нескольких запросов;",[1146,1314,1315],{},"\"сначала SELECT, потом UPDATE\" без lock или version;",[1146,1317,1318],{},"бизнес-инварианты вроде \"не больше N активных записей\".",[642,1320,775],{"id":1321},"repeatable-read",[15,1323,1324],{},"Транзакция видит snapshot на момент начала первой команды. Повторные чтения стабильны. В PostgreSQL этот уровень не допускает phantom read, но возможны serialization anomalies вроде write skew.",[15,1326,1285],{},[1143,1328,1329,1332,1335],{},[1146,1330,1331],{},"консистентные отчеты;",[1146,1333,1334],{},"операции, где нужно стабильное чтение набора данных;",[1146,1336,1337],{},"длинные read-only задачи, если они не держат лишние ресурсы слишком долго.",[15,1339,1340],{},"Нужно помнить: update строки, измененной после snapshot, может завершиться ошибкой сериализации. Приложение должно уметь retry.",[642,1342,788],{"id":1343},"serializable",[15,1345,1346],{},"Самая сильная изоляция: результат успешно закоммиченных транзакций должен быть эквивалентен некоторому последовательному выполнению.",[15,1348,1349,1350,840],{},"В PostgreSQL это Serializable Snapshot Isolation. СУБД отслеживает опасные зависимости и может откатить одну из транзакций с ",[28,1351,1154],{},[15,1353,1285],{},[1143,1355,1356,1359,1362],{},[1146,1357,1358],{},"сложные инварианты, которые трудно закрыть простыми constraints;",[1146,1360,1361],{},"финансовые или лимитные операции, где проще написать логику как для последовательного выполнения;",[1146,1363,1364],{},"небольшие критичные транзакции с нормальным retry-механизмом.",[15,1366,1367],{},"Цена:",[1143,1369,1370,1373,1376,1379],{},[1146,1371,1372],{},"возможны serialization failures;",[1146,1374,1375],{},"нужен общий retry-wrapper;",[1146,1377,1378],{},"длинные транзакции повышают риск конфликтов;",[1146,1380,1381],{},"плохие планы и последовательные сканы могут увеличить число predicate locks.",[161,1383],{},[164,1385,1387],{"id":1386},"конкурентность-и-блокировки","Конкурентность и блокировки",[642,1389,1391],{"id":1390},"блокировки","Блокировки",[15,1393,1394],{},"Блокировки отвечают за координацию конкурентных операций.",[642,1396,1398],{"id":1397},"row-level-locks","Row-level locks",[15,1400,1401,1404],{},[28,1402,1403],{},"SELECT ... FOR UPDATE"," блокирует выбранные строки для последующего изменения.",[21,1406,1408],{"className":23,"code":1407,"language":25,"meta":26,"style":26},"BEGIN;\n\nSELECT id, balance_cents\nFROM accounts\nWHERE id = $1\nFOR UPDATE;\n\nUPDATE accounts\nSET balance_cents = balance_cents - $2\nWHERE id = $1;\n\nCOMMIT;\n",[28,1409,1410,1416,1420,1427,1433,1445,1453,1457,1463,1479,1493,1497],{"__ignoreMap":26},[31,1411,1412,1414],{"class":33,"line":34},[31,1413,38],{"class":37},[31,1415,42],{"class":41},[31,1417,1418],{"class":33,"line":45},[31,1419,49],{"emptyLinePlaceholder":48},[31,1421,1422,1424],{"class":33,"line":52},[31,1423,1190],{"class":37},[31,1425,1426],{"class":41}," id, balance_cents\n",[31,1428,1429,1431],{"class":33,"line":61},[31,1430,1198],{"class":37},[31,1432,58],{"class":41},[31,1434,1435,1437,1439,1441,1443],{"class":33,"line":82},[31,1436,85],{"class":37},[31,1438,88],{"class":41},[31,1440,70],{"class":37},[31,1442,320],{"class":41},[31,1444,929],{"class":78},[31,1446,1447,1449,1451],{"class":33,"line":98},[31,1448,1217],{"class":37},[31,1450,1220],{"class":37},[31,1452,42],{"class":41},[31,1454,1455],{"class":33,"line":103},[31,1456,49],{"emptyLinePlaceholder":48},[31,1458,1459,1461],{"class":33,"line":110},[31,1460,55],{"class":37},[31,1462,58],{"class":41},[31,1464,1465,1467,1469,1471,1473,1475,1477],{"class":33,"line":126},[31,1466,64],{"class":37},[31,1468,67],{"class":41},[31,1470,70],{"class":37},[31,1472,67],{"class":41},[31,1474,75],{"class":37},[31,1476,320],{"class":41},[31,1478,942],{"class":78},[31,1480,1481,1483,1485,1487,1489,1491],{"class":33,"line":140},[31,1482,85],{"class":37},[31,1484,88],{"class":41},[31,1486,70],{"class":37},[31,1488,320],{"class":41},[31,1490,323],{"class":78},[31,1492,42],{"class":41},[31,1494,1495],{"class":33,"line":145},[31,1496,49],{"emptyLinePlaceholder":48},[31,1498,1499,1501],{"class":33,"line":602},[31,1500,148],{"class":37},[31,1502,42],{"class":41},[15,1504,1505],{},"Пока транзакция не завершится, другая транзакция, которая хочет обновить эту же строку или взять несовместимый lock, будет ждать.",[15,1507,1508],{},"Варианты:",[21,1510,1512],{"className":23,"code":1511,"language":25,"meta":26,"style":26},"SELECT ... FOR UPDATE;\nSELECT ... FOR NO KEY UPDATE;\nSELECT ... FOR SHARE;\nSELECT ... FOR KEY SHARE;\n",[28,1513,1514,1527,1545,1556],{"__ignoreMap":26},[31,1515,1516,1518,1521,1523,1525],{"class":33,"line":34},[31,1517,1190],{"class":37},[31,1519,1520],{"class":41}," ... ",[31,1522,1217],{"class":37},[31,1524,1220],{"class":37},[31,1526,42],{"class":41},[31,1528,1529,1531,1533,1535,1538,1541,1543],{"class":33,"line":45},[31,1530,1190],{"class":37},[31,1532,1520],{"class":41},[31,1534,1217],{"class":37},[31,1536,1537],{"class":37}," NO",[31,1539,1540],{"class":37}," KEY",[31,1542,1220],{"class":37},[31,1544,42],{"class":41},[31,1546,1547,1549,1551,1553],{"class":33,"line":52},[31,1548,1190],{"class":37},[31,1550,1520],{"class":41},[31,1552,1217],{"class":37},[31,1554,1555],{"class":41}," SHARE;\n",[31,1557,1558,1560,1562,1564,1566],{"class":33,"line":61},[31,1559,1190],{"class":37},[31,1561,1520],{"class":41},[31,1563,1217],{"class":37},[31,1565,1540],{"class":37},[31,1567,1555],{"class":41},[15,1569,1570,1571,1574],{},"Для очередей иногда используют ",[28,1572,1573],{},"SKIP LOCKED",":",[21,1576,1578],{"className":23,"code":1577,"language":25,"meta":26,"style":26},"WITH job AS (\n    SELECT id\n    FROM jobs\n    WHERE status = 'pending'\n    ORDER BY created_at\n    FOR UPDATE SKIP LOCKED\n    LIMIT 1\n)\nUPDATE jobs\nSET status = 'running'\nWHERE id IN (SELECT id FROM job)\nRETURNING *;\n",[28,1579,1580,1593,1601,1609,1621,1629,1642,1649,1653,1659,1670,1690],{"__ignoreMap":26},[31,1581,1582,1585,1588,1591],{"class":33,"line":34},[31,1583,1584],{"class":37},"WITH",[31,1586,1587],{"class":41}," job ",[31,1589,1590],{"class":37},"AS",[31,1592,1065],{"class":41},[31,1594,1595,1598],{"class":33,"line":45},[31,1596,1597],{"class":37},"    SELECT",[31,1599,1600],{"class":41}," id\n",[31,1602,1603,1606],{"class":33,"line":52},[31,1604,1605],{"class":37},"    FROM",[31,1607,1608],{"class":41}," jobs\n",[31,1610,1611,1614,1616,1618],{"class":33,"line":61},[31,1612,1613],{"class":37},"    WHERE",[31,1615,302],{"class":37},[31,1617,305],{"class":37},[31,1619,1620],{"class":308}," 'pending'\n",[31,1622,1623,1626],{"class":33,"line":82},[31,1624,1625],{"class":37},"    ORDER BY",[31,1627,1628],{"class":41}," created_at\n",[31,1630,1631,1634,1636,1639],{"class":33,"line":98},[31,1632,1633],{"class":37},"    FOR",[31,1635,1220],{"class":37},[31,1637,1638],{"class":37}," SKIP",[31,1640,1641],{"class":41}," LOCKED\n",[31,1643,1644,1647],{"class":33,"line":103},[31,1645,1646],{"class":37},"    LIMIT",[31,1648,1009],{"class":78},[31,1650,1651],{"class":33,"line":110},[31,1652,491],{"class":41},[31,1654,1655,1657],{"class":33,"line":126},[31,1656,55],{"class":37},[31,1658,1608],{"class":41},[31,1660,1661,1663,1665,1667],{"class":33,"line":140},[31,1662,64],{"class":37},[31,1664,302],{"class":37},[31,1666,305],{"class":37},[31,1668,1669],{"class":308}," 'running'\n",[31,1671,1672,1674,1676,1679,1681,1683,1685,1687],{"class":33,"line":145},[31,1673,85],{"class":37},[31,1675,88],{"class":41},[31,1677,1678],{"class":37},"IN",[31,1680,374],{"class":41},[31,1682,1190],{"class":37},[31,1684,88],{"class":41},[31,1686,1198],{"class":37},[31,1688,1689],{"class":41}," job)\n",[31,1691,1692,1695,1698],{"class":33,"line":602},[31,1693,1694],{"class":41},"RETURNING ",[31,1696,1697],{"class":37},"*",[31,1699,42],{"class":41},[15,1701,1702],{},"Несколько workers могут параллельно брать разные jobs, не ожидая уже заблокированные строки.",[642,1704,1706],{"id":1705},"table-level-locks","Table-level locks",[15,1708,1709,1710,1713,1714,380,1717,1720,1721,1724,1725,1728],{},"DDL, ",[28,1711,1712],{},"TRUNCATE",", некоторые ",[28,1715,1716],{},"ALTER TABLE",[28,1718,1719],{},"CREATE INDEX"," и explicit ",[28,1722,1723],{},"LOCK TABLE"," могут брать table-level locks. Самый сильный ",[28,1726,1727],{},"ACCESS EXCLUSIVE"," конфликтует практически со всем.",[15,1730,1731,1732,1734],{},"Для backend-разработчика главный вывод: миграции - это production-код. Нельзя бездумно запускать тяжелый ",[28,1733,1716],{}," на большой таблице днем и удивляться остановке API.",[642,1736,1738],{"id":1737},"deadlock","Deadlock",[15,1740,1741],{},"Deadlock возникает, когда транзакции ждут друг друга по кругу.",[21,1743,1746],{"className":1744,"code":1745,"language":825,"meta":26},[823],"T1: locked account 1\nT2: locked account 2\nT1: wants account 2\nT2: wants account 1\n",[28,1747,1745],{"__ignoreMap":26},[15,1749,1750],{},"Защита:",[1143,1752,1753,1756,1759,1762,1765],{},[1146,1754,1755],{},"брать locks в одинаковом порядке;",[1146,1757,1758],{},"держать транзакции короткими;",[1146,1760,1761],{},"не делать сетевые вызовы внутри транзакции;",[1146,1763,1764],{},"иметь retry для deadlock errors;",[1146,1766,1767],{},"обновлять пачки строк в стабильном порядке.",[15,1769,1770],{},"Для перевода между счетами:",[21,1772,1774],{"className":23,"code":1773,"language":25,"meta":26,"style":26},"SELECT id\nFROM accounts\nWHERE id IN ($1, $2)\nORDER BY id\nFOR UPDATE;\n",[28,1775,1776,1782,1788,1808,1815],{"__ignoreMap":26},[31,1777,1778,1780],{"class":33,"line":34},[31,1779,1190],{"class":37},[31,1781,1600],{"class":41},[31,1783,1784,1786],{"class":33,"line":45},[31,1785,1198],{"class":37},[31,1787,58],{"class":41},[31,1789,1790,1792,1794,1796,1799,1801,1804,1806],{"class":33,"line":52},[31,1791,85],{"class":37},[31,1793,88],{"class":41},[31,1795,1678],{"class":37},[31,1797,1798],{"class":41}," ($",[31,1800,323],{"class":78},[31,1802,1803],{"class":41},", $",[31,1805,1124],{"class":78},[31,1807,491],{"class":41},[31,1809,1810,1813],{"class":33,"line":61},[31,1811,1812],{"class":37},"ORDER BY",[31,1814,1600],{"class":41},[31,1816,1817,1819,1821],{"class":33,"line":82},[31,1818,1217],{"class":37},[31,1820,1220],{"class":37},[31,1822,42],{"class":41},[15,1824,1825],{},"Сначала блокируем обе строки в одном порядке, затем меняем balances.",[161,1827],{},[642,1829,1831],{"id":1830},"optimistic-и-pessimistic-concurrency","Optimistic и pessimistic concurrency",[642,1833,1835],{"id":1834},"pessimistic","Pessimistic",[15,1837,1838],{},"Пессимистический подход заранее блокирует данные:",[21,1840,1842],{"className":23,"code":1841,"language":25,"meta":26,"style":26},"SELECT *\nFROM orders\nWHERE id = $1\nFOR UPDATE;\n",[28,1843,1844,1850,1856,1868],{"__ignoreMap":26},[31,1845,1846,1848],{"class":33,"line":34},[31,1847,1190],{"class":37},[31,1849,1193],{"class":37},[31,1851,1852,1854],{"class":33,"line":45},[31,1853,1198],{"class":37},[31,1855,295],{"class":41},[31,1857,1858,1860,1862,1864,1866],{"class":33,"line":52},[31,1859,85],{"class":37},[31,1861,88],{"class":41},[31,1863,70],{"class":37},[31,1865,320],{"class":41},[31,1867,929],{"class":78},[31,1869,1870,1872,1874],{"class":33,"line":61},[31,1871,1217],{"class":37},[31,1873,1220],{"class":37},[31,1875,42],{"class":41},[15,1877,1878],{},"Подходит, если конфликтов много или цена конфликта высокая. Минус - ожидание, deadlocks, снижение параллелизма.",[642,1880,1882],{"id":1881},"optimistic","Optimistic",[15,1884,1885],{},"Оптимистический подход не блокирует заранее, а проверяет, что данные не изменились.",[21,1887,1889],{"className":23,"code":1888,"language":25,"meta":26,"style":26},"UPDATE orders\nSET status = 'paid',\n    version = version + 1\nWHERE id = $1\n  AND version = $2\n  AND status = 'draft';\n",[28,1890,1891,1897,1910,1922,1934,1946],{"__ignoreMap":26},[31,1892,1893,1895],{"class":33,"line":34},[31,1894,55],{"class":37},[31,1896,295],{"class":41},[31,1898,1899,1901,1903,1905,1908],{"class":33,"line":45},[31,1900,64],{"class":37},[31,1902,302],{"class":37},[31,1904,305],{"class":37},[31,1906,1907],{"class":308}," 'paid'",[31,1909,1079],{"class":41},[31,1911,1912,1914,1916,1918,1920],{"class":33,"line":52},[31,1913,998],{"class":37},[31,1915,305],{"class":37},[31,1917,1003],{"class":37},[31,1919,1006],{"class":37},[31,1921,1009],{"class":78},[31,1923,1924,1926,1928,1930,1932],{"class":33,"line":61},[31,1925,85],{"class":37},[31,1927,88],{"class":41},[31,1929,70],{"class":37},[31,1931,320],{"class":41},[31,1933,929],{"class":78},[31,1935,1936,1938,1940,1942,1944],{"class":33,"line":82},[31,1937,947],{"class":37},[31,1939,1003],{"class":37},[31,1941,305],{"class":37},[31,1943,320],{"class":41},[31,1945,942],{"class":78},[31,1947,1948,1950,1952,1954,1957],{"class":33,"line":98},[31,1949,947],{"class":37},[31,1951,302],{"class":37},[31,1953,305],{"class":37},[31,1955,1956],{"class":308}," 'draft'",[31,1958,42],{"class":41},[15,1960,1961,1962,840],{},"Подходит, если конфликтов мало. Минус - нужно обрабатывать \"0 rows affected\" и повторять операцию или возвращать клиенту ",[28,1963,1964],{},"409 Conflict",[15,1966,1967],{},"Типичная Go-модель:",[21,1969,1971],{"className":462,"code":1970,"language":464,"meta":465,"style":26},"type Order struct {\n    ID      int64\n    Status  string\n    Version int64\n}\n",[28,1972,1973,1986,1994,2002,2009],{"__ignoreMap":26},[31,1974,1975,1978,1981,1984],{"class":33,"line":34},[31,1976,1977],{"class":37},"type",[31,1979,1980],{"class":481}," Order",[31,1982,1983],{"class":37}," struct",[31,1985,508],{"class":41},[31,1987,1988,1991],{"class":33,"line":45},[31,1989,1990],{"class":41},"    ID      ",[31,1992,1993],{"class":37},"int64\n",[31,1995,1996,1999],{"class":33,"line":52},[31,1997,1998],{"class":41},"    Status  ",[31,2000,2001],{"class":37},"string\n",[31,2003,2004,2007],{"class":33,"line":61},[31,2005,2006],{"class":41},"    Version ",[31,2008,1993],{"class":37},[31,2010,2011],{"class":33,"line":82},[31,2012,521],{"class":41},[15,2014,2015],{},"И repository:",[21,2017,2019],{"className":462,"code":2018,"language":464,"meta":465,"style":26},"res, err := tx.ExecContext(ctx, `\n    UPDATE orders\n    SET status = $1, version = version + 1\n    WHERE id = $2 AND version = $3\n`, nextStatus, order.ID, order.Version)\nif err != nil {\n    return err\n}\n\naffected, err := res.RowsAffected()\nif err != nil {\n    return err\n}\nif affected == 0 {\n    return ErrConcurrentModification\n}\n",[28,2020,2021,2037,2042,2047,2052,2060,2072,2078,2082,2086,2101,2113,2119,2123,2138,2146],{"__ignoreMap":26},[31,2022,2023,2026,2028,2030,2032,2034],{"class":33,"line":34},[31,2024,2025],{"class":41},"res, err ",[31,2027,475],{"class":37},[31,2029,529],{"class":41},[31,2031,553],{"class":481},[31,2033,485],{"class":41},[31,2035,2036],{"class":308},"`\n",[31,2038,2039],{"class":33,"line":45},[31,2040,2041],{"class":308},"    UPDATE orders\n",[31,2043,2044],{"class":33,"line":52},[31,2045,2046],{"class":308},"    SET status = $1, version = version + 1\n",[31,2048,2049],{"class":33,"line":61},[31,2050,2051],{"class":308},"    WHERE id = $2 AND version = $3\n",[31,2053,2054,2057],{"class":33,"line":82},[31,2055,2056],{"class":308},"`",[31,2058,2059],{"class":41},", nextStatus, order.ID, order.Version)\n",[31,2061,2062,2064,2066,2068,2070],{"class":33,"line":98},[31,2063,496],{"class":37},[31,2065,499],{"class":41},[31,2067,502],{"class":37},[31,2069,505],{"class":78},[31,2071,508],{"class":41},[31,2073,2074,2076],{"class":33,"line":103},[31,2075,513],{"class":37},[31,2077,516],{"class":41},[31,2079,2080],{"class":33,"line":110},[31,2081,521],{"class":41},[31,2083,2084],{"class":33,"line":126},[31,2085,49],{"emptyLinePlaceholder":48},[31,2087,2088,2091,2093,2096,2099],{"class":33,"line":140},[31,2089,2090],{"class":41},"affected, err ",[31,2092,475],{"class":37},[31,2094,2095],{"class":41}," res.",[31,2097,2098],{"class":481},"RowsAffected",[31,2100,535],{"class":41},[31,2102,2103,2105,2107,2109,2111],{"class":33,"line":145},[31,2104,496],{"class":37},[31,2106,499],{"class":41},[31,2108,502],{"class":37},[31,2110,505],{"class":78},[31,2112,508],{"class":41},[31,2114,2115,2117],{"class":33,"line":602},[31,2116,513],{"class":37},[31,2118,516],{"class":41},[31,2120,2121],{"class":33,"line":607},[31,2122,521],{"class":41},[31,2124,2125,2127,2130,2133,2136],{"class":33,"line":612},[31,2126,496],{"class":37},[31,2128,2129],{"class":41}," affected ",[31,2131,2132],{"class":37},"==",[31,2134,2135],{"class":78}," 0",[31,2137,508],{"class":41},[31,2139,2141,2143],{"class":33,"line":2140},15,[31,2142,513],{"class":37},[31,2144,2145],{"class":41}," ErrConcurrentModification\n",[31,2147,2149],{"class":33,"line":2148},16,[31,2150,521],{"class":41},[161,2152],{},[642,2154,2156],{"id":2155},"constraints-как-concurrency-primitive","Constraints как concurrency primitive",[15,2158,2159,2160,2162],{},"Частая ошибка: проверять уникальность через ",[28,2161,1190],{},", потом вставлять.",[21,2164,2166],{"className":23,"code":2165,"language":25,"meta":26,"style":26},"-- Ненадежно при конкуренции.\nSELECT id FROM users WHERE email = $1;\nINSERT INTO users(email) VALUES ($1);\n",[28,2167,2168,2173,2197],{"__ignoreMap":26},[31,2169,2170],{"class":33,"line":34},[31,2171,2172],{"class":263},"-- Ненадежно при конкуренции.\n",[31,2174,2175,2177,2179,2181,2184,2186,2189,2191,2193,2195],{"class":33,"line":45},[31,2176,1190],{"class":37},[31,2178,88],{"class":41},[31,2180,1198],{"class":37},[31,2182,2183],{"class":41}," users ",[31,2185,85],{"class":37},[31,2187,2188],{"class":41}," email ",[31,2190,70],{"class":37},[31,2192,320],{"class":41},[31,2194,323],{"class":78},[31,2196,42],{"class":41},[31,2198,2199,2201,2204,2206,2208,2210],{"class":33,"line":52},[31,2200,359],{"class":37},[31,2202,2203],{"class":41}," users(email) ",[31,2205,371],{"class":37},[31,2207,1798],{"class":41},[31,2209,323],{"class":78},[31,2211,386],{"class":41},[15,2213,2214],{},"Два запроса могут одновременно увидеть, что email свободен. Правильная защита - unique constraint:",[21,2216,2218],{"className":23,"code":2217,"language":25,"meta":26,"style":26},"CREATE UNIQUE INDEX users_email_unique\nON users (lower(email));\n",[28,2219,2220,2230],{"__ignoreMap":26},[31,2221,2222,2224,2227],{"class":33,"line":34},[31,2223,1056],{"class":37},[31,2225,2226],{"class":37}," UNIQUE INDEX",[31,2228,2229],{"class":481}," users_email_unique\n",[31,2231,2232,2235,2238,2241],{"class":33,"line":45},[31,2233,2234],{"class":37},"ON",[31,2236,2237],{"class":41}," users (",[31,2239,2240],{"class":78},"lower",[31,2242,2243],{"class":41},"(email));\n",[15,2245,2246],{},"И затем:",[21,2248,2250],{"className":23,"code":2249,"language":25,"meta":26,"style":26},"INSERT INTO users(email, name)\nVALUES ($1, $2)\nON CONFLICT DO NOTHING\nRETURNING id;\n",[28,2251,2252,2263,2277,2284],{"__ignoreMap":26},[31,2253,2254,2256,2259,2261],{"class":33,"line":34},[31,2255,359],{"class":37},[31,2257,2258],{"class":41}," users(email, ",[31,2260,365],{"class":37},[31,2262,491],{"class":41},[31,2264,2265,2267,2269,2271,2273,2275],{"class":33,"line":45},[31,2266,371],{"class":37},[31,2268,1798],{"class":41},[31,2270,323],{"class":78},[31,2272,1803],{"class":41},[31,2274,1124],{"class":78},[31,2276,491],{"class":41},[31,2278,2279,2281],{"class":33,"line":52},[31,2280,2234],{"class":37},[31,2282,2283],{"class":41}," CONFLICT DO NOTHING\n",[31,2285,2286],{"class":33,"line":61},[31,2287,2288],{"class":41},"RETURNING id;\n",[15,2290,2291,2292,2295,2296,2299],{},"Здесь пример намеренно использует общий ",[28,2293,2294],{},"DO NOTHING",": он сработает при любом unique\u002Fexclusion conflict. Если нужен точный conflict target для expression index, синтаксис должен совпасть с индексным выражением. Иногда проще завести нормализованную колонку ",[28,2297,2298],{},"email_normalized"," или generated column и повесить unique constraint на нее.",[15,2301,2302],{},"База должна быть последней линией защиты. Go-код может дать красивую ошибку раньше, но constraint гарантирует корректность при гонке.",[161,2304],{},[642,2306,2308],{"id":2307},"retry","Retry",[15,2310,2311,2312,2314],{},"При ",[28,2313,1150],{},", deadlock или optimistic conflict retry должен повторять всю транзакцию, а не только последний statement.",[15,2316,2317],{},"Псевдокод:",[21,2319,2321],{"className":462,"code":2320,"language":464,"meta":465,"style":26},"func WithTxRetry(ctx context.Context, maxAttempts int, fn func(tx Tx) error) error {\n    for attempt := 1; attempt \u003C= maxAttempts; attempt++ {\n        err := runInTx(ctx, fn)\n        if err == nil {\n            return nil\n        }\n        if !isRetryable(err) {\n            return err\n        }\n        sleepBackoff(attempt)\n    }\n    return ErrTooMuchContention\n}\n",[28,2322,2323,2381,2407,2420,2433,2441,2446,2459,2465,2469,2477,2482,2489],{"__ignoreMap":26},[31,2324,2325,2328,2331,2334,2338,2341,2343,2346,2348,2351,2354,2356,2359,2362,2364,2367,2370,2372,2375,2377,2379],{"class":33,"line":34},[31,2326,2327],{"class":37},"func",[31,2329,2330],{"class":481}," WithTxRetry",[31,2332,2333],{"class":41},"(",[31,2335,2337],{"class":2336},"s9osk","ctx",[31,2339,2340],{"class":481}," context",[31,2342,840],{"class":41},[31,2344,2345],{"class":481},"Context",[31,2347,380],{"class":41},[31,2349,2350],{"class":2336},"maxAttempts",[31,2352,2353],{"class":37}," int",[31,2355,380],{"class":41},[31,2357,2358],{"class":2336},"fn",[31,2360,2361],{"class":37}," func",[31,2363,2333],{"class":41},[31,2365,2366],{"class":2336},"tx",[31,2368,2369],{"class":481}," Tx",[31,2371,368],{"class":41},[31,2373,2374],{"class":37},"error",[31,2376,368],{"class":41},[31,2378,2374],{"class":37},[31,2380,508],{"class":41},[31,2382,2383,2386,2389,2391,2393,2396,2399,2402,2405],{"class":33,"line":45},[31,2384,2385],{"class":37},"    for",[31,2387,2388],{"class":41}," attempt ",[31,2390,475],{"class":37},[31,2392,93],{"class":78},[31,2394,2395],{"class":41},"; attempt ",[31,2397,2398],{"class":37},"\u003C=",[31,2400,2401],{"class":41}," maxAttempts; attempt",[31,2403,2404],{"class":37},"++",[31,2406,508],{"class":41},[31,2408,2409,2412,2414,2417],{"class":33,"line":52},[31,2410,2411],{"class":41},"        err ",[31,2413,475],{"class":37},[31,2415,2416],{"class":481}," runInTx",[31,2418,2419],{"class":41},"(ctx, fn)\n",[31,2421,2422,2425,2427,2429,2431],{"class":33,"line":61},[31,2423,2424],{"class":37},"        if",[31,2426,499],{"class":41},[31,2428,2132],{"class":37},[31,2430,505],{"class":78},[31,2432,508],{"class":41},[31,2434,2435,2438],{"class":33,"line":82},[31,2436,2437],{"class":37},"            return",[31,2439,2440],{"class":78}," nil\n",[31,2442,2443],{"class":33,"line":98},[31,2444,2445],{"class":41},"        }\n",[31,2447,2448,2450,2453,2456],{"class":33,"line":103},[31,2449,2424],{"class":37},[31,2451,2452],{"class":37}," !",[31,2454,2455],{"class":481},"isRetryable",[31,2457,2458],{"class":41},"(err) {\n",[31,2460,2461,2463],{"class":33,"line":110},[31,2462,2437],{"class":37},[31,2464,516],{"class":41},[31,2466,2467],{"class":33,"line":126},[31,2468,2445],{"class":41},[31,2470,2471,2474],{"class":33,"line":140},[31,2472,2473],{"class":481},"        sleepBackoff",[31,2475,2476],{"class":41},"(attempt)\n",[31,2478,2479],{"class":33,"line":145},[31,2480,2481],{"class":41},"    }\n",[31,2483,2484,2486],{"class":33,"line":602},[31,2485,513],{"class":37},[31,2487,2488],{"class":41}," ErrTooMuchContention\n",[31,2490,2491],{"class":33,"line":607},[31,2492,521],{"class":41},[15,2494,2495],{},"Retryable обычно:",[1143,2497,2498,2504,2510],{},[1146,2499,2500,2501,1155],{},"serialization failure, SQLSTATE ",[28,2502,2503],{},"40001",[1146,2505,2506,2507,1155],{},"deadlock detected, SQLSTATE ",[28,2508,2509],{},"40P01",[1146,2511,2512],{},"optimistic conflict, если use case допускает повтор.",[15,2514,2515],{},"Не все ошибки можно повторять. Unique violation при регистрации email - это бизнес-конфликт, а не повод бесконечно retry.",[642,2517,2519],{"id":2518},"retry-idempotency-и-side-effects","Retry, idempotency и side effects",[15,2521,2522],{},"Production retry опасен тем, что повторяет не только SQL, но и бизнес-намерение. Перед тем как добавить retry-wrapper, senior reviewer спрашивает: \"Если эта операция выполнится дважды, что именно сломается?\"",[15,2524,2525],{},"Матрица решений:",[169,2527,2528,2541],{},[172,2529,2530],{},[175,2531,2532,2535,2538],{},[178,2533,2534],{"align":180},"Ситуация",[178,2536,2537],{"align":180},"Retry?",[178,2539,2540],{"align":180},"Что требуется",[189,2542,2543,2559,2570,2583,2594,2605],{},[175,2544,2545,2553,2556],{},[194,2546,2547,2549,2550,2552],{"align":180},[28,2548,2503],{}," \u002F ",[28,2551,2509],{}," до внешних side effects",[194,2554,2555],{"align":180},"Да",[194,2557,2558],{"align":180},"Повторить весь use case в новой transaction",[175,2560,2561,2564,2567],{},[194,2562,2563],{"align":180},"Optimistic conflict при смене статуса",[194,2565,2566],{"align":180},"Иногда",[194,2568,2569],{"align":180},"Перечитать состояние, проверить, что намерение всё ещё валидно",[175,2571,2572,2577,2580],{},[194,2573,2574,2575],{"align":180},"Timeout клиента после неизвестного ",[28,2576,148],{},[194,2578,2579],{"align":180},"Не слепо",[194,2581,2582],{"align":180},"Idempotency key или повторное чтение результата операции",[175,2584,2585,2588,2591],{},[194,2586,2587],{"align":180},"Unique violation на email\u002Forder number",[194,2589,2590],{"align":180},"Обычно нет",[194,2592,2593],{"align":180},"Вернуть business conflict или существующий ресурс",[175,2595,2596,2599,2602],{},[194,2597,2598],{"align":180},"HTTP-вызов провайдеру уже отправлен",[194,2600,2601],{"align":180},"Только идемпотентно",[194,2603,2604],{"align":180},"Provider idempotency key, outbox\u002Finbox или reconciliation",[175,2606,2607,2610,2613],{},[194,2608,2609],{"align":180},"Ошибка после записи в БД, но до публикации event",[194,2611,2612],{"align":180},"Не простым retry",[194,2614,2615],{"align":180},"Transactional outbox или восстановимый publisher",[15,2617,2618],{},"Idempotency key должен жить в базе рядом с результатом операции, а не только в Redis\u002Fcache. Типичный паттерн:",[21,2620,2622],{"className":23,"code":2621,"language":25,"meta":26,"style":26},"CREATE TABLE idempotency_keys (\n    key text PRIMARY KEY,\n    operation text NOT NULL,\n    request_hash text NOT NULL,\n    response_json jsonb,\n    status text NOT NULL,\n    created_at timestamptz NOT NULL DEFAULT now()\n);\n",[28,2623,2624,2635,2647,2659,2670,2675,2686,2704],{"__ignoreMap":26},[31,2625,2626,2628,2630,2633],{"class":33,"line":34},[31,2627,1056],{"class":37},[31,2629,1059],{"class":37},[31,2631,2632],{"class":481}," idempotency_keys",[31,2634,1065],{"class":41},[31,2636,2637,2640,2643,2645],{"class":33,"line":45},[31,2638,2639],{"class":37},"    key",[31,2641,2642],{"class":37}," text",[31,2644,1076],{"class":37},[31,2646,1079],{"class":41},[31,2648,2649,2652,2654,2657],{"class":33,"line":52},[31,2650,2651],{"class":41},"    operation ",[31,2653,825],{"class":37},[31,2655,2656],{"class":37}," NOT NULL",[31,2658,1079],{"class":41},[31,2660,2661,2664,2666,2668],{"class":33,"line":61},[31,2662,2663],{"class":41},"    request_hash ",[31,2665,825],{"class":37},[31,2667,2656],{"class":37},[31,2669,1079],{"class":41},[31,2671,2672],{"class":33,"line":82},[31,2673,2674],{"class":41},"    response_json jsonb,\n",[31,2676,2677,2680,2682,2684],{"class":33,"line":98},[31,2678,2679],{"class":37},"    status",[31,2681,2642],{"class":37},[31,2683,2656],{"class":37},[31,2685,1079],{"class":41},[31,2687,2688,2691,2694,2696,2699,2702],{"class":33,"line":103},[31,2689,2690],{"class":41},"    created_at ",[31,2692,2693],{"class":37},"timestamptz",[31,2695,2656],{"class":37},[31,2697,2698],{"class":37}," DEFAULT",[31,2700,2701],{"class":37}," now",[31,2703,535],{"class":41},[31,2705,2706],{"class":33,"line":110},[31,2707,386],{"class":41},[15,2709,2710,2711,2713],{},"Если повторный запрос приходит с тем же key и тем же request hash, API возвращает сохранённый результат или продолжает безопасное завершение. Если key тот же, а тело другое, это ",[28,2712,1964],{},": клиент переиспользовал ключ для другого намерения.",[15,2715,2716],{},"Side effects нельзя делать \"внутри надежной транзакции\" простым HTTP-вызовом. База может откатиться, сеть может зависнуть, клиент может не получить ответ, а внешний сервис уже принял команду. Для событий и интеграций часто используют transactional outbox:",[21,2718,2721],{"className":2719,"code":2720,"language":825,"meta":26},[823],"BEGIN\n  update order status\n  insert into outbox_events(order_id, type, payload, dedupe_key)\nCOMMIT\n\nbackground publisher:\n  read unpublished events\n  publish with dedupe key\n  mark published\n",[28,2722,2720],{"__ignoreMap":26},[15,2724,2725,2726,2729],{},"Outbox не делает внешний мир exactly-once. Он делает запись факта и намерения атомарной в вашей базе, а потребителей заставляет быть идемпотентными по ",[28,2727,2728],{},"dedupe_key","\u002Fevent id.",[15,2731,2732],{},"Retry budget тоже часть корректности:",[1143,2734,2735,2741,2748,2751,2754,2757],{},[1146,2736,2737,2738,2740],{},"ограничьте ",[28,2739,2350],{},", иначе contention превратится в retry storm;",[1146,2742,2743,2744,2747],{},"проверяйте ",[28,2745,2746],{},"ctx.Err()"," перед новой попыткой и перед sleep;",[1146,2749,2750],{},"используйте exponential backoff с jitter, а не одинаковый sleep для всех запросов;",[1146,2752,2753],{},"не повторяйте после истечения request deadline;",[1146,2755,2756],{},"логируйте число попыток, SQLSTATE и финальный исход;",[1146,2758,2759],{},"считайте метрики retry\u002Fabort отдельно от обычных ошибок.",[15,2761,2762],{},"Если retry нужен только чтобы \"скрыть\" постоянные deadlocks, это не лечение. Нужно искать порядок lock'ов, долгие transactions, плохие планы, hot rows и missing indexes.",[161,2764],{},[164,2766,2768],{"id":2767},"практические-правила","Практические правила",[642,2770,2772],{"id":2771},"не-держите-транзакцию-дольше-нужного","Не держите транзакцию дольше нужного",[15,2774,2775],{},"Плохой use case:",[21,2777,2780],{"className":2778,"code":2779,"language":825,"meta":26},[823],"BEGIN\n  SELECT order FOR UPDATE\n  HTTP call to payment provider\n  UPDATE order\nCOMMIT\n",[28,2781,2779],{"__ignoreMap":26},[15,2783,2784],{},"Пока внешний HTTP-запрос висит, база держит locks. Это увеличивает latency, deadlocks и contention.",[15,2786,2787],{},"Лучше:",[21,2789,2792],{"className":2790,"code":2791,"language":825,"meta":26},[823],"BEGIN\n  mark payment attempt as pending\nCOMMIT\n\nHTTP call to payment provider\n\nBEGIN\n  finalize payment attempt idempotently\n  update order\nCOMMIT\n",[28,2793,2791],{"__ignoreMap":26},[15,2795,2796],{},"Да, код сложнее. Зато база не ждет сеть.",[161,2798],{},[164,2800,2802],{"id":2801},"как-об-этом-спрашивают","Как об этом спрашивают",[642,2804,2806],{"id":2805},"что-такое-acid","Что такое ACID?",[15,2808,2809],{},"Ответьте коротко по четырем свойствам и приведите пример перевода денег. Не уходите в CAP, если вопрос про транзакции в одной СУБД.",[642,2811,2813,2814,2816,2817,2819],{"id":2812},"чем-read-committed-отличается-от-repeatable-read","Чем ",[28,2815,806],{}," отличается от ",[28,2818,810],{},"?",[15,2821,857,2822,2824,2825,2827,2828,2830,2831,840],{},[28,2823,806],{}," каждый statement видит новый snapshot. В ",[28,2826,810],{}," транзакция видит стабильный snapshot с начала транзакции. В PostgreSQL ",[28,2829,810],{}," не допускает phantom read, но не равен ",[28,2832,1150],{},[642,2834,2836],{"id":2835},"что-такое-lost-update-и-как-защититься","Что такое lost update и как защититься?",[15,2838,2839,2840,2843,2844,2847,2848,2851],{},"Две транзакции читают старое значение и перезаписывают друг друга. Защита: атомарный ",[28,2841,2842],{},"UPDATE balance = balance + ...",", row lock ",[28,2845,2846],{},"FOR UPDATE",", optimistic locking через ",[28,2849,2850],{},"version",", более строгая изоляция.",[642,2853,2855],{"id":2854},"что-такое-write-skew","Что такое write skew?",[15,2857,2858,2859,2861],{},"Две транзакции читают общий инвариант по набору строк и обновляют разные строки, поэтому row-level конфликтов нет, но инвариант нарушается. Защита: ",[28,2860,1150],{},", explicit lock общего ресурса или другая модель данных.",[642,2863,2865,2866,2868],{"id":2864},"почему-select-for-update-не-всегда-спасает","Почему ",[28,2867,662],{}," не всегда спасает?",[15,2870,2871,2872,2874],{},"Он блокирует выбранные строки. Если строка еще не существует или инвариант касается диапазона\u002Fмножества строк, нужны predicate locks на ",[28,2873,1150],{},", constraint, table\u002Fadvisory lock или блокировка общего родительского объекта.",[642,2876,2878],{"id":2877},"что-делать-с-ошибкой-serialization-failure","Что делать с ошибкой serialization failure?",[15,2880,2881],{},"Откатить транзакцию и повторить весь use case в новой транзакции. Нельзя просто повторить последний SQL statement, потому что все решения внутри транзакции принимались на старом snapshot.",[161,2883],{},[642,2885,2887],{"id":2886},"практический-чеклист","Практический чеклист",[1143,2889,2890,2893,2896,2905,2915,2921,2926,2929,2937,2940],{},[1146,2891,2892],{},"Все изменения, которые должны быть атомарными, находятся в одной транзакции?",[1146,2894,2895],{},"Транзакция короткая и не делает внешние HTTP\u002FRPC-вызовы?",[1146,2897,2898,2899,2901,2902,2819],{},"Все SQL внутри use case выполняется через ",[28,2900,2366],{},", а не случайно через ",[28,2903,2904],{},"db",[1146,2906,2907,2908,2911,2912,2819],{},"Бизнес-уникальность защищена ",[28,2909,2910],{},"UNIQUE",", а не только ",[28,2913,2914],{},"SELECT before INSERT",[1146,2916,2917,2918,2920],{},"Для read-modify-write есть atomic update, ",[28,2919,2846],{}," или version?",[1146,2922,2923,2924,2819],{},"Для инварианта по множеству строк выбран подход: constraint, lock общего ресурса или ",[28,2925,1150],{},[1146,2927,2928],{},"Retry повторяет всю транзакцию?",[1146,2930,2931,2932,380,2934,2936],{},"Ошибки ",[28,2933,2503],{},[28,2935,2509],{},", unique violation и foreign key violation маппятся осмысленно?",[1146,2938,2939],{},"Миграции не держат сильные locks дольше ожидаемого?",[1146,2941,2942,2943,2819],{},"Есть timeout через ",[28,2944,450],{},[161,2946],{},[164,2948,2950],{"id":2949},"что-запомнить","Что запомнить",[1143,2952,2953,2956,2964,2970,2976],{},[1146,2954,2955],{},"Транзакция защищает атомарность, но длинная транзакция сама становится production-проблемой.",[1146,2957,2958,2960,2961,2963],{},[28,2959,806],{}," видит новый snapshot на каждый statement; ",[28,2962,810],{}," держит snapshot транзакции.",[1146,2965,2966,2967,2969],{},"Lost update лечится atomic update, ",[28,2968,2846],{},", optimistic locking или более строгой изоляцией.",[1146,2971,2972,2973,2975],{},"Write skew часто требует ",[28,2974,1150],{},", constraint или lock общего ресурса.",[1146,2977,2978],{},"Serialization failure и deadlock - повод повторить всю транзакцию, а не один последний запрос.",[161,2980],{},[164,2982,2984],{"id":2983},"практика","Практика",[2986,2987,2988,2997,3002,3005],"ol",{},[1146,2989,2990,2991,2994,2995,840],{},"Смоделируйте lost update на таблице ",[28,2992,2993],{},"accounts"," и исправьте через atomic ",[28,2996,55],{},[1146,2998,2999,3000,840],{},"Реализуйте optimistic locking через колонку ",[28,3001,2850],{},[1146,3003,3004],{},"Напишите транзакцию перевода денег и перечислите, какие ошибки можно retry.",[1146,3006,3007],{},"Найдите во внешнем HTTP\u002FRPC-вызове место, которое нельзя держать внутри transaction.",[161,3009],{},[164,3011,3013],{"id":3012},"интерактивная-практика","Интерактивная практика",[3015,3016,3020,3027,3049],"quiz",{"answer":3017,"id":3018,"xp":3019},"4","db-transactions-q1","10",[15,3021,3022,3023,3026],{},"Что нужно повторять после ",[28,3024,3025],{},"serialization_failure"," в PostgreSQL?",[3028,3029,3030],"template",{"v-slot:options":26},[1143,3031,3032,3037,3041,3046],{},[1146,3033,3034,3035],{},"Только последний ",[28,3036,1190],{},[1146,3038,3034,3039],{},[28,3040,55],{},[1146,3042,3043,3044],{},"Только ",[28,3045,148],{},[1146,3047,3048],{},"Весь use case в новой транзакции",[3028,3050,3051],{"v-slot:explanation":26},[15,3052,3053],{},"Решения внутри транзакции принимались на старом snapshot, поэтому retry должен повторить всю бизнес-операцию.",[3055,3056,3060,3063,3163],"predict",{"answer":3057,"id":3058,"xp":3059},"retry\\nfail","db-transactions-p1","15",[15,3061,3062],{},"Что вернёт запрос?",[3028,3064,3065],{"v-slot:code":26},[21,3066,3068],{"className":23,"code":3067,"language":25,"meta":26,"style":26},"WITH errors(sqlstate) AS (\n    VALUES ('40001'), ('23503')\n)\nSELECT CASE\n    WHEN sqlstate IN ('40001', '40P01') THEN 'retry'\n    ELSE 'fail'\nEND AS decision\nFROM errors;\n",[28,3069,3070,3081,3099,3103,3110,3137,3145,3156],{"__ignoreMap":26},[31,3071,3072,3074,3077,3079],{"class":33,"line":34},[31,3073,1584],{"class":37},[31,3075,3076],{"class":41}," errors(sqlstate) ",[31,3078,1590],{"class":37},[31,3080,1065],{"class":41},[31,3082,3083,3086,3088,3091,3094,3097],{"class":33,"line":45},[31,3084,3085],{"class":37},"    VALUES",[31,3087,374],{"class":41},[31,3089,3090],{"class":308},"'40001'",[31,3092,3093],{"class":41},"), (",[31,3095,3096],{"class":308},"'23503'",[31,3098,491],{"class":41},[31,3100,3101],{"class":33,"line":52},[31,3102,491],{"class":41},[31,3104,3105,3107],{"class":33,"line":61},[31,3106,1190],{"class":37},[31,3108,3109],{"class":37}," CASE\n",[31,3111,3112,3115,3118,3120,3122,3124,3126,3129,3131,3134],{"class":33,"line":82},[31,3113,3114],{"class":37},"    WHEN",[31,3116,3117],{"class":41}," sqlstate ",[31,3119,1678],{"class":37},[31,3121,374],{"class":41},[31,3123,3090],{"class":308},[31,3125,380],{"class":41},[31,3127,3128],{"class":308},"'40P01'",[31,3130,368],{"class":41},[31,3132,3133],{"class":37},"THEN",[31,3135,3136],{"class":308}," 'retry'\n",[31,3138,3139,3142],{"class":33,"line":98},[31,3140,3141],{"class":37},"    ELSE",[31,3143,3144],{"class":308}," 'fail'\n",[31,3146,3147,3150,3153],{"class":33,"line":103},[31,3148,3149],{"class":37},"END",[31,3151,3152],{"class":37}," AS",[31,3154,3155],{"class":41}," decision\n",[31,3157,3158,3160],{"class":33,"line":110},[31,3159,1198],{"class":37},[31,3161,3162],{"class":41}," errors;\n",[3028,3164,3165],{"v-slot:hint":26},[15,3166,3167,3169,3170,3172],{},[28,3168,2503],{}," - serialization failure, ",[28,3171,2509],{}," - deadlock detected.",[161,3174],{},[164,3176,3178],{"id":3177},"полезные-источники","Полезные источники",[1143,3180,3181,3191,3198,3205],{},[1146,3182,3183,3184],{},"PostgreSQL Documentation: ",[3185,3186,3190],"a",{"href":3187,"rel":3188},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Ftransaction-iso.html",[3189],"nofollow","Transaction Isolation",[1146,3192,3183,3193],{},[3185,3194,3197],{"href":3195,"rel":3196},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Fexplicit-locking.html",[3189],"Explicit Locking",[1146,3199,3183,3200],{},[3185,3201,3204],{"href":3202,"rel":3203},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Fsql-lock.html",[3189],"LOCK",[1146,3206,3183,3207],{},[3185,3208,3211],{"href":3209,"rel":3210},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Ftransactions.html",[3189],"Transaction Processing",[3213,3214,3215],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}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 pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":26,"searchDepth":45,"depth":45,"links":3217},[3218,3219,3220,3230,3235,3247,3250,3261,3262,3263,3264],{"id":166,"depth":45,"text":167},{"id":243,"depth":45,"text":244},{"id":639,"depth":45,"text":640,"children":3221},[3222,3223,3224,3225,3226,3227,3228,3229],{"id":644,"depth":52,"text":645},{"id":668,"depth":52,"text":669},{"id":816,"depth":52,"text":691},{"id":845,"depth":52,"text":699},{"id":868,"depth":52,"text":707},{"id":885,"depth":52,"text":886},{"id":1039,"depth":52,"text":1040},{"id":1246,"depth":52,"text":1247},{"id":1272,"depth":45,"text":1273,"children":3231},[3232,3233,3234],{"id":1276,"depth":52,"text":761},{"id":1321,"depth":52,"text":775},{"id":1343,"depth":52,"text":788},{"id":1386,"depth":45,"text":1387,"children":3236},[3237,3238,3239,3240,3241,3242,3243,3244,3245,3246],{"id":1390,"depth":52,"text":1391},{"id":1397,"depth":52,"text":1398},{"id":1705,"depth":52,"text":1706},{"id":1737,"depth":52,"text":1738},{"id":1830,"depth":52,"text":1831},{"id":1834,"depth":52,"text":1835},{"id":1881,"depth":52,"text":1882},{"id":2155,"depth":52,"text":2156},{"id":2307,"depth":52,"text":2308},{"id":2518,"depth":52,"text":2519},{"id":2767,"depth":45,"text":2768,"children":3248},[3249],{"id":2771,"depth":52,"text":2772},{"id":2801,"depth":45,"text":2802,"children":3251},[3252,3253,3255,3256,3257,3259,3260],{"id":2805,"depth":52,"text":2806},{"id":2812,"depth":52,"text":3254},"Чем READ COMMITTED отличается от REPEATABLE READ?",{"id":2835,"depth":52,"text":2836},{"id":2854,"depth":52,"text":2855},{"id":2864,"depth":52,"text":3258},"Почему SELECT FOR UPDATE не всегда спасает?",{"id":2877,"depth":52,"text":2878},{"id":2886,"depth":52,"text":2887},{"id":2949,"depth":45,"text":2950},{"id":2983,"depth":45,"text":2984},{"id":3012,"depth":45,"text":3013},{"id":3177,"depth":45,"text":3178},1781022065440]