Дублирование записей в БД (cs 1.6)

Сообщения
73
Реакции
8
Всем привет.

На сервере использую некую систему монеток, которые сохраняются в бд по steamid.

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

Сама проблема: в бд иногда появляются записи игроков со steamid, которые уже есть в бд.
То есть, запись в бд имеется, а проверка "не проходит что ли"...
Может с кодом что-то не так ?

Вопрос: почему такое может быть ?

ps код урезан
Код:
#include <amxmodx>
#include <sqlx>

enum _:ques { INSTALL_PLUGIN, LOAD_COINS, ADD_COINS, UPD_COINS, PRUNED };

new Handle:g_sql_tuple, szQuery[512], data[3];

new bool:g_InBase[33], coins[33];

public PluginCfg()
{
    g_sql_tuple = SQL_MakeDbTuple(szHost, szUser, szPasswd, szDb);
    
    data[0] = INSTALL_PLUGIN;
    formatex(szQuery, charsmax(szQuery), "CREATE TABLE IF NOT EXISTS `Coins_BD` (`coins` int(10) NOT NULL, `SteamID` varchar(25) NOT NULL, `Last_Connection` int(11) NOT NULL) ENGINE=MyISAM DEFAULT CHARSET=utf8;");
    SQL_ThreadQuery(g_sql_tuple, "SQL_Handler", szQuery, data, sizeof(data));
    
    data[0] = PRUNED;
    formatex(szQuery, charsmax(szQuery), "DELETE FROM `Coins_BD` WHERE `Last_Connection` < (UNIX_TIMESTAMP(NOW()) - %d)", (30 * 86400));
    SQL_ThreadQuery(g_sql_tuple, "SQL_Handler", szQuery, data, sizeof(data));
}

public client_putinserver(id)
{
    data[0] = LOAD_COINS;
    data[1] = id;
    
    new szAuthid[25]; get_user_authid(id, szAuthid, charsmax(szAuthid));
    
    formatex(szQuery, charsmax(szQuery), "SELECT * FROM `Coins_BD` WHERE `SteamID` = '%s'", szAuthid);
    SQL_ThreadQuery(g_sql_tuple, "SQL_Handler", szQuery, data, sizeof(data));
}

public client_disconnected(id)
{
    new szAuthid[25]; get_user_authid(id, szAuthid, charsmax(szAuthid));
    
    data[1] = id;
    
    switch(g_InBase[id])
    {
        case false:
        {
            data[0] = ADD_COINS;
            formatex(szQuery, charsmax(szQuery), "INSERT INTO `Coins_BD` (coins, SteamID, Last_Connection) VALUES ('%d', '%s', UNIX_TIMESTAMP(NOW()))", coins[id], szAuthid);
        }
        case true:
        {
            data[0] = UPD_COINS;
            formatex(szQuery, charsmax(szQuery), "UPDATE `Coins_BD` SET `coins` = '%d', `Last_Connection` = UNIX_TIMESTAMP(NOW()) WHERE `SteamID` = '%s'", coins[id], szAuthid);
        }
    }
    SQL_ThreadQuery(g_sql_tuple, "SQL_Handler", szQuery, data, sizeof(data));
}

public SQL_Handler(failstate, Handle:query, err[], errcode, data[], datasize)
{
    if(failstate == TQUERY_SUCCESS)
    {
        switch(data[0])
        {
            case LOAD_COINS:
            {
                new id = data[1];
                
                if(SQL_NumResults(query))
                {
                    g_InBase[id] = true;
                    //code
                }
                else
                {
                    g_InBase[id] = false;
                    //code
                }
            }
            case PRUNED:
            {
                SQL_AffectedRows(query);
            }
            case INSTALL_PLUGIN, ADD_COINS, UPD_COINS: { }
        }
    }
    else    log_amx("[State #%d] Error [#%d] %s", data[0], errcode, err);
    return PLUGIN_CONTINUE;
}

public plugin_end()
    SQL_FreeHandle(g_sql_tuple);
 
Сообщения
145
Реакции
276
Помог
1 раз(а)
Любой отложенный запрос, типа SQL_ThreadQuery, должен железобитонно вызваться на правильную запись. Правильная запись - это UserID + ID + is_user_connected

В вашем коде получается так что:

1. Заходит игрок и отправляет SQL_ThreadQuery
2. Т.к. массив g_InBase[id] может быть в значении false для данного игрока, а кэлбек от SQL_ThreadQuery ещё ничего не вернул, то при дисконнекте отрабатывает INSERT INTO. И вот тебе свежая запись.

