Javascript-форум (https://javascript.ru/forum/)
-   Node.JS (https://javascript.ru/forum/node-js-io-js/)
-   -   Синхронная запись в БД (https://javascript.ru/forum/node-js-io-js/73612-sinkhronnaya-zapis-v-bd.html)

stweet 28.04.2018 14:57

Синхронная запись в БД
 
День добрый форумчане! Устал бороться в одиночку с «асинхронностью» сии детища NodeJS. Прибегаю к помощи гуру Promises & Multyrequest.

Ситуация такая:
Допустим, у нас в базе (юзаю Pg + Sequalize) есть табличка с данными такого вида:

ID   TYPE    COUNTER
 1   «one»       1
 2   «two»       1


В модели есть методы decCounterByType(type); & incCounterByType(type); и вроде бы все ни чего пока не попробуешь использовать эти методы в цикле. Цикл же имитирует многочисленное обращение к базе, пример:

for(let i  = 0; i < 1000; i ++) {
	Model.incCounterByType(«one»)
		.then(res => console.log(`${i}, ${type}, ${res.counter}`))
		.catch(console.error);
}


Понимаем да, что вывод будет хаотичен и не каждое обращение получит правильный ответ, т. е. лог будет выглядеть примерно так:

0, «one», 1
3, «one», 2
1, «one», 4
2, «one», 3


Где то на просторах нашел решение, и да, оно работает, блокирует обращение:

let last = Promise.resolve({})
for(let i  = 0; i < 1000; i ++) {
	last  = last .then(() => Model.incCounterByType(«one»))
		.then(res => console.log(`${i}, ${type}, ${res.counter}`))
		.catch(console.error);
}


в этом случае у нас вывод будет уже:

0, «one», 1
1, «one», 2
2, «one», 3
3, «one», 4


и вроде бы эврика, решение есть! Но, под правильным углом со знаниями работы алгоритма Promise, мы понимаем, что если один из Promise не выполнит «resolve», или выполнит «reject» or «throw» все остальные обращения ждет фиаско. Пока остановился на таком варианте но уверен, за такой костыль будут бить ногами:

let last = Promise.resolve({})
for(let i  = 0; i < 1000; i ++) {
	last  = last .then(() => Model.incCounterByType(«one»))
		.then(res => console.log(`${i}, ${type}, ${res.counter}`))
		.catch(Promise.resolve));
}


Цель, добиться корректного обращения к БД и синхронный вывод результатов. А как вы блокируете обращения к БД?

Спасибо за участие в обсуждении!

Nexus 28.04.2018 15:03

Что вы пытаетесь вообще сделать?
Почему не увеличить сразу все значения 1 запросом?

stweet 28.04.2018 15:11

Цитата:

Сообщение от Nexus (Сообщение 484331)
Что вы пытаетесь вообще сделать?
Почему не увеличить сразу все значения 1 запросом?

Как вы себе это представляете? Зашло на сайт 100500 человек, сделали запросы а я их должен копить, и когда надумаю(видимо произвольно) сохраню, так?

Aetae 28.04.2018 15:19

Если каждый юзер посылает запрос при заходе - каждый юзер получает свой ответ. И на момент ответа - этот ответ верен. В чём тогда проблема?
Чисто чтоб "порядок был" и цифры красивенькие?

EmperioAf 28.04.2018 15:21

Async/await в помощь
async function foo() { // вернет Promise<undefined>
    for (let i = 0; i < 1000; i++) {
        try {
            const res = await Model.incCounterByType(«one»)
            console.log(`${i}, ${type}, ${res.counter}`)
        } catch(err) {
            // обработка ошибки
        }
    }
}

Nexus 28.04.2018 15:24

stweet, перечитал, прошу прощение.
Не обратил внимание, что у вас это просто имитация.

C postgresql не работал, она не блокирует строку во время её обновления?

stweet 28.04.2018 15:33

Цитата:

Сообщение от Aetae (Сообщение 484334)
Если каждый юзер посылает запрос при заходе - каждый юзер получает свой ответ. И на момент ответа - этот ответ верен. В чём тогда проблема?
Чисто чтоб "порядок был" и цифры красивенькие?

Скорее, мне важно правильное(надежное) выполнение кода. Что бы не получилось так, пока один юзер пытается поднять счетчик второй пытается его понизить и т.д. А в итоге каждый получает не разбириху, у второго счетчик уже поднялся, а у первого ещё ответ не пришел. Или это приблуды консоли и на рабочем примере все будет в порядке? Мне важно, что бы счетчик надежно работал. Если повторить туже операцию но с созданием записей, вы получите и в базе хаотичный счетчик. т.е.:
record 1 = 1
record 2 = 10
record 3 = 9
record 4 = 11
и т.д.

stweet 28.04.2018 15:34

Цитата:

Сообщение от EmperioAf (Сообщение 484335)
Async/await в помощь
async function foo() { // вернет Promise<undefined>
    for (let i = 0; i < 1000; i++) {
        try {
            const res = await Model.incCounterByType(«one»)
            console.log(`${i}, ${type}, ${res.counter}`)
        } catch(err) {
            // обработка ошибки
        }
    }
}

Вы выполните свой код и посмотрите на результат.

stweet 28.04.2018 15:39

Цитата:

Сообщение от Nexus (Сообщение 484336)
stweet, перечитал, прошу прощение.
Не обратил внимание, что у вас это просто имитация.

C postgresql не работал, она не блокирует строку во время её обновления?

По идее любая база должна блокировать обращение до его завершения. По тому и не видел проблем пока не столкнулся с выводом и/или попыткой изменить записи. Если 100500 человек одновременно попытаются внести изменения в запись, ответ будет хаотичен.

Пример такой, есть счетчик, равен "3"
Если одновременно "два" юзера жмякнут на "поднять" оба получают "4". А в базе стоит "5" - !неудачный пример!
Если два юзера одновременно зарегистрируются, счетчик идти у них будет не по порядку. - Скорее я об этом.

EmperioAf 28.04.2018 15:39

let GLOBAL_ITER = 0;

function iter() {
    return new Promise((resolve) => {
        setTimeout(() => { 
            GLOBAL_ITER++;
            resolve(GLOBAL_ITER);
        }, 1e3);
    });
}

async function test() {
    for (let i = 0; i < 5; i++) {
        try {
            const curGlobalIter = await iter();
            console.log(curGlobalIter)
        } catch(err) {
        
        }
    }
}

test().then(() => console.log('done'));

как вы можете понять, я увижу постепенно появившиеся с delay 1с
1
2
3
4
5
done

stweet 28.04.2018 15:48

Вот и я так думал, пока не попробовал это сделать используя базу данных. На примере обычного(локального) счетчика у меня тоже все чистенько.

EmperioAf 28.04.2018 15:51

Значит у вас проблема в логике на БД

async function registerUser(data) { // Promise<number>
    // ...
    // register_user - хранимка, возвращает integer id-а пользователя, вся логика работы с данными в ней описана
    const id = (await db.query('SELECT register_user as id FROM register_user(%)', [data])).id;
    // ...
    return id;
}

/* 
    если в БД все правильно сделано и id пользователя это primary key, 
    и например выдается какой-то последовательностью(например user_id_sequence), 
    то вы никогда не получите два одинаковых id-а вызвав функцию registerUser
*/

Aetae 28.04.2018 15:52

Цитата:

Сообщение от stweet (Сообщение 484340)
Если одновременно "два" юзера жмякнут на "поднять" оба получают "4". А в базе стоит "5" - !неудачный пример!

По логике - нет. Если изначально там 3, то один юзер получит 4 - другой 5. Какой - что - зависит от сетевых задержек. Понятия "одновременно" - не существует.
Вы можете искусственно накапливать обращения, приходящие по очереди с интервалом меньше n, и, как только дольше чем n не было обращений, красиво отправлять в базу одним куском и красиво же отправлять обратно всем, но зачем?

...upd
Цитата:

Сообщение от EmperioAf (Сообщение 484345)
Значит у вас проблема в логике на БД

Кстати да, такую мысль я даж не допускал.) В таком случае возможно что угодно.

...upd
Если уж так хочется красоты - можно сделать переменную someCount и при получении любого соответствующего ответа от базы проверять если та меньше возвращённого значения - устанавливать оное. И при ответе пользователю использовать уже её. Всё равно это не ничего не гарантирует, но будет немного "красивее".

Белый шум 28.04.2018 15:59

Цитата:

Сообщение от stweet
Что бы не получилось так, пока один юзер пытается поднять счетчик второй пытается его понизить и т.д.

Если вам нужен контроль счётчика пользователем, то логичнее периодически делать запросы к серверу и обновлять значение на странице. Иначе, разные пользователи всё-равно будут видеть разные цифры - по порядку вы им отдадите результат или нет.

EmperioAf 28.04.2018 16:00

В общем вопрос такой:
как выглядит ваша функция Model.incCounterByType?
Если это вызов к postgresql типа:
UPDATE counters SET counter = counter + 1 WHERE type = % RETURNING counter;
то все должно нормально работать

stweet 28.04.2018 16:08

Давайте на примере игры рассмотрим данную задачу. Допустим есть игра "однорукий бандит" (рулетка) и у нее есть общий баланс. Так вот, если 50 пользователей запустят эту игру (у каждого свой инстанц но общий баланс игры), выполняя какие то действия будут изменять состояние общего баланса игры. Меня пугает то, что в цикле разные ответы.
Т.е. пользователь "i" делает запрос на изменение общего баланса игры и получает "левый" ответ в тот момент когда должен был получить свой. Посмотрите внимательно на мною приведенный код в старте топика.

stweet 28.04.2018 16:10

Да, я рассматривал данное решение, но, если в момент хранения данных в памяти а не в базе рухнет сервер то он утащит за собой и память. А подняв будет масса вопросов у пользователей!

stweet 28.04.2018 16:12

Как раз так и выглядит. Но в старте топика я вывел вам данные консоли, и они не по порядку, это меня и пугает. В базе все вроде бы пучком но ответы на запросы разные. Т.е. запрашиваемый может получить ответ предыдущего и на оборот.

Aetae 28.04.2018 16:16

stweet, каждый получает свой ответ.
Проблема исключительно в порядке. Но порядок в случае с пользователями не важен. Проблема решена.

EmperioAf 28.04.2018 16:19

Между этим кодом никакой разницы нет. Он делает одно и то же.
let last = Promise.resolve({})
for(let i  = 0; i < 1000; i ++) {
    last  = last .then(() => Model.incCounterByType(«one»))
        .then(res => console.log(`${i}, ${type}, ${res.counter}`))
        .catch(console.error);
}

for(let i  = 0; i < 1000; i ++) {
    try {
        const res  = await Model.incCounterByType(«one»))
        console.log(`${i}, ${type}, ${res.counter}`)
    } catch(err) {
        console.error(err);
    }
}

stweet 28.04.2018 16:23

Не вполне убедительно и все же, огромное спасибо за участие.

stweet 28.04.2018 16:24

Спасибо, будем надеяться так и есть.

EmperioAf 28.04.2018 16:29

И да, если вы вдруг захотите вызвать таки все обращения к бд одновременно(то что у вас изначально делалось) то код надо привести примерно к такому виду:
const res = await Promise.all(Array(1000).fill(Model.incCounterByType(«one»)).map(promise => promise.then(res => res.counter).catch(err => { console.error(err); return -1;}))); // res: number[] - массив чисел

stweet 28.04.2018 16:55

Цитата:

Сообщение от EmperioAf (Сообщение 484356)
И да, если вы вдруг захотите вызвать таки все обращения к бд одновременно(то что у вас изначально делалось) то код надо привести примерно к такому виду:
const res = await Promise.all(Array(1000).fill(Model.incCounterByType(«one»)).map(promise => promise.then(res => res.counter).catch(err => { console.error(err); return -1;}))); // res: number[] - массив чисел

Нет нет, выше я описал идею. Есть несколько игр с общим балансом и целью задался железно изменять баланс любой игрой для всех. Т.е. 100500 игр должны быть уверенны, что у них из бд самый актуальный баланс. Меня смутило, что в консоли каждая игра получает произвольный баланс хотя в БД все в порядке.


Часовой пояс GMT +3, время: 03:42.