Redis’de Data Consistency’i Sağlama
Selamlar,
Bu makalede, Redis kullanırken data tutarlılığının sağlanması amacı ile, dikkat edilmesi gereken birkaç konuya değinmek istiyorum.
Konunun daha iyi anlaşılması için örnek bir senaryo oluşturalım. Aşağıda görüldüğü gibi, birbirleri ile bağlantılı dört tablo gözükmektedir.
- “DB_USER” adında kullanıcıların saklandığı bir tablo bulunmaktadır.
- “DB_SECURITY_ROLE” adında, kullanıcı rollerinin tutulduğu bir başka tablo bulunmaktadır.
- “[DB_SECURITY_USER_ROLE]”: Herbir Role’e ait, yetkilerin tutulduğu bir tablodur.
- “[DB_SECURITY_USER_ACTION]”: Herbir kullanıcının “IdSecurityRole” kolonu olsa da, kendilerine ait yetkiler bu tabloda ayrıca tutulmaktadır. “DB_SECURITY_ROLE” tablosunun kullanılma amacı, user ilk başta yaratılırken, yetkilerinin ilgili tablo üzerinden yaratılmasıdır. Bu bir çeşit taslak şeklinde kullanılmaktadır. Böylece özellikle bir kullanıcıya spesifik bir yetki verileceği zaman, ilgili role ayrıca bir yetki verilmesine gerek kalmayacaktır. Kolayca kullanıcı üzerinden bu yetki değişikliği yapılabilecektir. Kısaca Role ile kullanıcı yetkisi, ilk yaratılma anından sonra ayrılacaktır. Ta ki, ilgili Role’de bir güncelleme yapılana kadar :) Makalenin devamında bu örneği de işleyeceğiz.
İsterseniz gelin şimdi, ilgili servisleri ve Cache yönetimini beraberce inceleyelim.
.Net Core WebApi bir projede, illgili tablolara ait crud işlemlere ait servislerin bir kısmını aşağıdaki gibi yazalım. Amaç, ilgili güncellemelerden sonra Redis’deki Cache yönetiminin incelenmesidir.
GetAllUsers(): Tüm kullanıcı listesini, rolleri ile beraber önce Redis’e bakarak eğer Redis’de yok ise, daha sonrasında DB’den çekip, Redis’e atayan ve en sonunda da tüm kullanıcı listesini geri dönen bir methoddur.
- “_redisCacheManager.Set(cacheKeyGetAllUsers, users)“: Belirlenen key’e göre Redise atılan UserModel, “RedisCacheManager” sınıfında yönetilmektedir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
public ServiceResponse<UserModel> GetAllUsers() { var response = new ServiceResponse<UserModel>(null); //Check Redis var cacheKeyGetAllUsers = string.Format(CacheKeys.GetAllUsers); var getAllUsersResult = _redisCacheManager.Get<List<UserModel>>(cacheKeyGetAllUsers); if (getAllUsersResult == null) { var users = (from user in _context.DbUser join role in _context.DbSecurityRole on user.IdSecurityRole equals role.IdSecurityRole into roleLefts from role in roleLefts.DefaultIfEmpty() select new UserModel { Name = user.Name, LastName = user.LastName, UserName = user.UserName, Password = user.Password, Email = user.Email, Gsm = _encryption.DecryptText(user.Gsm, ""), IsAdmin = user.IsAdmin, SecurityRoleName = role.SecurityRoleName, IdSecurityRole = role.IdSecurityRole, IdUser = user.IdUser, CreDate = user.CreDate, IsRoleLock = user.IsRoleLock }).ToList(); if (users != null) { response.List = users; //Set Redis _redisCacheManager.Set(cacheKeyGetAllUsers, users); } } else { //Get From Redis response.List = getAllUsersResult; } return response; } |
“Nasıl görmen gerektiğini öğren. Her şeyin birbiriyle bağlantılı olduğunu fark et.” ― Leonardo da Vinci
1. Önemli Nokta:
Redis için oluşturulan “KEY”, kodun içine static olarak yazılmamalı, mutlaka tek bir yerden yönetilmelidir. Yukarıdaki örnekde görüldüğü gibi, “CacheKeys.GetAllUsers” key’i, farklı bir class’dan alınarak servis içinde kullanılmıştır. Böylece, ilerde Redis’den ilgili string key değiştirilmek istendiğinde, bunu kullanan tüm servisler değil sadece “CacheKeys” class’ının düzenlenmesi yeterli olacaktır. Ayrıca kodlamada bir standardization sağlanacak ve her bir developer’a göre Redis’de benzer keylerin oluşturulmasının önüne geçilebilecektir.
InsertUser():
Bir kullanıcı kaydı girilirken, çağrılan methoddur.
- “_usersRepository.Insert(model, true)” => İlgili kod ile, yeni bir kullanıcı girişi DB’ye yapılır. Biz bu makalede, Redis üzerindeki adımaları inceleyeceğiz.
- “if (entityViewModel.IdSecurityRole != 0)“: Kaydedilen kullanıcıya, eğer bir Role atanmış ise, bu koşul çalıştırılır. Amaç, aynı bir taslakda olduğu gibi ilgili role ait yetkilerin, oluşturulan yeni kullanıcı için de sıfırdan atanmasıdır.
- “var userRoleModelList = _userRoleRepository.TableNoTracking.Where(ur => ur.IdSecurityRole == entityViewModel.IdSecurityRole).ToList()“: “DB_SECURITY_USER_ROLE” üzerindeki tüm yetkiler, kullanıcının RoleID’sine göre çekilir. Başta da bahsetildiği gibi, Role tablosu aslında kullanıcı yetkileri için bir çeşit taslak tablodur.
- “_userActionRepository.Insert(userActionModelList)“: Seçilen Role ait yetkiler çekildikten sonra, kaydedilecek user’ın yetkileri olarak “DB_SECURITY_USER_ACTION” tablosuna kaydedilir.
-
“var cacheKeyGetAllUsers = string.Format(CacheKeys.GetAllUsers)“: Tüm kullancı datası, Redis Cache’den çekilmesi amaçlı ilgili “GetAllUsers” key, “CacheKeys” sınıfından çekilir.
-
“var getAllUsersResult = _redisCacheManager.Get<List<UserModel>>(cacheKeyGetAllUsers)“: Redis’de bulunan tüm kullanıcı listesi alınır.
- “if (getAllUsersResult != null){ getAllUsersResult.Add(redisModel); _redisCacheManager.Set(cacheKeyGetAllUsers, getAllUsersResult); }” : Yeni eklenen kullanıcı, cache’deki dataya eklendikten sonra, tekrardan redise set edilir. Kısacası üzerine ezilir.
- “var cacheUserKey = string.Format(CacheKeys.UserDetail, redisModel.IdUser); _redisCacheManager.Set(cacheUserKey, redisModel)“: Kullanıcı detay sayfasında da, istenen kullanıcının detay bilgisi, yine Redis Cache’den çekildiği için ,buradaki “UserDetail” alanına, yeni girilen kullanıcı bilgisi ayrıca set edilir.
InsertUser():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
public IServiceResponse<UserModel> Insert(UserModel entityViewModel, int userId) { using (var transaction = _context.Database.BeginTransaction()) { var response = new ServiceResponse<UserModel>(null); var model = _mapper.Map<DB.Entities.DbUser>(entityViewModel); try { _usersRepository.Insert(model, true); if (entityViewModel.IdSecurityRole != 0) { var userRoleModelList = _userRoleRepository.TableNoTracking.Where(ur => ur.IdSecurityRole == entityViewModel.IdSecurityRole).ToList(); var userActionModelList = _mapper.Map<List<DbSecurityUserAction>>(userRoleModelList); userActionModelList.ForEach(ua => ua.IdUser = model.IdUser); _userActionRepository.Insert(userActionModelList); } var returnModel = _mapper.Map<UserModel>(model); response.Entity = returnModel; transaction.Commit(); //Check And Insert Redis var redisModel = _mapper.Map<UserModel>(model); redisModel.Gsm = entityViewModel.Gsm; //Decode GSM For Redis var cacheKeyGetAllUsers = string.Format(CacheKeys.GetAllUsers); var getAllUsersResult = _redisCacheManager.Get<List<UserModel>>(cacheKeyGetAllUsers); if (getAllUsersResult != null) { getAllUsersResult.Add(redisModel); //Set Redis _redisCacheManager.Set(cacheKeyGetAllUsers, getAllUsersResult); } //Check And Insert UserDetail Redis var cacheUserKey = string.Format(CacheKeys.UserDetail, redisModel.IdUser); _redisCacheManager.Set(cacheUserKey, redisModel); //------------------------------- return response; } catch (Exception ex) { transaction.Rollback(); response.ExceptionMessage = ex.Message; response.IsSuccessful = false; response.HasExceptionError = true; return response; } } } |
2. Önemli Nokta:
Yeni bir data sisteme dahil olduğu zaman, bu ve buna bağlı tüm cache datalarının güncellenmesi gerekmektedir. Buradan yola çıkılır ise, birbirine bağımlı cachle alanlar arasında relation, DB’deki relationdan farklı olabilir. Bu durumda, cacheli datalar arasındaki bağlantıyı göstermek için, ayrıca bir döküman veya Diagram hazırlanması gerekmektedir.
AddUserActionList(): Bu 3. örnekde aynı anda bir veya birden fazla kullanıcıya, seçilen kullanıcı yetkileri ya güncellenecek ya da yeni bir kayıt olarak atanacaktır. Gelin Redis tarafında neler yapılıyor, hep beraber inceleyelim.
- “Dictionary<string, List<SecurityUserActionModel>> redisValues = new()“: Tüm key – value değerleri, Redis’e topluca atılması için, bir Dictionary Liste oluşturulur. Görüldüğü üzere .Net 5.0’a özel :) sadece “new()” tanımlaması yeterli olmuştur.
- “foreach (int userid in userIDs)“: Seçilen tüm kullanıcılar, tek tek gezilmiştir.
- “redisValues.Add(string.Format(CacheKeys.GetUserPermissionByUserId, userid), UserActionList)“: Seçilen her bir kullanıcıya, işaretlenen yetkiler toplu olarak Dictionary List’e “GetUserPermissionByUserId” keyi ve ilgili UserID’si ile eklenmiştir.
- “_redisCacheManager.SetAll(redisValues)“: Son olarak seçilen tüm userlar için oluşturulan Dictionary List, Redis’e Bulk olarak atılır.
- Redis/SetAll<T>(): Aşağıda görüldüğü gibi redisCacheManager sınıfındaki SetAll() methodu, kendisine gönderilen <T> tipindeki Dictionary Listi, bulk olarak Redis’e, ilgili keyi ile birlikte atamaktadır.
-
12345678910111213141516public void SetAll<T>(IDictionary<string, T> values){try{using (IRedisClient client = new RedisClient(conf)){client.SetAll(values);}}catch{throw new RedisNotAvailableException();}}
-
- Redis/SetAll<T>(): Aşağıda görüldüğü gibi redisCacheManager sınıfındaki SetAll() methodu, kendisine gönderilen <T> tipindeki Dictionary Listi, bulk olarak Redis’e, ilgili keyi ile birlikte atamaktadır.
AddUserActionList():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
public ServiceResponse<SecurityUserActionModel> AddUserActionList(List<SecurityUserActionModel> UserActionList, int?[] userIDs) { var response = new ServiceResponse<SecurityUserActionModel>(null); var model = _mapper.Map<List<DB.Entities.DbSecurityUserAction>>(UserActionList); Dictionary<string, List<SecurityUserActionModel>> redisValues = new(); foreach (int userid in userIDs) { redisValues.Add(string.Format(CacheKeys.GetUserPermissionByUserId, userid), UserActionList); } _redisCacheManager.SetAll(redisValues); using (var transaction = _context.Database.BeginTransaction()) { try { _userActionRepository.Delete(_userActionRepository.Table.Where(u => userIDs.Contains(u.IdUser))); _userActionRepository.Insert(model); transaction.Commit(); return response; } catch (Exception ex) { transaction.Rollback(); return response; } } } |
Redise atılan kullanıcı yetkileri aşağıdaki gibidir. Aşağıda görüldüğü gibi, “GetUserPermissionByUserId” keyword’ünün yanına “:UserID” değeri getirilmiştir. Value değeri olarak da User’ın tüm yetkileri, “SecurityUserActionModel” şeklinde bir liste olarak atılmıştır. Böylece istenen kullanıcının yetkileri, kolaylıkla redisden çekilebilecektir. Aşağıda görüldüğü gibi, GetUserPermissionByUserId sürekli tekrarladığı için ala klasör olarak atanmış, sonuna “:” getirilen “UserID” subfolder olarak tanımlanmıştır. Bu klasörün de altında kullanıcı yetkileri, bir list şeklinde durmaktadır.
3. Önemli Nokta:
Redis’de klasörleme, tekrarlayan keywordler baz alınarak yapılır. Alt kırılım “:“, sembolü ile yapılmaktadır. Aşağıdaki örnekde olduğu gibi, sabit önde tekrarlayan GetUserPermissionByUserId keyword’ü, ana klasör, sonradan sonuna “:” ile eklenen UserID, subfolder olarak Redisde oluşturulmuştur. Bu klasörün de altında, kullanıcı yetkileri olan, SecurityUserActionModel liste şeklinde saklanmaktadır. Redisi görüntülemek için, Redis Desktop Manager kullanılmıştır.
AddRoleActionList(): Son örnek olarak, nispeten diğerlerine göre biraz daha complex bir senaryo ile karşı karşıyayız. Var olan bir Rolün, yetkilerinin güncellenmesi durumu. Doğal olarak bu role ait tüm kullanıcıların da, bu değişimden etkileneceği unutulmamalıdır. Kısacası, ilgili Role’e ait yetkilerde bir güncelleme olduğu zaman, bu Role ait bağımsız Userlerın da yetkilerinin, güncellenmesi gerekmektedir.
- “var cachkeyRolePermissionByRoleId = string.Format(CacheKeys.GetRolePermissionByRoleId, roleID) ” : Redis’de kayıtlı ilgili Role’e ait tüm yetkilerin alınması için, ilgili key oluşturulur.
- “_redisCacheManager.Remove(cachkeyRolePermissionByRoleId)“: Güncellenecek Role Redis datası, temizlenir.
- “var userIds = _usersRepository.TableNoTracking.Where(x => x.IdSecurityRole == roleID && x.IsRoleLock == false).Select(x => x.IdUser).ToList()”: İlgili Role’ü kullanan ve kitli olmayan tüm kullanıcılar, DB’den çekilir.
- “_redisCacheManager.Set(cachkeyRolePermissionByRoleId, model)” : Son güncel Role yetkileri, Redis cache’e tekrardan atılır.
- “foreach (var userId in userIds)“: İlgili Role’e sahip herbir user, tek tek gezilir.
- “redisValues.Add(string.Format(CacheKeys.GetUserPermissionByUserId, userId), _mapper.Map<List<SecurityUserActionModel>>(modelUserAction))“: Değişen Role sahip herbir kullanıcının Redisdeki kullanıcı yetkileri, güncelenen role’ün yetkileri ile değiştirilmek amacı ile redisValues dictionary’e eklenir.
- “_redisCacheManager.SetAll(redisValues)“: Değişen Role’e sahip tüm kullanıcıların yetkileri, DB’den olduğu gibi Redis’den de güncellenir.
Not: “var userIds = _usersRepository.TableNoTracking.Where(x => x.IdSecurityRole == roleID && x.IsRoleLock == false).Select(x => x.IdUser).ToList()” : Merak edenler için biraz konu dışı ama :), burada “IsRoleLock” değeri true olan user’ların yetkiler değiştirilmeyecektir. İlk yaratılma anında, bazı kişilere o Role ait yetkiler özel olarak atanmış olsa da, devamında Role’den bağımsız hale getirilmek istendiğinde “IsRoleLock” kolonuna true değeri atanmış ve ilgili Role’ün yetikilerinin değişmesine rağmen, kitli userın yetkilerinin değişmemesi sağlanmıştır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
public ServiceResponse<SecurityUserRoleModel> AddRoleActionList(List<SecurityUserRoleModel> roleActionList, int roleID) { var response = new ServiceResponse<SecurityUserRoleModel>(null); var model = _mapper.Map<List<DB.Entities.DbSecurityUserRole>>(roleActionList); var cachkeyRolePermissionByRoleId = string.Format(CacheKeys.GetRolePermissionByRoleId, roleID); _redisCacheManager.Remove(cachkeyRolePermissionByRoleId); Dictionary<string, List<SecurityUserActionModel>> redisValues = new(); using (var transaction = _context.Database.BeginTransaction()) { try { var userIds = _usersRepository.TableNoTracking.Where(x => x.IdSecurityRole == roleID && x.IsRoleLock == false).Select(x => x.IdUser).ToList(); var deleteUserActions = _userActionRepository.Table.Where(x => userIds.Contains(x.IdUser ?? 0)); _userActionRepository.Delete(deleteUserActions); _roleActionRepository.Delete(_roleActionRepository.Table.Where(u => u.IdSecurityRole == roleID)); _roleActionRepository.Insert(model); _redisCacheManager.Set(cachkeyRolePermissionByRoleId, model); foreach (var userId in userIds) { var modelUserAction = _mapper.Map<List<DbSecurityUserAction>>(model); // DeepCopy'si alınıyor. modelUserAction.ForEach(ua => ua.IdUser = userId); _userActionRepository.Insert(modelUserAction); redisValues.Add(string.Format(CacheKeys.GetUserPermissionByUserId, userId), _mapper.Map<List<SecurityUserActionModel>>(modelUserAction)); } transaction.Commit(); _redisCacheManager.SetAll(redisValues); return response; } catch (Exception ex) { transaction.Rollback(); return response; } } } |
Redis ile çalışırken, relational data modellerinde, data tutarlılığını sağlamak gerçekten çok zordur. Redisin kendi üzerinde Data consistency’i sağlıyacağı ayrıca bir tool’u yoktur. Ama ben size third party’i tool olarak, RedisRaft‘ı öneririm. Dataların ve alt kümelerinin değişiminde, Redis’de bundan etkilenecek ve ilgili keywordlerin altındaki dataların, alt kümelerinin ve hatta onların da altındaki diğer kümelerin cache’den düşürlmesi gerekmekte ve bunun için istenirse de, bir başka yöntem olan “Lua Script” kullanılabilmektedir. Main keyword üzerinden filitreleme yapılarak, valuelar ve onların altındaki tüm alt kümelerin temizlenmesi sağlanabilir. Tek sorun, “Lua Script” biraz yavaştır. Yüksek trafikli yapılarda pek tercih edilmemelidir. Ama tabi Redis’in tekrardan güncellemesi isteniyor ise, gene alt kümelerin keywordlerinin belirlenmesi ve güncel dataların tek tek setlenmesi gerekmektedir.
Geldik bir makalenin sonuna. Yeni bir makalede görüşmek üzere hepinize hoşçakalın.
Son Yorumlar