Pull to refresh

Написание своего Session Store Provider ASP.NET использующего Redis

Reading time8 min
Views5.3K
Приветствую,

В этой статье я бы хотел поделиться опытом написания собственного Session Store Provider'a с использованием Redis в качестве хранилища.

Про Redis много всего написано. Но если вкратце, то Redis это очень быстрое хранилище в формате ключ-значение (и не только). Как мне кажется, очень хорошо подходит для подобных задач и мне давно было интересно попробовать его в своем проекте.

Так же, для тех кто хочет попробовать результат в деле, я дам ссылку на исходники.

Зачем все это нужно


Мне понадобилось хранилище сессии отвечающее следующим требованиям:

1. Контроль над процессом записи/чтения сессии
Объясню попозже зачем.

2. Распределение между несколькими экземплярами приложения.
Для масштабирования приложения на несколько серверов.

3. Скорость.
Тут все понятно зачем.

Disclaimer

Сразу скажу, что пока этот код на этапе прототипа, сделанный в свободное время просто посмотреть, что из этого получится. Я не решил использовать его в проекте или нет, но думаю описанное тут, будет кому-то интересно. Так же есть корыстный умысел этой публикации — возможно кто-то укажет на недостатки такого подхода, до того как я все-таки решу использовать это в продакшене :)

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

Процесс установки Redis опущен. Могу сказать, что я поднял на VirtualBox Ubuntu Server, а дальше это было дело нескольких команд и правки нескольких файлов (для статического IP и для разрешения Redis принимать соединения не только с localhost'a).

Так же, я обязан упомянуть, что есть уже существующие решения, например Windows Server AppFabric Cache с его провайдером сессии, но из за пункта 1, все равно необходимо писать свой провайдер(с использованием только его функуциональности кэша), а по скорости мне кажется Redis будет быстрее.

Зачем нужен именно свой провайдер

Сессия по сути представляет собой коллекцию ключ-значение, этот словарь достается в начале запроса, и записывается(в случае изменений) в конце запроса.
Если у вас есть несколько запросов(request), которые используют сессию, причем не только для чтения(read-only) но и меняют в ней какие-то значения, то эти запросы не смогут выполнится параллельно. Это потому что в стандартных провайдерах эксклюзивно сессию (что необходимо для записи) может использовать только 1 запрос.

У нас на каждой странице есть несколько блоков которые загружаются параллельно(AJAX), и во всех необходим доступ к сессии (каждый блок меняет свои переменные). При любом стандартном провайдере они выполняются один за другим. А нам нужно параллельно.

Классы для реализации Session Store Provider

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

  1. RedisNoLockSessionStateStoreProvider унаследованный от SessionStateStoreProviderBase. Отвечает за «доставание» сессии (т.е. целой коллекции) из хранилища. Тут надо заметить, что меня поначалу всегда путали названия его методов: GetItem,RemoveItem,… По названиям можно решить, что класс отвечает за отдельные объекты в сессии, но это не так.

    В реализации, в которой элементы сессии могут быть изменены в нескольких запросах параллельно, этот класс на себя почти ничего не берет.
    Copy Source | Copy HTML
    1. public class RedisNoLockSessionStateStoreProvider : SessionStateStoreProviderBase
    2. {
    3. //...
    4. public override SessionStateStoreData CreateNewStoreData(System.Web.HttpContext context, int timeout)
    5.         {
    6.             string sessionId = context.Request.Cookies[RedisNoLockSessionIDManager.CookieName].Value;
    7.             return new SessionStateStoreData(new RedisNoLockSessionStateItemsCollection(sessionId, _defaultTimeout, _redisServer, _redisDb), SessionStateUtility.GetSessionStaticObjects(context), timeout);
    8.         }
    9.  
    10.         public override SessionStateStoreData GetItem(System.Web.HttpContext context, string id, out bool locked, out TimeSpan lockAge, out object lockId, out SessionStateActions actions)
    11.         {
    12.             lockAge = TimeSpan.Zero;
    13.             lockId = null;
    14.             locked = false;
    15.             actions = SessionStateActions.None;
    16.              //Вот тут мы и возвращаем коллекцию RedisNoLockSessionStateItemsCollection
    17.             return new SessionStateStoreData(new RedisNoLockSessionStateItemsCollection(id, _defaultTimeout, _redisServer, _redisDb), SessionStateUtility.GetSessionStaticObjects(context), _defaultTimeout);
    18.         }
    19.  
    20.         public override SessionStateStoreData GetItemExclusive(System.Web.HttpContext context, string id, out bool locked, out TimeSpan lockAge, out object lockId, out SessionStateActions actions)
    21.         {
    22.             return this.GetItem(context, id, out locked, out lockAge, out lockId, out actions);
    23.         }
    24.  
    25. //...
    26. }


  2. RedisNoLockSessionStateItemsCollection унаследованный от ISessionStateItemCollection — это как раз класс реализующий все необходимое для доступа именно к элементам сесссии. В текущей реализации он берет на себя почти всю работу.
    Думаю из кода ниже понятно, как там все устроено. Идея в том, что каджый item получается и сохранятеся отдельно.

    Copy Source | Copy HTML
    1. public class RedisNoLockSessionStateItemsCollection : RedisSessionConfig, ISessionStateItemCollection
    2.     {
    3. //...
    4.     public object this[string name]
    5.         {
    6.             get
    7.             {
    8.                 string key = GetKey(name);
    9.                 using (var redis = SingleRedisPool.GetClient(_redisServer))
    10.                 {
    11.                     return redis.Get<object>(key);
    12.                 }
    13.             }
    14.             set
    15.             {
    16.                 string key = GetKey(name);
    17.                 using (var redis = SingleRedisPool.GetClient(_redisServer))
    18.                 {
    19.                     bool result = redis.Set<object>(key, value);
    20.                 }
    21.             }
    22.         }
    23. //...
    24. }


  3. Ну и третий класс RedisNoLockSessionIDManager который отвечает за создание ID сессии и запись cookie, он совсем простой.
    Copy Source | Copy HTML
    1. public class RedisNoLockSessionIDManager : RedisSessionConfig, ISessionIDManager
    2.     {
    3.         public string CreateSessionID(HttpContext context)
    4.         {
    5.             return Guid.NewGuid().ToString("N");
    6.         }
    7.  
    8.         public string GetSessionID(HttpContext context)
    9.         {
    10.             var cookie = context.Request.Cookies.Get(CookieName);
    11.             return cookie != null ? cookie.Value : null;
    12.         }
    13.  
    14.         public bool InitializeRequest(HttpContext context, bool suppressAutoDetectRedirect, out bool supportSessionIDReissue)
    15.         {
    16.             supportSessionIDReissue = false;
    17.             return false;
    18.         }
    19.  
    20.         public void SaveSessionID(HttpContext context, string id, out bool redirected, out bool cookieAdded)
    21.         {
    22.             HttpCookie cookie = new HttpCookie(CookieName, id);
    23.             cookie.Expires = DateTime.Now.Add(Configuration.Timeout);
    24.             context.Response.Cookies.Add(cookie);
    25.  
    26.             redirected = false;
    27.             cookieAdded = true;
    28.         }
    29.  
    30.         public bool Validate(string id)
    31.         {
    32.             return true;
    33.         }
    34.         //..
    35.     }

Вот и все. Опущены моменты получения конфигурационных настроек из web.config, но эта часть имеет довольно посредственное отношение к самой сессии, можно посмотреть эту часть в исходниках.

Заключение

Несколько вещей хотелось бы сказать в заключение.
  • Эта версия построена из интереса, хотя на первый взгляд и решает все мои задачи. Пользоваться стоит аккуратно.
  • Почти 100% будут проблемы если несколько запросов будут стараться поменять один и тот же элемент в сессии.
  • В исходниках так же есть версия с более классической реализацией, т.е. с «вытаскиванием» и lock'ом всей коллекции, но я ее почти не тестировал. Имеется страничка эмулирующая параллельные запросы от одного пользователя, можно посмотреть как это работает при в влючении разных реализаций сессии.
  • Ну и собствеенно сами исходники находятся тут redissessionstore.codeplex.com

Буду рад услышать ваши комментарии. Спасибо за внимание.
Tags:
Hubs:
+22
Comments28

Articles

Change theme settings