消えた取引
台帳には、確かに二つ立っている。なのに、蔵の数は、片方しか引かれていない。
わたしは隊商の差配役だ。街道に点々と宿場を構え、そのどこからでも同じ品を卸せるよう、中央の蔵と、一冊の在庫元帳を預かっている。触媒の壺が幾つ残っているか——それを記すのが、わたしの四十年の仕事だった。注文が来れば、宿場の窓口が蔵に「あと幾つ残っているか」を早馬で問い、戻った数から引いて、引いた後の数を書き戻す。それだけの、素直な帳面だ。
棚卸しの朝だった。現物の壺と、帳面の残りを照らし合わせていて、手が止まった。今朝、星砂の触媒に、二つの引き当てがあった。一つは三壺、もう一つは五壺。どちらも、この手でちゃんと記した。台帳には、二つとも立っている。だが——蔵の残りは、五壺しか引かれていない。三壺の引き当ては、まるで最初から無かったかのように、数に効いていない。
魔物使いは、帳場の隅で、わたしの元帳と棚の壺とを、交互に検めていた。広げたままの二つの取引の行を上から下へ読んでは、棚に残る壺の数へ視線を返す。それを幾度も繰り返す。問いも、慰めも、口にしない。ただ、数の出入りを勘定し直しているようだった。
市を回る連れの口上は、いつも半分が与太だ。“勘定の合わぬ蔵を視る、変わった魔物使いが市の外れにいる”——その半分に賭けて、商いの外へ使いを出した。お抱えの記帳役に何度検めさせても、原因が掴めなかったからだ。来たのが、カイナと名乗る、この人だった。
四十年、書き損じたことはない
わたしは、自分の帳面を誇りはしない。だが、静かな矜持はある。
「四十年、この帳面で食ってきました。書き損じたことは、ありません。今朝の二つの引き当ても、両方、この手でちゃんと記した。書き間違いも、書き漏れも、ない」
ここで、一拍置いた。不可解を、どう差し出せばいいのか、言葉を探した。
「なのに、片方の取引が、蔵の数に効いていないんです。台帳には、立っている。確かに記した。なのに——最初から無かったように、残りが減っていない。書いたものが、消えるんです」
どちらの手跡が悪いのか、いくら見ても分からなかった。どちらも、正しく書かれている。正しく書いて、なお消える。勘定家の言うことではないと、自分でも思う。だが、本当に、そうとしか言えなかった。
カイナは、ようやく口を開いた。わたしの記帳を貶める響きは、なかった。
「お前の手跡に、間違いは一つもない。二つとも、正しく書かれている。——だが、片方が"なかったこと"になるのは、書いた後じゃない。書く前に、種が蒔かれている。読んでから書くまでの間に、何が起きるか。それを、この目で見てからだ」
「読んでから、書くまで……?」わたしには、意味が掴めなかった。「早馬が、往って還る、あの間のことですか」
残りを読んで、引いて、書き戻す
わたしは、窓口の手順を写した板を取り出した。難しいことは、何もしていない。だからこそ、ここに穴があるとは、思えなかった。
「注文が来たら、蔵にあと幾つ残っているか、早馬で問います。残りが戻ったら、注文の数を引いて、引いた後の数を書き戻す。それだけです」
蔵は遠い。残りを問うのも、書き戻すのも、早馬を出して、戻るのを待つ。その往復を、術式の言葉では read と write と呼ぶのだと、カイナは言った。早馬が戻るのを待つ間、帳場は手を遊ばせない。次の注文が来れば、また別の窓口が、蔵へ早馬を出す。
| |
read も write も、返ってくるのに間がある。だから Promise——「いずれ返ってくる」という約束で包まれ、その約束を待つのが await だ。窓口の手順は、こうなる。
| |
カイナは、手順を最後まで追って、低く言った。「読み、引き、書く。一点の曇りもない。——だが、await store.read のところで、早馬が出る。戻るまで、この窓口は手を止める。その間に、巡回は他の窓口へ回る。読んでから書くまでの間に、別の手が、同じ蔵を覗ける」
わたしは、まだ実感が湧かなかった。一度に一つしか書けないのに、と心の中で繰り返すばかりだった。
早馬を、途中で止める
「その不整合は、ここで起こせる」とカイナは言った。「二つの注文を流し、早馬を、途中で止めるだけだ」
順に戻れば、数は合う
カイナがまず組んだのは、蔵を真似た小さな帳面だった。残りを問えば数を返し、書き戻せば数を改める。本物の蔵と同じ作法で、しかし早馬の往復を、こちらの手で握れる仕掛けだ。
そして、二つの注文を素直に流した。星砂を三壺引く窓口と、五壺引く窓口。早馬が順に戻れば——一つ目が読んで、引いて、書き終えてから、二つ目が読む——どちらも正しく引けた。十から三で七、七から五で二。残りは二壺。消えない。
| |
カイナは、同じ流しを何度か繰り返した。合うこともあれば——と、ここで初めて、合わないことが混じった。
「これだ」とカイナは言った。「お前が棚卸しまで掴めなかったのは、これが運任せに紛れるからだ。早馬の還る順しだいで、合うこともある。合わないこともある。だが、運に任せていては、正体は掴めん。消える刻を、こちらで作る」
(蔵の数や書き戻しの記録を直に覗く窓——remaining や writeLog——は、模擬戦のために添えてある。writeLog は、書き戻された数を順に控える帳面だ。台帳に立った取引を、そのまま並べたものだと思っていい。)
同じ刻に、同じ数を持ち帰らせる
「鍵は、早馬だ」とカイナは言った。「残りを問う早馬を、還る途中で止めておく。二つの窓口の早馬を、両方止めて、同じ"十"を持たせてから、揃って還す。そうすれば、二人は必ず、同じ古い数を握る」
これが、肝だった。本物の蔵では、二つの早馬が同じ刻に同じ数を持ち帰るのは、繁忙のときの、稀な巡り合わせだ。だからこそ、棚卸しまで露れない。カイナは、その稀な巡り合わせを、檻の中で狙って起こした。早馬を二つとも止め、両方に「残り十」を持たせ、それから順に書き戻させる。
| |
一つ目の窓口は、十から三を引いて、七と書いた。二つ目の窓口は、その七を見ないまま——自分が握った十から五を引いて、五と書いた。蔵の残りは、五。
わたしは、帳面(writeLog)を覗いた。七と五、二つの書き戻しが、どちらも記されている。確かに、両方とも書かれた。なのに、蔵の残りに効いたのは、後の五だけだった。正しくは、十から三を引き、さらに五を引いて、二壺のはず。それが、五壺のまま。三を引いた取引が、消えていた。台帳に二つ、残りは片方。棚卸しのあのズレが、ここに、狙って起こせる形で立った。
わたしは、ようやく像を結びかけた。「……二人とも、十を握っていました。一人が七と書いて、もう一人が、その七を見ずに、五で塗った。早馬が、同じ"十"を持ち帰ったから」
頭の中で、早馬たちが交差して帳面を塗りつぶしていく光景が、一枚の絵になって浮かび上がった。

読んでから書くまでの、隙
カイナは、二つの早馬の符牒を、卓に並べた。
「種は、読んでから書くまでの隙だ。残りを問う早馬が還るまで、巡回は他の窓口へ回る。その隙に、もう一人が同じ古い残りを読む。書くのは一つずつでも、古い数を二人が握ることは、起きる」
「一度に一つしか書けないなら」とわたしは食い下がった。「なぜ、重なるんです」
「書く瞬間は、重ならない。お前の言う通り、一つずつだ。だが、消えるのは書く瞬間の衝突じゃない。読んだ数が、書く頃にはもう古い——その隙だ。一つずつ書いても、古い数を二人が握れば、後の手が先の手を、塗り消す」
ここで、わたしの思い込みが、ひとつ覆った。一度に一つしか書けないから、安全だと思っていた。だが、消えるのは書くときではなく、読んだあの瞬間から、もう種が蒔かれていたのだ。
カイナは、面白いところだ、と言った。「どちらの窓口の手順も、型の上では一点の曇りもない。残りを読み、引き、書く。型は、“数を引き忘れた"とか"品を取り違えた"なら叱る。だが、“二人が同じ古い数を読んだ”——その重なりは、型のどこにも書かれない。走らせて、二つの早馬が同じ刻に同じ数を持ち帰ったときだけ、立ち上がる」
二つ以上の処理が、同じ資源を読んで、書き換えて、書き戻す。その手順が重なって、一方の書き込みがもう一方を上書きして消す。これを Lost Update(ロストアップデート)——読んで書き戻すまでの隙に割り込まれ、更新が静かに失われる事故——という。そして、読んで・直して・書き戻すこの一連の手順を、read-modify-write と呼ぶ。隙が生まれるのは、いつもこの真ん中、読んでから書くまでの間だ。
「一つ、はっきりさせておく」とカイナは続けた。「前に、別の工房で、二つの錬金釜が、互いの鍵を握ったまま、相手の鍵が空くのを待って、にらみ合って石になったことがある。あれは、動かなくなる獣だった。互いを待って、どちらも前に進めない。——だが、これは逆だ。お前の二つの窓口は、止まらない。両方とも、最後まで走り切る。走り切って、なお、片方が消える。止まる病と、消える病。似て、別の獣だ」
そう言って、カイナは符牒の一つを指で弾いた。「これは、分裂する妖精だ。一匹に見えて、二匹いる。双子は、互いを見ない。同じ壺の数字を、別々の刻に握って、別々に塗り替える。互いの仕事を、知らないまま」
平時には、決して露れない。二つの早馬が同じ刻に同じ数を持ち帰った、ちょうどその巡り合わせでだけ、片方が消える。だから、わたしは四十年、書き損じもないのに、棚卸しまで気づけなかった。
帳に、版を刻む
「壺の数字に、版を添える」とカイナは言った。
後から来た手に、やり直させる
「書き換えるたび、版を一つ繰り上げる。零版、一版、二版、と。誰かが書けば、版が進む。残りの数とは別に、書かれた回数の証だ。肝心なのは——版が書かれるたびに、必ず一つ増えること。減ることも、途中を飛ばすこともない。常に、前へ進むだけだ」
数えるのは滅多にぶつからない、と楽観して、皆に好きに読ませ、好きに書かせる。ただし書き戻す時だけ、版を照らす。この手当てを、Optimistic Locking(楽観的ロック)——衝突は稀だと構えて、ぶつかったときだけやり直させる仕組み——という。
「書き戻すとき、こう検める」とカイナは言った。「自分が読んだのは、何版か。読んだのが零版で、今の壺も零版なら——わたしが読んでから、誰も書いていない。だから、書いてよい。版を一版に繰り上げて。だが、読んだのは零版なのに、今の壺が一版になっていたら——読んでから書くまでの間に、誰かが書いた証拠だ。その時は、書かせない。退いて、読み直して、最新の数から、やり直す」
この「読んだ版と今の版を照らし、同じときだけ書く」一手を、compareAndSwap——読んだときの値と今の値を見比べ、変わっていなければ書き換える操作——という。版が合わなければ、衝突の合図を投げる。版が必ず一つずつ前へ進むからこそ、「読んだ版と今の版が同じ」は「読んでから誰も書いていない」と、ぴたり同じ意味になる。間に誰か一人でも書いていれば、版はもう進んでいて、決して元の数には戻っていないからだ。
照らすのと書くのは、一続きで行う。照らしてから書き入れるまでの間に、また別の手が割り込めるなら、せっかくの照合が無駄になる。だから、版を確かめて書き換えるこの一手は、途中で割り込ませない、ひとかたまりの所作にする。
| |
そして、窓口の手順は、こう変わった。
| |
「変えたのは、書き戻す時の一手だけだ」とカイナは言った。「前は、握った数を、そのまま塗った。今度は、塗る前に版を照らす。読んだ版と、今の版。同じなら塗る、違えば退いて、読み直す。読む・引く・書く、の形は、何も変わっていない」
ここで、カイナは念を押した。指したのは、ぶつかった後の読み直しのところだ。「肝は、ぶつかったら、必ず読み直すことだ。退いておいて、さっき握った古い数で、また同じものを書こうとしたら——版は照らせても、塗るのは古い計算だ。それでは検めただけで、直していない。やり直すなら、まず今の数を、もう一度読む。それから、引く」
reserveSafe の中で、ぶつかった後にもう一度 store.read を呼び、その新しい record.remaining から引き直しているのは、そのためだった。古い手元の数を握り直すのではなく、最新を読み直す。それでこそ、消えた更新が、次の計算に乗る。
さあ、と、カイナは同じ檻を組んだ。さっきと寸分同じ注入——二つの早馬を止め、両方に「残り十・零版」を持ち帰らせる。
| |
一つ目の窓口は、読んだ零版が今も零版だったから、七と書けた。版は、一版へ。二つ目の窓口は、読んだのは零版なのに、今の壺は一版になっていた。一つ目が、書いたのだ。だから、衝突。二つ目は退いて、読み直す——今度は、最新の「残り七・一版」を握る。七から五を引いて、二。再び、版を照らす。読んだ一版が今も一版だから、書けた。残りは、二壺。版は、二版へ。
三を引いた手も、五を引いた手も、どちらも消えなかった。
版を刻み、弾かれた手が退いて読み直す一連の流動が、今度は整然とした歩みとなって見えた。

「これは、運じゃない」とカイナは、念を押すように言った。「後から書こうとした手は、必ず版違いに気づく。零版で読んで、壺が一版なら、間に誰かが書いた——それは数を照らさずとも、版が告げる。気づいたら、必ず読み直す。読み直せば、必ず最新の七を握る。だから、どちらの引き当ても、消えはしない」
断っておくが、と、カイナは付け加えた。「版が消すのは、読んでから書くまでに割り込まれた、その上書きだ。ぶつかった手は、退いて、やり直す。そのやり直しの間に、もう一度ぶつかることは、ある——次でまた、別の手が書けば。だが、ぶつかるたび版が告げ、読み直すたび最新を握る。だから、いつまでも消えはしない」
わたしは、ひとつ訊いた。後から来た手を、待たせはしないのか、と。
「待たせはしない」とカイナは言った。「先に書いた者は、止めない。好きに書かせる。——ぶつかった者にだけ、もう一度、書かせる」
数は嘘をつけても、版は嘘をつけない
「ですが」とわたしは、勘定家の常識から、口を挟んだ。「版などなくとも、残りの数を照らせばよいのでは。読んだ時が十で、書く時も十なら、誰も触っていない。数が同じなら、無事でしょう」
カイナは、口で否定しなかった。代わりに、版でなく「残りの数」を照らす照合を、その場で、仮に組んで見せた。本筋では使わない、反証のための、使い捨ての手だ。
| |
「壺が十だった」とカイナは言った。「ある手が三引いて七にし、別の手が、補充で三足して十に戻した。お前が読んだ十と、書こうとする今の十。数は、同じだ。だが、その間に壺は二度、書き換わっている。数だけ照らせば、“誰も触っていない"と見える。間に挟まった二つの書き換えを、まるごと見逃す」
そして、版に戻した。「数は、元に戻れる。十から七、また十へ。だが版は、戻らない。零版、一版、二版、と進むだけだ。同じ手が今読めば、壺は二版。自分が握ったのは、零版。食い違う。だから、版なら——往復しても、必ず気づく。数は嘘をつけても、版は嘘をつけない。照らすのは、数じゃない。版だ」
値が同じ場所を二度読んだとき、その間に別の手が値を変えて、また元へ戻していても、二度の読みでは見分けられない。この見逃しを ABA 問題——値が A から B へ、また A へ戻ると、何も起きなかったように見えてしまう罠——という。版は単調に増えるだけで戻らないから、間の書き換えを構造として見逃さない。
| |
版は、ただ一つ繰り上がる数でいい、とカイナは言った。凝った印は要らない。戻らない——それだけが、効く。
待つか、やり直すか
「いっそ」とわたしは、別の手を思いついて訊いた。「片方を待たせて、触らせなければよいのでは。一人が書き終えるまで、もう一人を蔵に入れない。そうすれば、二人が同じ数を読むことも、ない」
「それも、一つの手だ」とカイナは言った。「さっき話した、鍵で釜を縛る手——一人が鍵を握れば、もう一人は外で待つ。あれを壺に掛ければ、二人が同じ数を読むことは、起きない。ぶつかる前に、待たせる。“どうせぶつかる"と構えて、先に締める手だ」
だが、と、カイナは続けた。「待たせれば、列ができる。ぶつかりが滅多に起きないのに、皆を一列に待たせるのは、惜しい。版を照らす手は、待たせない。皆、好きに読んで、好きに書く。ぶつかった時だけ、やり直す。——ぶつかりが稀なら、こちらが速い。ぶつかりが多いなら、待たせるほうが、無駄がない。どちらが上でもない。お前の蔵で、二人が同じ壺を奪い合うのが、年に何度か。なら、版を照らす手だ」
待たせて防ぐか、やり直してすり抜けるか。それは、衝突がどれだけ起きるかで選ぶものなのだと、わたしは知った。
やり直しに、刻限を
「やり直しは、いつまで続くのです」とわたしは訊いた。「ぶつかり続けたら、永遠に?」
カイナは即答せず、わたしの帳面の隅に、小さく版の数字を書き添えた。
「やり直しにも、限りを設ける。何度ぶつかっても通らなければ、諦めて、呼び手に返す。さもなくば、運悪くぶつかり続けて、いつまでも書けない手が出る」
| |
「もし大繁盛で、十人が同じ壺を狙えば」とカイナは言った。「皆が一斉に弾かれ、一斉にやり直して、また一斉にぶつかる。その時は、やり直す刻を、少しずつ散らす別の工夫が要る。前に、嵐の中で雷鳥が揃って飛び立って、塔を潰した話があった——あのとき効いた、揺らぎを混ぜる手だ。だが、それはこの蔵の話じゃない」
ここで、わたしは、最初に口にした疑問を思い出した。「そういえば——わたしは初め、“一度に一つしか書けないのに、なぜ重なる"と言いました。一度に一つなら、後から来た方を、はじく札を立てればいいのでは。一つの窓口が、二度書こうとするのを止めるように」
「前に、一つの転送陣が、連打で同じ荷を二度送った話がある」とカイナは言った。「あれは、一人が二度動くのを止める札だった。一人の手が、一度きりになるように、入口で締める。——だが、今度は逆だ。二人が、それぞれ一度ずつ、正しく動く。一度ずつなら、入口の札は立たない。立てるものが、無い。同じ"重なり"でも、一人の重なりと、二人の重なりは、別の獣だ。一人を止める札では、二人には届かない」
一人の連打を止める仕組みと、独立した二人の書き手が同じ数を握る競合は、別のものだ。前者は入口で一人を締めればいい。後者は、入口では締められない。だから、書き戻す時に版を照らす。わたしは、ようやく、その違いが腑に落ちた。
数と帳面を、照らす
模擬戦を、まとめて走らせた。
| |
六本、すべて緑。Before の窓口は、早馬が順に戻る素直な流しなら両方とも正しく引き、同じ数を二人に握らせた途端、後の書き戻しが先を塗り消した。After の窓口は、まったく同じ注入を流しても、後から来た手が版違いに気づいて読み直し、二つの引き当てが、どちらも残った。
変えたのは、書き戻す時の一手だけだった。残りを問う早馬も、引く計算も、外から見た「予約する」作法も、Before と After でまるごと同じ。違うのは、塗る前に版を照らすか、照らさずに塗るか。その一点が、消える取引を消した。
数の隣に、版を置く
蔵の数と、帳面の残りが、もう食い違わなくなった。棚卸しのズレは、消えた。
「わたしは、書くことばかり見ていました」とわたしは言った。「正しく書けば、正しく残ると。でも——書く前に読んだ数が、書く頃にはもう古いことがある。版は、その"古さ"を、教えてくれる。読んだ時の版と、書く時の版。それが食い違えば、わたしの手元の数は、もう古い。四十年、数だけを信じてきました。数の隣に、版を置くべきだったんですね」
カイナは、最後に一つだけ、別の帳場の話をした。版が消すのは、読んでから書くまでに割り込まれた、その上書きだけだ、と。待たせて防ぐ手も、しばらくズレを許して後でゆっくり揃える手も、大きな帳場——本物のデータベース——での版の掛け方も、それぞれ別の獣、別の帳場の話だと。詳しくは、また別の機会に、とだけ言い添えた。
カイナは帳場を出る前に、棚の壺を一つ、手に取った。中の星砂を、灯りに透かして、また戻す。
「数は、元へ戻る。だが、お前が今日添えた版は、戻らない。——覚えておくのは、それだけでいい」
それだけ言い残して、カイナは帳場を後にした。勘定で、答えの出ることだけを。
わたしは、元帳を開いた。星砂の残りの数の隣に、小さく「零」と、版を一つ、書き添えた。これから書き換えるたびに、ここを一つ繰り上げる。インクが乾くのを、しばらく、見ていた。
📜 カイナの魔獣契約録(Tamer’s Registry)
- 魔獣名(クラス/パターン名): ピクシー(分裂する妖精)/ データレース・ロストアップデート(Optimistic Locking=版の照合で衝突を検知し読み直してやり直す/対:Pessimistic Locking=事前に締めて待たせる)
- 危険度(難易度/バグの影響度): ★★★★☆(書き損じも例外も無く、更新が静かに消える。台帳に二取引立つのに残りは片方しか減らず、在庫が実際より多く見える=幽霊在庫から、やがて売り越し・二重引き当てに化ける。エラーを出さないぶん発覚が遅く、棚卸しまで露れない)
- 主な生態(アンチパターンの特徴):
- read→(早馬の往復=中断点)→modify→write の「読んでから書くまでの隙」に、別フローが同じ古い値を読む。二人とも古い値を握り、後の write が先の write を無音で上書きする(Lost Update)。デッドロックの循環待機(互いを待って止まる・liveness)と異なり、両フローは止まらず完走して、なお一方が消える(safety)
- JS はシングルスレッドで「書く瞬間」は重ならないが、消失は書く瞬間でなく「読んだ値が書く頃には古い」隙に宿る。一度に一つ書いても、古い値を二人が握れば消える
- 害はタイミング依存。早馬(I/O)が順に戻れば(直列)露見せず、同じ刻に同じ値を持ち帰った時だけ消える=棚卸しまで気づけない
- 契約のポイント(設計の要点):
- Optimistic Locking=レコードに version(単調増加)を持たせ、書き戻し時に compareAndSwap(読んだ version=現在 version のときだけ書き、version を繰り上げる)。不一致=衝突=間に誰かが書いた証拠ゆえ書かせない
- 読み直しリトライ=衝突したら最新値を読み直して再計算し再 CAS(古い手元値の再送は、検知だけで直さない誤実装)。maxRetries で高競合の無限ループを防ぐ
- 1:1 の単一差分=Before(store.write で無検証上書き)→ After(store.compareAndSwap で版照合+for ループの読み直しリトライ)。read→modify の骨格と外部窓口は不変、差分は「書き戻しを版照合にし、衝突時に読み直す」のみ
- なぜ値でなく版か(ABA)=値(remaining)は 10→7→10 と往復しうるが version は戻らない。値比較は往復を見逃すが、版比較は単調増加ゆえ間の更新を構造として検知
- 契約外事項(保証しないこと):
- Pessimistic Locking(鍵/Mutex)は別の賭け方=事前に締めて待たせる。衝突が稀なら楽観が速く、頻繁なら悲観/待ち行列が無駄が少ない(優劣でなく衝突頻度で選ぶ)。ABA は楽観に特有で、悲観には無い
- リトライストームは高競合で多発=やり直す刻を散らす Jitter(揺らぎ)で対称を破る。本話の最小形は version+maxRetries に絞り、深入りしない
- 再入ガード(単一書き手の連打を止める札)では防げない=独立した二人が各一度ずつ書く競合には届かない
- 結果整合性は逆の設計思想=「今すぐ揃える」楽観ロックに対し「ズレを許して後で揃える」。別の獣の領分
- 本話はアプリ層の最小形(in-memory+version)。実 DB(
WHERE version=?・条件付き書き込み)への一般化は接続のみ
- 現在のステータス: 🟢 壺に版を刻み、版の照合で「読んでから誰かが書いたか」を検知、ぶつかった手だけ読み直してやり直させ、更新の消失を構造から排除した契約成立(衝突は事後検知・先着は止めない)/待たせる悲観ロック・高競合の揺らぎ・結果整合性は別の獣として後日。型で消せぬ同時更新の競合を、版の照合で律する一手が、また加わった
