天天看點

Restful API實作樂觀鎖,應傳回409還是412?

近些年Restful API變得很流行,一個重要的原因是其充分利用HTTP協定标準,這樣API Consumer消費Restful API的成本就小很多,API開發人員也更加有據可循。

Restful API複用HTTP協定的方法和狀态碼來指代不同的行為,比如POST代表建立一條資源,建立成功用201表示,請求校驗失敗用400表示;GET代表擷取一條或多條資源,擷取成功用200表示;DELETE代表删除一條資源,删除成功用204表示,删除記錄不存在用404表示。

個人覺得,Restful API相比傳統RPC(比如:SOAP)的差別,就可類比于面向對象程式設計和面向過程程式設計。

這裡讨論一個問題,對于Restful API的PUT操作,在并發環境下,兩個Request更新同一條資源,可能會出現更新内容丢失的情況。針對這個問題,通常可以“加鎖”來解決,那麼加什麼鎖呢?一般來說就是:樂觀鎖或者悲觀鎖。

假設PUT請求的處理邏輯是先校驗資源存不存在;然後存在的話更新資源到資料庫。這個邏輯對應就是下面兩條sql語句:

select * from product where productid = 110;
update product set stock = 49 where productid = 110;
           

如果是加悲觀鎖的話,就是在執行第一條select語句時加一個排他鎖(select for update),在update語句執行完了才釋放鎖,這樣兩個PUT請求隻能一個先執行一個後執行,就不會出現更新内容丢失的情況。

使用悲觀鎖,由于在相應記錄上加了排他鎖,并且鎖的範圍相對較大,會對讀操作産生一定影響;其次,如果索引建得不合适,容易導緻鎖住整個表,進而影響系統吞吐量。

悲觀鎖有很多應用場景,之前我寫過一篇文章(liquibase和flyway中分布式鎖實作的差別?)介紹liquibase和flyway,其中flyway就是利用悲觀鎖實作了分布式鎖。

和悲觀鎖不同,使用樂觀鎖,其實并不是在相應的記錄或者表上加鎖,而是在update的時候加一個version的比對。

回到上面例子,select語句查詢到對應product的version,然後把version添加到update語句比對條件,如下:

update product set stock = 49 where productid = 110 and version = 5;           

如果目前資料庫中記錄的version是5,則update語句執行成功,version增加;如果目前資料庫中記錄version不是5,則update語句執行失敗,傳回相應狀态碼提示使用者請求執行失敗。

可以看到,樂觀鎖并沒有添加額外的鎖,是以在某些情況下,性能會好過悲觀鎖;但是,在高并發頻繁更新的情況下,可能會導緻很多請求失敗,對使用者體驗很不好,使用者需要重試很多次。

上面提到update執行失敗,傳回相應狀态碼提示使用者請求執行失敗,那麼對于Restful API,應該傳回什麼狀态碼呢?

根據HTTP規範,有兩個狀态碼可以使用:409和412。

從409的規範可以看出,當某一個資源的state發生了變化,導緻request不能完成,可以傳回409,提示使用者解決沖突,重新送出請求。

The request could not be completed due to a conflict with the current state of the resource. This code is only allowed in situations where it is expected that the user might be able to resolve the conflict and resubmit the request.

https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.10

從412的規範可以看出,412是前置條件不符合,并且前置條件須是位于HTTP的header。

References

  • https://stackoverflow.com/questions/3620203/http-status-412-precondition-failed-and-database-versioning?rq=1