Кэширование данных в ASPNET MVC SinglePageApplication

Вопрос:

Я ищу решение или передовую практику в отношении моей проблемы. Поскольку это больше проблема архитектуры, у меня нет исходного кода…

Моя проблема заключается в том, как я могу справиться с сессией ASP.NET, касающейся моих требований. Или это не соответствует моим требованиям?

Ситуация: у меня есть сборка SinglePageApplication поверх ASP.NET MVC. MVC-приложение предоставляет серверный интерфейс для работы с бэкэнд. MVC-приложение вызывает службы другого бэкэнд-сервера для запроса данных. Эти данные могут быть много и должны быть кэшированы в MVC-приложении.

Насколько я вижу, ASP.NET-сессия не кажется лучшим решением для SPA с момента своего параллельного поведения…?! Невозможно иметь одновременные ajax-запросы, если есть какой-либо запрос, который требует доступа на запись к сеансу (и это происходит в моем случае, поскольку данные должны быть кэшированы).

Пример. Типичная ситуация, по крайней мере в моем приложении, заключается в том, что когда пользователь обращается к определенному экрану, выполняются некоторые ajax-запросы. Например, первый запрос запускает загрузку данных с сервера-сервера и занимает некоторое время (может быть, около 30 секунд). Он также требует доступа к записи для сеанса, поскольку он должен кэшировать данные для этого (!) Пользователя. Другие запросы теперь должны ждать, даже если они требуют только чтение-доступ к сеансу и не обрабатывают загружаемые данные. Поведение, которое один запрос ожидает другого, должно происходить только в определенных ситуациях.

Теперь я могу реализовать другой механизм кэширования данных для каждого пользователя, возможно, используя System.Runtime.Caching, но является ли это рекомендуемым или лучшим решением?

Какой был бы лучший образец для решения этой ситуации?

Есть ли лучшие практики?

Edit2:

Одной из важных моментов является поведение сеанса при выполнении одновременных ajax-вызовов. Я подготовил небольшой пример: приложение, выполняющее ajax-вызовы – некоторые используют сеанс как ReadOnly – для других требуются ReadWrite. У действий есть тайм-аут, вопрос более заметный…

Это мои контроллеры:

[SessionState(System.Web.SessionState.SessionStateBehavior.ReadOnly)]
public class SessionReadOnlyController : Controller
{
public ActionResult Perform()
{
Session["x"] = 5;

Thread.Sleep(1500);
return Json(new { success = true }, JsonRequestBehavior.AllowGet);
}
}

[SessionState(System.Web.SessionState.SessionStateBehavior.Required)]
public class SessionReadWriteController : Controller
{
public ActionResult Perform()
{
Session["x"] = 5;

Thread.Sleep(1500);
return Json(new { success = true }, JsonRequestBehavior.AllowGet);
}
}

Это javascript-код, который выполняет ajax-вызовы. Все вызовы выполняются немедленно:

jQuery(function ()
{
$('body').append('<p>start....1</p>');
$.ajax({
url: '/SessionReadWrite/Perform?id=1',
success: function () { $('body').append('<p>success1</p>'); }
});

$('body').append('<p>start....2</p>');
$.ajax({
url: '/SessionReadWrite/Perform?id=2',
success: function () { $('body').append('<p>success2</p>'); }
});

$('body').append('<p>start....3</p>');
$.ajax({
url: '/SessionReadOnly/Perform?id=3',
success: function () { $('body').append('<p>success3</p>'); }
});

$('body').append('<p>start....4</p>');
$.ajax({
url: '/SessionReadOnly/Perform?id=4',
success: function () { $('body').append('<p>success4</p>'); }
});

$('body').append('<p>start....5</p>');
$.ajax({
url: '/SessionReadOnly/Perform?id=5',
success: function () { $('body').append('<p>success5</p>'); }
});
});

Результаты в моем случае (взяты из инструмента разработчика интернет-исследователя):

  • Вызов 1 отвечает после 1,51
  • Звонок 2 отвечает после 3 03
  • Звонок 3 отвечает после 4,56
  • Call 4 отвечает после 4,56s
  • Звонок 5 отвечает после 4,56

Поскольку я понимаю поведение ситуации, действия ReadWrite-Session блокируют сеанс даже для действий ReadOnly-Session…?! (Или я сделал что-то совершенно не так?)

Из msdn (http://msdn.microsoft.com/de-de/library/ms178581%28v=vs.100%29.aspx), последняя глава:

[…] Однако, если два одновременных запроса сделаны для одного и того же сеанса (с использованием того же значения SessionID), первый запрос получает эксклюзивный доступ к информации о сеансе. Второй запрос выполняется только после завершения первого запроса. […] Тем не менее, запросы только для чтения для данных сеанса могут по-прежнему ждать блокировки, установленной запросом чтения-записи для данных сеанса, чтобы очистить.

Что касается моей ситуации: если я хочу кэшировать данные (в памяти все в порядке, потому что приложение используется всего несколькими пользователями) в SinglePageApplication мне нужна другая концепция, чем сеанс… поэтому мой вопрос, есть ли лучшие практики?

Изменение: Улучшено описание ситуации…

Edit2: добавлен пример…

Лучший ответ:

Теперь, наконец, проблема ясна. Поскольку ASP.NET блокирует доступ к сеансу для одного и того же пользователя из разных потоков, сеанс не позволит вам решить вашу проблему.

Таким образом, вам нужно реализовать что-то потокобезопасное самостоятельно. Как вы говорите в своем вопросе о том, что людей не так много, а потребление памяти не представляет проблемы, вы можете использовать собственный статический класс, срок жизни которого зависит от его создания (первого доступа) до конца или приложения (например, перезагрузка сервера, или утилизации пула приложений).

Вы должны решить, хранятся ли данные для пользователя или для сеанса. Если он хранится для пользователя, он “выдержит” таймауты сеанса. Если он хранится для сеанса, идентифицированного идентификатором сеанса, вы можете обработать событие конца сеанса в global.asax когда закончится сеанс.

Вам нужно два уровня блокировки:

  1. один для доступа к данным и блокировок для конкретного пользователя (или для создания нового и возврата его).
  2. другой для блокировки пользователя и данных (который описан выше)

Для 1 вы можете использовать что-то вроде ConcurrentDictionary. В этом словаре держите идентификатор пользователя + связанную блокировку и данные для пользователя. Всякий раз, когда вам нужно читать или писать данные для пользователя, получить его из этого словаря. Этот первый уровень или блокировка гарантирует, что блокировка и данные для пользователя доступны без помех от других потоков. Для этого вы можете использовать метод GetOrAdd.

Для 2 вы можете использовать объект с такими членами, который будет объектом, возвращаемым из словаря, когда предоставляется идентификатор пользователя:

private object _data; // to store the cached data

public void StoreData(object data)
{
    // - If there is a lock return inmediately: 
    //   someone is already writing the data
    // - If there is no lock, create the lock, store the data in _data, 
    //   and release the lock
}

public object GetData(int userId)
{
    // - Wait for the lock
    // - Return data from _data
}

Существует множество способов блокировки доступа к _data. Я бы использовал обработчики ожидания событий: см. Здесь полное объяснение. Но есть еще много правильных вариантов.

С этой реализацией,

  • любой поток, пытающийся прочитать или написать, получит пользовательские данные из словаря 1. Этот замок для GetOrAdd будет очень коротким. Первое, что доступ к словарю создаст объект (2). Следующие потоки будут восстанавливать один и тот же объект (2).
  • если поток пытается прочитать данные, он будет использовать метод GetData объекта (2), который будет блокироваться до тех пор, пока не будут доступны данные (на самом деле он мог бы читать нулевые данные и запускать StoreData и StoreData GetData для гарантии создание данных). * если поток пытается записать данные, он будет использовать объект (2) метод StoreData, который создаст блокировку, и освободит его, когда он будет записан

Чтобы выполнить хорошую реализацию, вам нужно будет получить функцию, которая возвращает данные, так что создание данных также будет защищено блокировкой, то есть использовать такой метод, а не StoreData:

public void CreateStoreData(Func<object> methodThatCreatesData)
{
    // - If there is a lock return inmediately: 
    //   someone is already writing the data
    // - If there is no lock, create the lock, call methodThatCreatesData,
    //   write the data, and release the lock
}

Тогда ваш код должен был бы сделать это:

DataCreator dc = new DataCreator(); // prepare the class that generates the data
var userData = UsersData.GetOrAdd(...) // get the dictionary entry for the user
userData.CreateStoreData(dc.DataCreatorMethod);

Блокировка – это не простая задача для достижения, и очень важная вещь – гарантировать, что любой установленный замок всегда выпускается. Для этого вы можете использовать шаблон try-finally.

Оцените статью
TechArks.Ru
Добавить комментарий