Почему нельзя вызвать INSERT внутри SQL_ThreadQuery при Путинсервере и решить все проблемы, для меня загадка.
С какой стати автор кода решил, что кэлбек SQL_Handler вызванный при Путинсервере, должен вызываться до Дисконнекта, для меня тоже остается загадкой.

Т.к. нет нормального кода для идентификации конкретного тела и его записи (ибо всё держится на ID игрока), то потенциально тут могут записаться данные от одного игрока, другому.

g_InBase[id]
поставь в значение -1 в путинсервере.

SQL_ThreadQuery(g_sql_tuple, "SQL_Handler", szQuery, data, sizeof(data));
перенеси внутрь обоих кейсов.

Конечно это крайне кривой костыль, как и весь код в целом, но должно сработать.
 
Сообщения
73
Реакции
8
SISA,
2. Т.к. массив g_InBase[id] может быть в значении false для данного игрока, а кэлбек от SQL_ThreadQuery ещё ничего не вернул, то при дисконнекте отрабатывает INSERT INTO. И вот тебе свежая запись.
Для такой ситуации я создавал еще одну булевую, значение которой становилось true после загрузки игрока из бд, а дисконнект прирывался, если булевая = false.

поставь в значение -1 в путинсервере.
Можно этим же заменить 2ю булевую, но проблема остается не решенной.
 
Сообщения
145
Реакции
276
Помог
1 раз(а)
Если вы сделаете так, как написал я, при текущем коде, то проблема дублирующих записей при дисконнекте будет решена, т.к. без ответа от SQL_ThreadQuery, в дисконнекте будет g_InBase[id] == -1 и кейсы не отработают.
 
Сообщения
73
Реакции
8
SISA, почему мой вариант не сработал?
Пока БД не ответит (не загрузится информация из Бд) - то и код в дисконнекте не работал у меня.

При этом все равно очень редко появлялись записи в бд с одинаковым steamid
22 Июн 2019
SISA, пример
Код:
#include <amxmodx>
#include <sqlx>

enum _:ques { INSTALL_PLUGIN, LOAD_COINS, ADD_COINS, UPD_COINS, PRUNED };

new Handle:g_sql_tuple, szQuery[512], data[3];

new bool:g_InBase[33], coins[33];

new bool:zagruzka[33];

public PluginCfg()
{
    g_sql_tuple = SQL_MakeDbTuple(szHost, szUser, szPasswd, szDb);
    
    data[0] = INSTALL_PLUGIN;
    formatex(szQuery, charsmax(szQuery), "CREATE TABLE IF NOT EXISTS `Coins_BD` (`coins` int(10) NOT NULL, `SteamID` varchar(25) NOT NULL, `Last_Connection` int(11) NOT NULL) ENGINE=MyISAM DEFAULT CHARSET=utf8;");
    SQL_ThreadQuery(g_sql_tuple, "SQL_Handler", szQuery, data, sizeof(data));
    
    data[0] = PRUNED;
    formatex(szQuery, charsmax(szQuery), "DELETE FROM `Coins_BD` WHERE `Last_Connection` < (UNIX_TIMESTAMP(NOW()) - %d)", (30 * 86400));
    SQL_ThreadQuery(g_sql_tuple, "SQL_Handler", szQuery, data, sizeof(data));
}

public client_putinserver(id)
{
    data[0] = LOAD_COINS;
    data[1] = id;
    
    zagruzka[id] = false;
    
    new szAuthid[25]; get_user_authid(id, szAuthid, charsmax(szAuthid));
    
    formatex(szQuery, charsmax(szQuery), "SELECT * FROM `Coins_BD` WHERE `SteamID` = '%s'", szAuthid);
    SQL_ThreadQuery(g_sql_tuple, "SQL_Handler", szQuery, data, sizeof(data));
}

public client_disconnected(id)
{
    if(!zagruzka[id]) return;
    
    new szAuthid[25]; get_user_authid(id, szAuthid, charsmax(szAuthid));
    
    data[1] = id;
    
    switch(g_InBase[id])
    {
        case false:
        {
            data[0] = ADD_COINS;
            formatex(szQuery, charsmax(szQuery), "INSERT INTO `Coins_BD` (coins, SteamID, Last_Connection) VALUES ('%d', '%s', UNIX_TIMESTAMP(NOW()))", coins[id], szAuthid);
        }
        case true:
        {
            data[0] = UPD_COINS;
            formatex(szQuery, charsmax(szQuery), "UPDATE `Coins_BD` SET `coins` = '%d', `Last_Connection` = UNIX_TIMESTAMP(NOW()) WHERE `SteamID` = '%s'", coins[id], szAuthid);
        }
    }
    SQL_ThreadQuery(g_sql_tuple, "SQL_Handler", szQuery, data, sizeof(data));
}

public SQL_Handler(failstate, Handle:query, err[], errcode, data[], datasize)
{
    if(failstate == TQUERY_SUCCESS)
    {
        switch(data[0])
        {
            case LOAD_COINS:
            {
                new id = data[1];
                
                if(SQL_NumResults(query))
                {
                    g_InBase[id] = true;
                    //code
                }
                else
                {
                    g_InBase[id] = false;
                    //code
                }
                
                zagruzka[id] = true;
            }
            case PRUNED:
            {
                SQL_AffectedRows(query);
            }
            case INSTALL_PLUGIN, ADD_COINS, UPD_COINS: { }
        }
    }
    else    log_amx("[State #%d] Error [#%d] %s", data[0], errcode, err);
    return PLUGIN_CONTINUE;
}

public plugin_end()
    SQL_FreeHandle(g_sql_tuple);
 
Сообщения
147
Реакции
29
Im sorry if i missunderstood your poblem, but if you get duplicated rows with players (steamid) then create new table with UNIQUE or PRIMARY KEY for steamid ( varchar )...or even new column like player_id with primarykey+int (and then use like g_iPlayerIndex[ id ] with his loaded (SELECT ...... where steamid="his_steamid") and ect)...i hope thats what you are looking for
 
Сообщения
145
Реакции
276
Помог
1 раз(а)
Не видя код, мне трудно судить, почему не сработал ваш вариант, но как минимум, там есть ещё одна потенциальная проблема из-за отсутствия нормальной проверки игрока.

Произойти это может так:

Заходит игрок Вася. Отсылается SQL_ThreadQuery.
Вася выходит, при этом значение у него может всё ещё остаться g_InBase[id] -1, т.к. ответ в кэлбеке SQL_ThreadQuery до сих пор не получен.

В этот момент заходит Игрок Петя на тот же ID. Отправляется ещё один запрос (а запрос от Васи ещё не выполнился).
Запрос от Васи наконец-то выполняется, Игрок Петя выходит с g_InBase[id] = false
и благополучно отправляется INSERT в базу.
После этого уже приходит кэлбек от запроса Пети, что уже роли не играет по факту.
 
Сообщения
73
Реакции
8
SISA, код выше.
А по поводу нормальной проверки - что именно нужно сделать ?
 
Сообщения
145
Реакции
276
Помог
1 раз(а)
SISA, код выше.
А по поводу нормальной проверки - что именно нужно сделать ?
Как минимум, надо передавать больше данных, чтобы отличить одного игрока от другого. Вы же, передаете только ID, а на этот ID может зайти кто угодно, пока будет идти ответ от мьюскула.

Создаете массив на 33 ячейки (допустим g_UserAuthorisation[33]).

Когда игрок заходит на сервер, ставите ему значение -1. Это будет означать, что игрок с данным ID не авторизован и выполнять какие-либо запросы MySQL для него с минусовым значением мы не можем, ни где.

Создаете enum для формирования массива кэлбека SQL_ThreadQuery (а не тот трындец с data[0] и data[1])

Создаете массив для этого enum и записываете в ячейки:
ID игрока
USERID игрока get_user_userid(id)
Тип запроса (назовем его QUERYMODE_PLAYER_AUTHOR)

Дальше идете в кэлбек SQL_ThreadQuery. Там.

Проверяете запрос на возможные ошибки.
Блокируете любой выполняемый код, если отработал plugin_end
Извлекаете из массива Кэлбека ID и USERID передаваемого игрока
Делаете свич по типам запрос и в первый кейс ставите QUERYMODE_PLAYER_AUTHOR
Дальше делаете проверку на основе извлеченных данных из массива:

if(is_user_connected(ID игрока) && get_user_userid(ID игрока) == USERID)
{
тут выполняем код работы с ответом мьюскула
}

Так вы с гарантией 100% выполните код на того игрока, который был в Путинсервере.

Если вы получили SQL_NumResults > 0, значит запись уже есть.

Я рекомендую сразу взять ID ячейки полученной записи из базы данных "SQL_ReadResult(Query, SQL_FieldNameToNum(Query, "id"))"
и назначить её на g_UserAuthorisation[id] или выставить значение 1, что будет свидетельствовать об окончании процесса авторизации.

Если у нас свежее тело, без записи в бд (SQL_NumResults == 0), мы сразу же, внутри кэлбека SQL_ThreadQuery отсылаем INSERT запрос на его добавление и g_UserAuthorisation[id] мы продолжаем держать в значении -1, т.к. фактически игрок ещё не авторизован.

Соответственно в массиве кэлбека меняем тип запроса, назовем его QUERYMODE_PLAYER_ADD и вызываем SQL_ThreadQuery

В кэлбеке добавляем ещё один кейс QUERYMODE_PLAYER_ADD и делаем такую проверку:

if(g_UserAuthorisation[ID игрока] == -1 && is_user_connected(ID игрока) && get_user_userid(ID игрока) == USERID)
{
тут выполняем код работы с ответом мьюскула
}

соответственно получаем ID добавленной ячейки SQL_GetInsertId(Query) и присваиваем его к g_UserAuthorisation[id] или ставим значение 1

Вот теперь всё, игрок на 100% авторизован и если у него g_UserAuthorisation[id] != -1, то мы можем смело вызывать UPDATE в дисконнекте, используя g_UserAuthorisation[id] как номер ячейки. Впрочем, можно и STEAMID залупить, но с ячейкой будет меньше нагрузки на мьюскул. В дисконнекте g_UserAuthorisation можно сбросить на -1, на всякий случай.

Обмануть такую систему авторизации игрока и потенциально дублировать записи, физически, невозможно.
 
Последнее редактирование:
Сообщения
333
Реакции
290
Помог
9 раз(а)
Лишь бы не утруждать себя в переводе? Вам дали действительно годную тему
create new table with UNIQUE or PRIMARY KEY for steamid
Делаете столбец steamid уникальным, отсылаете INSERT... ON DUPLICATE KEY UPDATE. Нет проблем с дублированием и половину вашего запутанного кода с булевыми и прочей ненужной инфой и все предыдущие посты темы можно смело удалять :smile3:
 
Сообщения
147
Реакции
29
Ne razbiram sve ?
i also learned that wheh you work with sql databases, its better to have minimum 0.1sec set task on plugin init, to avoid poss problems....good luck ?
 
Сообщения
957
Реакции
1,185
Помог
52 раз(а)
Лишь бы не утруждать себя в переводе? Вам дали действительно годную тему

Делаете столбец steamid уникальным, отсылаете INSERT... ON DUPLICATE KEY UPDATE. Нет проблем с дублированием и половину вашего запутанного кода с булевыми и прочей ненужной инфой и все предыдущие посты темы можно смело удалять :smile3:
Полностью солидарен с данным постом. To_be_or_not_to_be прислушайтесь и не городите огород из запросов
 
Сообщения
145
Реакции
276
Помог
1 раз(а)
zhorzh78,

На сколько я помню, стороковой параметр в виде уникального ключа, снижает производительность работы мьюскула в данной таблице, а если вы захотите выставить какие-то данные в ячейки новому игроку и не выставлять уже существующим, то понадобится 2 отдельных запроса.
 
Последнее редактирование:
Сообщения
333
Реакции
290
Помог
9 раз(а)
SISA, Не понимаю, о каких 2-х запросах идет речь и не представляю существование базы данных (реальной базы данных, а не пары столбцов для хранения циферок), где бы не использовался уникальный "строковый" индекс. Например, в таблицах данного форума. А, т.к. для выполнения задач необходим именной такой уникальный индекс, то разговоры о скорости не имеют никакого значения.
 
Сообщения
145
Реакции
276
Помог
1 раз(а)
zhorzh78,

о каких 2-х запросах идет речь
В том случае, если игроку на новую запись надо назначить особые данные при её добавлении, при этом на существующие записи, эти данные назначать нельзя.

где бы не использовался уникальный "строковый" индекс
Я не говорил про "Не использование". Я сказал, что при использовании строки в качестве уникального ключа, по идее снижается скорость работы с таблицей, по этому, как правило, используют числовой ID в виде ключа и при таком раскладе ON DUPLICATE KEY UPDATE не прокатит.
 
Последнее редактирование:
Сообщения
957
Реакции
1,185
Помог
52 раз(а)
Я не говорил про "Не использование". Я сказал, что при использовании строки в качестве уникального ключа, по идее снижается скорость работы с таблицей, по этому, как правило, используют числовой ID в виде ключа и при таком раскладе ON DUPLICATE KEY UPDATE не прокатит.
Таблица без индесов работает медленее, чем при наличии оных. Курите статьи - для чего собственно были введены индексы в СУБД.
 
Сообщения
145
Реакции
276
Помог
1 раз(а)
Sonyx, где я писал про таблицу без индексов ?
 
Сообщения
73
Реакции
8
zhorzh78, булевая мне все же нужна (если я ничего не путаю), чтобы исключить сохранение при дисконнекте еще до загрузки информации из бд.
У меня пока что такой код получился. Посмотрите, пожалуйста, вдруг что-то не так ? (речь о дублировании)
Код:
#include <amxmodx>
#include <sqlx>

enum _:ques { INSTALL_PLUGIN, LOAD_COINS, PRUNED };
new Handle:g_sql_tuple, szQuery[512], data[3];

new bool:g_UserLoad[33], coins[33];

public PluginCfg()
{
    g_sql_tuple = SQL_MakeDbTuple(szHost, szUser, szPasswd, szDb);
    
    data[0] = INSTALL_PLUGIN;
    formatex(szQuery, charsmax(szQuery), "CREATE TABLE IF NOT EXISTS `Coins_BD` (`coins` int(10) NOT NULL, `SteamID` varchar(25) NOT NULL, `Last_Connection` int(11) NOT NULL) ENGINE=MyISAM DEFAULT CHARSET=utf8;");
    SQL_ThreadQuery(g_sql_tuple, "SQL_Handler", szQuery, data, sizeof(data));
    
    data[0] = PRUNED;
    formatex(szQuery, charsmax(szQuery), "DELETE FROM `Coins_BD` WHERE `Last_Connection` < (UNIX_TIMESTAMP(NOW()) - %d)", (30 * 86400));
    SQL_ThreadQuery(g_sql_tuple, "SQL_Handler", szQuery, data, sizeof(data));
}

public client_putinserver(id)
{
    g_UserLoad[id] = false;
    
    new szAuthid[25]; get_user_authid(id, szAuthid, charsmax(szAuthid));
    
    data[0] = LOAD_COINS;
    data[1] = id;
    
    formatex(szQuery, charsmax(szQuery), "SELECT * FROM `Coins_BD` WHERE `SteamID` = '%s'", szAuthid);
    SQL_ThreadQuery(g_sql_tuple, "SQL_Handler", szQuery, data, sizeof(data));
}

public client_disconnected(id)
{
    if(!g_UserLoad[id]) return;
    
    new szAuthid[25]; get_user_authid(id, szAuthid, charsmax(szAuthid));
    
    formatex(szQuery, charsmax(szQuery), "INSERT INTO `Coins_BD` (coins, SteamID, Last_Connection) VALUES ('%d', '%s', UNIX_TIMESTAMP(NOW())) ON DUPLICATE KEY UPDATE `coins` = '%d', `Last_Connection` = UNIX_TIMESTAMP(NOW())", coins[id], szAuthid, coins[id]);
    SQL_ThreadQuery(g_sql_tuple, "SQL_HandlerInsert", szQuery);
}

public SQL_HandlerUpdate(failstate, Handle:query, err[], errcode)
{
    if(failstate != TQUERY_SUCCESS)
        log_amx("[State #%d] Error [#%d] %s", data[0], errcode, err);
        
    return PLUGIN_CONTINUE;
}

public SQL_Handler(failstate, Handle:query, err[], errcode, data[], datasize)
{
    if(failstate == TQUERY_SUCCESS)
    {
        switch(data[0])
        {
            case LOAD_COINS:
            {
                new id = data[1];
                
                if(SQL_NumResults(query))
                {
                    //code
                }
                else
                {
                    //code
                }
                
                g_UserLoad[id] = true;
            }
            case PRUNED:
            {
                SQL_AffectedRows(query);
            }
            case INSTALL_PLUGIN: { }
        }
    }
    else log_amx("[State #%d] Error [#%d] %s", data[0], errcode, err);
    return PLUGIN_CONTINUE;
}

public plugin_end()
    SQL_FreeHandle(g_sql_tuple);
 

Пользователи, просматривающие эту тему

Сейчас на форуме нет ни одного пользователя.
Сверху Снизу