Дисклеймер. Сказанное ниже является личным мнением автора. Сам по себе автор является мастером спорта международного класса по продавливанию дивана и ничего полезного в своей жизни он, разумеется, не сделал. Урок не претендует на звание истины в последней инстанции, а автор готов признавать свои ошибки и публично приносить извинения, если заденет чьи-то чувства прекрасного своей графоманией с шизофреническими наклонностями.
В рамках данного FAQ я рассмотрю примеры, взятые из мода самая последняя сборка RedAge (от Golem) от 19.08.21 и на конкретных участках кода покажу места, где есть определённые проблемы, а также предложу своё решение.
1. Выполнение запросов синхронно. Вероятно, это решение планировалось использовать временно в целях упрощения разработки. Но, как мы все знаем, нет ничего более вечного, чем временное. В результате это решение осталось в сливе оригинального мода и, смею предположить, с того момента этот код не модифицировался.
Проблема: блокировка потока исполнения (простыми словами код не исполняется, пока база данных не обработает запрос), падение общей производительности как следствие.
Решение: использование асинхронных запросов. В моде уже реализовано всё для асинхронных запросов, необходимо лишь переписать часто вызываемые методы, которые используют MySQL.QueryRead с использованием MySQL.QueryReadAsync.
Пример:
Также рекомендую прочитать дополнения от @Serafim:
Дополнение 1
Дополнение 2
2. Создание нового подключения для каждого запроса. В целом, это не такая плохая практика, учитывая использование disposable. Но есть одно "но": при массовых запросах в базу данных (например, при сохранении игроков, домов или бизнесов) происходит большой оверхед при создании новых соединений. При игре на сервере с онлайном 1 вы вряд ли это заметите, но при онлайне 500 человек и больше это может стать серьёзной проблемой, так как помимо создания множества соединений необходимо будет обрабатывать данные от других игроков.
Проблема: блокировка потока исполнения, создание и закрытие соединений, когда можно использовать только лишь одно.
Решение: множество различных вариантов. Один из самых очевидных и простых для реализации - создание метода, который выполнит все запросы в рамках одного соединения. Я бы хотел рассмотреть ещё вариант использования транзакций, но, смею предположить, в данном случае он только лишь нанесёт ущерб производительности.
P. S. Пока я подготавливал пример заметил также проблему с реализацией сохранения бизнесов: сохранение происходит синхронно, что влечёт за собой заморозку потока. Учитывая, что сохранение вызывается только лишь при выключении сервера, это не такой серьёзный удар по производительности, но при массовом сохранении во время работы (а это именно то, что стоит делать) возникнет лаг.
P. P. S. Пример предложенного мною метода не является превосходной реализацией, но это лучше, чем было. В связи с тем, что я не хочу усложнять FAQ, я предложил именно это решение. Я ставлю перед собой задачу дать толчок в сторону хорошей (или хотя бы неплохой) реализации, а вы, при должном желании, можете сделать ещё лучше.
3. Допуск неэкранированных строк к запросу. Последствия очевидны: SQL-инъекции. Я не буду останавливаться на данной проблеме, так как в интернете есть исчерпывающая информация по эксплуатации данной уязвимости и защите от неё.
Проблема: допуск произвольных строк к исполнению при запросе влечёт за собой возможность исполнения произвольного запроса в базу данных.
Решение: экранирование строк.
Пример:
В моде есть ещё и другие проблемы с базой данных, но в рамках данного FAQ я не стал рассматривать все мелочи. Данные мною решения позволят оптимизировать самые критичные для производительности задачи, а последнее немного улучшит безопасность. Буду рад видеть ваши комментарии и критику. Если FAQ получит положительный отклик, то я с радостью выпущу ещё одну статью с критикой безопасности данного мода.
Другие статьи:
Как не стоит делать: безопасность ивентов и клиентских скриптов
[JS] Как подключить отладчик (Debugger) в PhpStorm для server-side
В рамках данного FAQ я рассмотрю примеры, взятые из мода самая последняя сборка RedAge (от Golem) от 19.08.21 и на конкретных участках кода покажу места, где есть определённые проблемы, а также предложу своё решение.
1. Выполнение запросов синхронно. Вероятно, это решение планировалось использовать временно в целях упрощения разработки. Но, как мы все знаем, нет ничего более вечного, чем временное. В результате это решение осталось в сливе оригинального мода и, смею предположить, с того момента этот код не модифицировался.
Проблема: блокировка потока исполнения (простыми словами код не исполняется, пока база данных не обработает запрос), падение общей производительности как следствие.
Решение: использование асинхронных запросов. В моде уже реализовано всё для асинхронных запросов, необходимо лишь переписать часто вызываемые методы, которые используют MySQL.QueryRead с использованием MySQL.QueryReadAsync.
Пример:
C#:
[Command("offunwarn")] // Снять варн у игрока в оффлайне (3 лвл)
public static void CMD_offunwarn(Player player, string target)
{
try
{
if (!Main.Players.ContainsKey(player)) return;
if (!Group.CanUseCmd(player, "offunwarn")) return;
if (!Main.PlayerNames.ContainsValue(target))
{
Notify.Send(player, NotifyType.Error, NotifyPosition.BottomCenter, $"Игрок не найден", 3000);
return;
}
if (NAPI.Player.GetPlayerFromName(target) != null)
{
Notify.Send(player, NotifyType.Error, NotifyPosition.BottomCenter, $"Игрок онлайн", 3000);
return;
}
var split = target.Split('_');
var data = MySQL.QueryRead($"SELECT warns FROM characters WHERE firstname='{split[0]}' AND lastname='{split[1]}'");
var warns = 0;
foreach (System.Data.DataRow Row in data.Rows)
{
warns = Convert.ToInt32(Row["warns"]);
}
if (warns <= 0)
{
Notify.Send(player, NotifyType.Error, NotifyPosition.BottomCenter, $"У игрока нет варнов", 3000);
return;
}
warns--;
GameLog.Admin($"{player.Name}", $"offUnwarn", $"{target}");
MySQL.Query($"UPDATE characters SET warns={warns} WHERE firstname='{split[0]}' AND lastname='{split[1]}'");
Notify.Send(player, NotifyType.Success, NotifyPosition.BottomCenter, $"Вы сняли варн у игрока {target}, у него {warns} варнов", 3000);
}
catch (Exception e) { Log.Write("offunwarn: " + e.Message, nLog.Type.Error); }
}
C#:
[Command("offunwarn")] // Снять варн у игрока в оффлайне (3 лвл)
public static async void CMD_offunwarn(Player player, string target)
{
try
{
if (!Main.Players.ContainsKey(player)) return;
if (!Group.CanUseCmd(player, "offunwarn")) return;
if (!Main.PlayerNames.ContainsValue(target))
{
Notify.Send(player, NotifyType.Error, NotifyPosition.BottomCenter, $"Игрок не найден", 3000);
return;
}
if (NAPI.Player.GetPlayerFromName(target) != null)
{
Notify.Send(player, NotifyType.Error, NotifyPosition.BottomCenter, $"Игрок онлайн", 3000);
return;
}
var split = target.Split('_');
var data = await MySQL.QueryReadAsync($"SELECT warns FROM characters WHERE firstname='{split[0]}' AND lastname='{split[1]}'");
var warns = 0;
foreach (System.Data.DataRow Row in data.Rows)
{
warns = Convert.ToInt32(Row["warns"]);
}
if (warns <= 0)
{
Notify.Send(player, NotifyType.Error, NotifyPosition.BottomCenter, $"У игрока нет варнов", 3000);
return;
}
warns--;
GameLog.Admin($"{player.Name}", $"offUnwarn", $"{target}");
MySQL.QueryAsync($"UPDATE characters SET warns={warns} WHERE firstname='{split[0]}' AND lastname='{split[1]}'");
Notify.Send(player, NotifyType.Success, NotifyPosition.BottomCenter, $"Вы сняли варн у игрока {target}, у него {warns} варнов", 3000);
}
catch (Exception e) { Log.Write("offunwarn: " + e.Message, nLog.Type.Error); }
}
Дополнение 1
Дополнение 2
2. Создание нового подключения для каждого запроса. В целом, это не такая плохая практика, учитывая использование disposable. Но есть одно "но": при массовых запросах в базу данных (например, при сохранении игроков, домов или бизнесов) происходит большой оверхед при создании новых соединений. При игре на сервере с онлайном 1 вы вряд ли это заметите, но при онлайне 500 человек и больше это может стать серьёзной проблемой, так как помимо создания множества соединений необходимо будет обрабатывать данные от других игроков.
Проблема: блокировка потока исполнения, создание и закрытие соединений, когда можно использовать только лишь одно.
Решение: множество различных вариантов. Один из самых очевидных и простых для реализации - создание метода, который выполнит все запросы в рамках одного соединения. Я бы хотел рассмотреть ещё вариант использования транзакций, но, смею предположить, в данном случае он только лишь нанесёт ущерб производительности.
P. S. Пока я подготавливал пример заметил также проблему с реализацией сохранения бизнесов: сохранение происходит синхронно, что влечёт за собой заморозку потока. Учитывая, что сохранение вызывается только лишь при выключении сервера, это не такой серьёзный удар по производительности, но при массовом сохранении во время работы (а это именно то, что стоит делать) возникнет лаг.
P. P. S. Пример предложенного мною метода не является превосходной реализацией, но это лучше, чем было. В связи с тем, что я не хочу усложнять FAQ, я предложил именно это решение. Я ставлю перед собой задачу дать толчок в сторону хорошей (или хотя бы неплохой) реализации, а вы, при должном желании, можете сделать ещё лучше.
C#:
public static void SaveAll(object state = null)
{
try
{
Log.Write("Saving items...", nLog.Type.Info);
if (Items.Count == 0) return;
Dictionary<int, List<nItem>> cItems = new Dictionary<int, List<nItem>>(Items);
foreach (KeyValuePair<int, List<nItem>> kvp in cItems)
{
int UUID = kvp.Key;
string json = JsonConvert.SerializeObject(kvp.Value);
MySQL.Query($"UPDATE `inventory` SET items='{json}' WHERE uuid={UUID}");
}
Log.Write("Items has been saved to DB.", nLog.Type.Success);
}
catch (Exception e)
{
Log.Write("EXCEPTION AT \"INVENTORY_SAVEALL\":\n" + e.ToString(), nLog.Type.Error);
}
}
C#:
public static async void SaveAll(object state = null)
{
try
{
Log.Write("Saving items...", nLog.Type.Info);
if (Items.Count == 0) return;
Dictionary<int, List<nItem>> cItems = new Dictionary<int, List<nItem>>(Items);
List<string> queries = new List<string>();
foreach (KeyValuePair<int, List<nItem>> kvp in cItems)
{
int UUID = kvp.Key;
string json = JsonConvert.SerializeObject(kvp.Value);
queries.Add($"UPDATE `inventory` SET items='{json}' WHERE uuid={UUID}");
}
await MySQL.QueryAsync(queries);
Log.Write("Items has been saved to DB.", nLog.Type.Success);
}
catch (Exception e)
{
Log.Write("EXCEPTION AT \"INVENTORY_SAVEALL\":\n" + e.ToString(), nLog.Type.Error);
}
}
C#:
public static async Task QueryAsync(List<string> queries)
{
try
{
using (MySqlConnection connection = new MySqlConnection(Connection))
{
await connection.OpenAsync();
using (MySqlCommand cmd = new MySqlCommand())
{
foreach (var query in queries)
{
if (Debug) Log.Debug("Query to DB:\n" + query);
cmd.Connection = connection;
cmd.CommandText = query;
await cmd.ExecuteNonQueryAsync();
}
}
}
}
catch (Exception e)
{
Log.Write(e.ToString(), nLog.Type.Error);
}
}
3. Допуск неэкранированных строк к запросу. Последствия очевидны: SQL-инъекции. Я не буду останавливаться на данной проблеме, так как в интернете есть исчерпывающая информация по эксплуатации данной уязвимости и защите от неё.
Проблема: допуск произвольных строк к исполнению при запросе влечёт за собой возможность исполнения произвольного запроса в базу данных.
Решение: экранирование строк.
Пример:
C#:
await MySQL.QueryAsync($"UPDATE accounts SET character{slot}=-1 WHERE login='{Login}'");
C#:
// Я не экранировал slot, так как можно считать, что данная переменная не может иметь
// произвольное значение, взятое извне.
using MySqlCommand cmd = new MySqlCommand("UPDATE accounts SET character{slot}=-1 WHERE login='@login'");
cmd.Parameters.AddWithValue("@login", Login);
await MySQL.QueryAsync(cmd);
В моде есть ещё и другие проблемы с базой данных, но в рамках данного FAQ я не стал рассматривать все мелочи. Данные мною решения позволят оптимизировать самые критичные для производительности задачи, а последнее немного улучшит безопасность. Буду рад видеть ваши комментарии и критику. Если FAQ получит положительный отклик, то я с радостью выпущу ещё одну статью с критикой безопасности данного мода.
Другие статьи:
Как не стоит делать: безопасность ивентов и клиентских скриптов
[JS] Как подключить отладчик (Debugger) в PhpStorm для server-side
Последнее редактирование: