Я работал над проектом ASP.NET MVC 4
, в котором пользователи могут применять фильтр к пользовательскому интерфейсу и извлекать отчет Excel. Данные хранились в MS SQL server
. Чтобы повысить производительность, я решил использовать async
в своем приложении, чтобы:
- Сократить время извлечения
- Разрешить пользователям иметь параллельное извлечение
Для этого я использую следующие методы:
Когда пользователь нажимает кнопку извлечения в пользовательском интерфейсе -> от клиента будет выполнен вызов ajax
к функции async
на сервере -> Эта функция async
, в свою очередь, создаст объект Command
и сделает ExecuteReaderAsync()
. Используя этот DbDatareader
, сгенерируйте файл Excel с помощью NPOI
и сохраните содержимое файла в TempData
. Обработчик для получения файла будет возвращен клиенту для последующей загрузки с использованием window.location
. Я перенял эти методы из этой публикации. Загрузить файл Excel через AJAX MVC
После первого извлечения, если пользователи хотят извлекать другие наборы данных параллельно, они могут нажать кнопку извлечения еще раз, и приложение повторит шаг 1.
В результате может произойти 2 или более извлечения данных одновременно.
Моя проблема, возьмем, к примеру, 4 извлечения, которые в настоящее время выполняются параллельно, если какое-либо из этих извлечений завершено и загружен 1 файл (с использованием window.location
). В следующий раз, когда пользователь нажмет кнопку извлечения (которая повторяет шаг 1), он больше не будет асинхронным c, и более поздние извлечения будут ждать завершения предыдущего извлечения sh перед выполнением.
При отладке, если я перезапустите сервер ISS, проблема исчезла на некоторое время, пока не будет загружен 1 файл, поэтому я сомневался, что window.location
делает что-то, что блокирует потоки на сервере при загрузке любого файла.
ОБНОВЛЕНИЕ 1
Класс:
public class QUERYREADER
{
public DbConnection CONNECTION { get; set; }
public DbDataReader READER { get; set; }
}
Модель:
public async Task<QUERYREADER> GET_DATA(CancellationToken ct)
{
//Create the query reader
QUERYREADER qr = new QUERYREADER();
//Set up the database instances
DbProviderFactory dbFactory = DbProviderFactories.GetFactory(db.Database.Connection);
//Defined the query
var query = "SELECT * FROM Table";
//Set up the sql command object
using (var cmd = dbFactory.CreateCommand())
{
//Try to open the database connection
try
{
//Check if SQL connection is set up
if (cmd.Connection == null)
{
cmd.CommandType = CommandType.Text;
cmd.Connection = db.Database.Connection;
}
//Open connection to SQL if current state is closed
if (cmd.Connection.State == ConnectionState.Closed)
{
//Change the connection string to set the packet size to max. value = 32768 to improve efficiency for I/O transmit to SQL server
cmd.Connection.ConnectionString = cmd.Connection.ConnectionString + ";Packet Size=20000";
//Open connection
await cmd.Connection.OpenAsync(ct);
}
//Save the connection
qr.CONNECTION = cmd.Connection;
} catch (Exception ex) {
//If errors throw, close the connection
cmd.Connection.Close();
};
//Retrieve the database reader of provided sql query
cmd.CommandText = query;
DbDataReader dr = await cmd.ExecuteReaderAsync(ct);
qr.READER.Add(dr);
}
//Return the queryreader
return qr;
}
Контроллер:
public async Task<JsonResult> SQL_TO_EXCEL()
{
//Set up the subscription to client for "cancellation request, browser closing"
CancellationToken disToken = Response.ClientDisconnectedToken;
//Get the datareader
try
{
qr = await GET_DATA(disToken);
}
catch(Exception ex) { }
//Open the connection to SQL server
using (qr.CONNECTION)
{
using (var dr = qr.READER)
{
while (await dr.ReadAsync(disToken))
{
for (int k = 0; k < dr.FieldCount; k++)
{
//.... using NPOI to write Excel file to MemoryStream
}
}
dr.Close();
}
}
//Generate XL file if controller action is still running (no "cancellation request, browser closing")
if (!disToken.IsCancellationRequested)
{
string file_id = Guid.NewGuid().ToString();
//... Write the NPOI excel file to TempData and then create a handler for later download at client
//This line caused trouble
TempData["file_id"] = XLMemoryStream.ToArray();
HANDLER["file_id"] = file_id;
HANDLER["file_name"] = FILE["FILE_NAME"].ToString().NonUnicode() + FILE["FILE_TYPE"].ToString() ;
}
//Return JSON to caller
var JSONRESULT = Json(JsonConvert.SerializeObject(HANDLER), JsonRequestBehavior.AllowGet);
JSONRESULT.MaxJsonLength = int.MaxValue;
return JSONRESULT;
}
public async Task<ActionResult> DOWNLOAD_EXCEL(string file_id, string file_name)
{
if (TempData[file_id] != null)
{
byte[] data = await Task.Run(() => TempData[file_id] as byte[]);
return File(data, "application/vnd.ms-excel", file_name);
}
else
{
return new EmptyResult();
}
}
Javascript
$.ajax({
type: 'POST',
async: true,
cache: false,
url: 'SQL_TO_EXCEL',
success: function (data)
{
var response = JSON.parse(data);
window.location =
(
"DOWNLOAD_EXCEL" +
'?file_id=' + response.file_id +
'&file_name=' + response.file_name
);
},
error: function (XMLHttpRequest, textStatus, errorThrown) {
console.log(errorThrown);
}
});
ОБНОВЛЕНИЕ 2:
После множества тестов я понял, что window.location
не имеет ничего общего с потоками на сервере, строка TempData[file_id] = XLMemoryStream.ToArray()
вызвали проблемы. Похоже, проблема аналогична описанной в этом посте. Два параллельных ajax запроса к методам Action поставлены в очередь, почему?