簡體   English   中英

創建引用可以在帶有 objectify 的事務內引發 ConcurrentModificationException

[英]Creating a reference can throw a ConcurrentModificationException inside of a transaction with objectify

我在這樣的事務中進行祖先查詢:

Task task = OfyService.ofy().load().type(Task.class)
                        .ancestor(jobKey)
                        .filter("locationKey", locationKey)
                        .first().now();

稍后在事務中,我創建並保存了一個新實體,該實體使用我在ancestor()中使用的鍵作為Ref<?>屬性:

Task newTask = new Task(jobKey)

// Task POJO with the following property and constructor:
@Parent
private Ref<Job> jobKey;

public Task(Key<Job> jobKey) {
    this.jobKey = Ref.create(jobKey);
}

當我的整個方法在一秒鍾內運行多次時,我在jobKey上得到一個ConcurrentModificationException 這很奇怪,因為我所做的只是創建一個引用並將其設置為一個屬性。 我查看了Ref<?>的描述,它說:

請注意,這些方法可能會或可能不會引發與數據存儲操作相關的運行時異常;ConcurrentModificationException、DatastoreTimeoutException、DatastoreFailureException 和 DatastoreNeedIndexException。 一些 Ref 隱藏了可能引發這些異常的數據存儲操作。

有人可以向我解釋Ref<?>發生了什么以及為什么它向我拋出ConcurrentModificationException嗎? 看起來它是這里的罪魁禍首。

這是 Objectify 的 API 搞砸並濫用異常系統,以傳達重試

事務系統具有解決基本問題的三種主要方法。 想象一下這一系列命令,都是單個事務的一部分(用 SQL 編寫,假設它可讀性強並且足夠熟悉可以理解。這只是一個示例):

// transfer 10 bucks from speedy to me
int rBalance = [SELECT balance FROM accounts WHERE user = 'rzwitserloot']
int sBalance = [SELECT balance FROM accounts WHERE user = 'Speedy']
if (sBalance < 10) throw new BalanceInsufficientException();
sBalance -= 10;
rBalance += 10;
[UPDATE accounts SET balance = %rBalance% WHERE user = 'rzwitserloot']
[UPDATE accounts SET balance = %sBalance% WHERE user = 'Speedy']
COMMIT;

看起來足夠安全吧?

不,實際上,這真的很棘手。 想象一下,就在中間,在sBalance -= 10; ,您從 ATM 上從您的帳戶中提取 50 美元(而您的帳戶開始時有 50 美元)。

你現在富了 50 美元,你的賬戶余額應該是 -10,但實際上是 40。

哎呀。

可怕。

有3種方法可以解決這個問題:

  1. 鎖定

想象一下,當我讀取該事務時,該事務鎖定了整個帳戶表。 在提交此事務之前,地球上沒有其他任何東西可以寫入此表。 這將解決問題:您的 ATM 會掛起一會兒,等待余額轉移完成,然后再做它的事情。 實際上,它甚至無法讀取。 如果你讀了,那么這個事務寫入一個新值呢? 可能會出現同樣的問題。 因此,為所有內容全局鎖定整個表。

解決了問題,但這並不能擴展。

  1. 嗯,草草了。 誰在乎?

只是,不要在意這個。 有基本的 R/W 鎖或行鎖,銀行在這里只損失 50 美元。 聽起來很瘋狂,但許多事務系統都是這樣工作的。 即壞了。

  1. 重試

魔法來了。 獲得兩全其美的一種迂回方法是重新運行所有查詢並仔細檢查結果是否相同,銀行不可能搞砸並免費給你 50 美元,同時避免鎖定地球的情況。

在這個假設場景中,交易系統的任務是意識到[SELECT balance FROM accounts WHERE user = 'Speedy']命令現在將返回與之前返回的結果不同的結果,這意味着整個交易是現在無效,需要從頂部重新運行。 這解決了問題:整個塊重新運行,意識到您現在的余額為 0,並通過拋出InsufficientBalanceException正確中止轉移資金的嘗試。 我們避免了世界鎖,但代價是一些簿記和原子的“快速檢查是否有任何查詢觸及自那以后發生的任何變化”操作。

這正是您在這里遇到的問題- 這就是 objectify 在拋出 ConcurrentModificationException 時的含義。 這是不好的 API 設計:這不是正確的例外,通常您不應該僅僅因為名稱聽起來模糊匹配而重用現有的例外。 但是,無論如何,您將不得不接受 objectify 在這方面犯了錯誤的事實。

如果您沒有從一開始就以正確的方式進行編程,那么一般修復將非常復雜,聽起來您還沒有。

看,有一個大問題:該代碼不僅僅是db/persistence 層中的原語。 db 引擎無法重播該塊 畢竟,該塊包含一堆 java 代碼!

不,代碼本身需要被告知重新開始。

那么這就更加復雜了。 計算機是非常可靠的機器。 如果 2 個單獨的進程(比如,銀行 web 接口,您向我訂購了 10 美元的資金轉賬,以及那台 ATM 機)發生沖突,並且都被迫從頭開始命令,運氣不好,兩台機器都可靠地重試和可靠地再次以對方的方式進入,再次重試,並將不斷地相互配合,總是強迫對方重試,永遠卡住。

解決方案是骰子。 不完全是。 爸爸需要一雙新的鞋子骰子。 解決方案是:如果發生沖突,請等待一段隨機的時間(但為每次發生的沖突選擇一個越來越大的潛在暫停,直到某事成功),從而確保 2 個系統最終將停止吻合。 聽起來很瘋狂,但沒有這個,你就不會閱讀這個頁面——這個算法是以太網的基本部分,它至少為 Stack Overflow 和/或你家的互聯網服務提供動力。

因此問題就變成了你不能只用一個while循環來解決這個問題。 “哎呀,需要重試”的代碼很復雜。

唯一的解決方案是閉包 與事務系統交互的所有代碼都必須放在 lambda 中,並且在修改存儲系統中數據的那些部分之外必須是冪等的(運行一次和多次運行沒有區別)。 這樣,框架本身可以捕獲重試問題,應用適當的隨機指數退避,然后重新開始。

SQL 抽象如 JDBI 做到了這一點(這是你永遠不應該為實際應用程序編寫 JDBC 的一個非常重要的原因。總是使用 JDBI 或 JOOQ 或類似的東西)。 我不知道 objectify 是否有這樣的 API。 如果沒有,你將不得不自己寫。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM