В следующих примерах я приведу фиктивный код, поскольку не хочу показывать слишком много ненужных деталей.
Итак, у меня есть таблица базы данных, например:
Schema::create('settings', function (Blueprint $table) {
$table->increments('id');
$table->json('settings');
$table->timestamps();
});
Поскольку settings
- это JSON, я хотел бы иметь возможность частично обновить его с помощью моего REST-контроллера, например, /api/settings/setting_group.sub_setting?v=new value
. Вышеприведенное будет видоизменяться примерно так: $setting->settings["sub_setting"] = $new_value;
Я использую это для хранения своих переводов, поэтому отправлю их одновременно, когда клиент браузера что-то изменил.en.links
- это просто путь в settings
JSON.
/api/settings/en.links
/api/settings/zh-HK.links
Эта проблема не возникала, когдаv
- простая строка, я предполагаю, что это потому, что Laravel работает быстрее, если v
не является большим массивом PHP, поэтому быстрая синхронизация уменьшила проблему.Но состояние гонки произошло, когда v
было большим.
Это данные формы, которые у меня были, когда это произошло.Для удобства чтения это без кодировки URL.
/api/settings/en.links?
v[0][text]=en link a&
v[0][url]=http://link.a&
v[1][text]=en link b&
v[1][url]=http://link.b&
api_token=xxx
/api/settings/zh-HK.links?
same thing but in zh-HK translation
Это способ обработки ввода в моем контроллере:
public function update(Request $request, $setting)
{
$user = Auth::user();
$sectioned_setting = Setting::findOrFail($user->section_id);
// ...validator logic...
$form_v = $request->input("v")?:"";
// This line duplicates $setting->settings then make the changes in that clone
// and set the clone back into the Setting model
$v = $sectioned_setting->setValue($setting, $form_v);
$sectioned_setting->save();
return response()->json([
"v" => $sectioned_setting->settings // This line used to just return $v
], Response::HTTP_ACCEPTED);
}
Вот в основном то, что я получил в Chrome NetworkНа вкладке, если я обновил оба текста ссылки в en и zh-HK одновременно:
/api/settings/en.links
{
en: {
links: [
{
text: "xxxUPDATED",
url: "xxx"
},
]
},
zh-HK: {
links: [
{
text: "xxx",
url: "xxx"
},
]
}
}
/api/settings/zh-HK.links
{
en: {
links: [
{
text: "xxx",
url: "xxx"
},
]
},
zh-HK: {
links: [
{
text: "xxxUPDATED",
url: "xxx"
},
]
}
}
Как видите, мутация zh-HK в поле модели перезаписала переменную en.Так что это должно быть проблемой гонки.Мне нужно, чтобы весь процесс обновления и сохранения был атомарной операцией.
Есть ли способ решить эту проблему?Как можно защитить сервер?
РЕДАКТИРОВАТЬ:
После прочтения я попробовал пессимистическую блокировку для выбора и обновления, но она также не работала.(Это было сделано для части моих тестов. Поэтому я считаю, что это не работает, или это сработало для немногих случайно. Хотя я не знаю, почему несколько раз это сработало.)
Я заменил:
$sectioned_setting = Setting::findOrFail($user->section_id);
до
$sectioned_setting = Setting::whereId($user->section_id)->limit(1)->lockForUpdate()->first();
Возможно, я думаю, потому что запросы используют разные соединения с базой данных, поэтому блокировки не сохраняются.
Я предполагаю, что возможноможно добавить столбец version
в таблицу и вручную выполнить атомарную операцию для этого.(Поэтому он проверяет прямо перед сохранением, если обновленная модель имеет ту же версию, повторите операцию, если она отличается.) Но Я хотел бы знать, есть ли способ в Laravel сделать это.