.Net Core快取元件(Redis)原始碼解析
上一篇文章已經介紹了MemoryCache,MemoryCache儲存的資料型別是Object,也說了Redis支援五中資料型別的儲存,但是微軟的Redis快取元件只實現了Hash型別的儲存。在分析原始碼之前,先學幾個關於Redis操作的命令。
一、Redis命令
Redis所有的命令在http://doc.redisfans.com/上有詳細介紹。下面介紹幾個常用的關於Hash型別的命令。
HSET:用於新增快取
用法:HSET key field value 。
返回值:如果 field 是雜湊表中的一個新建域,並且值設定成功,返回 1 。
如果雜湊表中域 field 已經存在且舊值已被新值覆蓋,返回 0 。
例如:HSET user Name Microheart
HSET user Age 18
HMSET:用於同時新增多個
用法:HMSET key [field value field1 value1 ...]
返回值:如果命令執行成功,返回 OK 。
當 key 不是雜湊表(hash)型別時,返回一個錯誤。
例如:HMSET user1 Name Microheart Age 18
HGET:獲取欄位值
用法:HGET key field
返回值:給定域的值。
當給定域不存在或是給定 key 不存在時,返回 nil
例如:HGET user Name (注意 Redis區分大小寫)
HMGET:獲取多個欄位的值
用法:HMGET key [field1,field2]
返回值:一個包含多個給定域的關聯值的表,表值的排列順序和給定域引數的請求順序一樣。
例如:HMGET user Name Age
EXPIRE:設定快取的過期時間
用法:EXPIRE key seconds
返回值:設定成功返回 1 。
當 key 不存在或者不能為 key 設定生存時間時(比如在低於 2.1.3 版本的 Redis 中你嘗試更新 key 的生存時間),返回 0 。
例如:EXPIRE user 60
TTL:表示剩餘生存時間。57表示還有57秒這個快取過期。過期後,Redis會自動刪除。在 Redis 2.4 版本中,過期時間的延遲在 1 秒鐘之內 —— 也即是,就算 key 已經過期,但它還是可能在過期之後一秒鐘之內被訪問到,而在新的 Redis 2.6 版本中,延遲被降低到 1 毫秒之內。
二、在.Net Core中使用Redis元件
首先在Startup類中新增Redis快取功能。配置的Option中設定的InstanceName的值會作為key的一部分。比如設定的InstanceName為test,程式碼中設定一個快取key為user,儲存到Redis中的實際key為testuser。
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddDistributedRedisCache(option => { option.Configuration = "121.41.55.55:6379";//連線字串 option.InstanceName = "test"; }); }
然後在需要使用的地方注入IDistributedCache。如下面所示:
public class ValuesController : Controller { private readonly IDistributedCache redisCache; public ValuesController(IDistributedCache redisCache) { this.redisCache = redisCache; } [HttpGet] public IEnumerable<string> Get() { redisCache.SetAsync("key1", Encoding.UTF8.GetBytes("value1"), new DistributedCacheEntryOptions() { AbsoluteExpiration = DateTime.Now.AddSeconds(10)//設定過期時間,時間一到快取立刻就被移除了 }); redisCache.SetString("key2", "value2");//沒有設定快取過期時間,表示是永久快取 return new string[] { "value1", "value2" }; } }
三、原始碼解析
原始碼在https://github.com/aspnet/Caching,Redis的原始碼相對簡單,主要是因為很多都直接使用的StackExchange.Redis的API。
RedisCacheOptions類:主要是Redis配置相關。
Configuration:設定Redis配置,如連線字串、超時時間等,最終被裝換為StackExchange.Redis中的ConfigurationOptions
InstanceName:例項名稱。會和程式碼中設定的key拼接成為Redis中的key。
RedisCacheServiceCollectionExtensions類:跟服務注入相關。
就一個方法AddDistributedRedisCache,依賴注入IDistributedCache的例項。
public static IServiceCollection AddDistributedRedisCache(this IServiceCollection services, Action<RedisCacheOptions> setupAction) { if (services == null) { throw new ArgumentNullException(nameof(services)); } if (setupAction == null) { throw new ArgumentNullException(nameof(setupAction)); } services.AddOptions(); services.Configure(setupAction); services.Add(ServiceDescriptor.Singleton<IDistributedCache, RedisCache>());//注入一個單例 return services; }
RedisCache類:最主要的類,快取操作相關的類。
其中插入、獲取資料的方法比較重要。
3.1 Set方法,插入資料。
public void Set(string key, byte[] value, DistributedCacheEntryOptions options) { //省略一些邏輯判斷 Connect(); var creationTime = DateTimeOffset.UtcNow; //對於一個快取可以設定為絕對過期時間,相對於現在時間的過期時間和滑動過期時間三種(上一篇文章有例子),其實前兩種時間型別可以相互轉換。 //下面這一步就是 如果設定了絕對過期時間或者相對於現在時間的過期時間,裝換為絕對過期時間 var absoluteExpiration = GetAbsoluteExpiration(creationTime, options); //呼叫了StackExchange.Redis的API 插入快取 var result = _cache.ScriptEvaluate(SetScript, new RedisKey[] { _instance + key },//這裡的key是例項名稱+key=Redis中的key,當然我們在查詢快取的時候,並不需要我們手動拼接,只需要傳我們複製的key,不需要例項名稱 new RedisValue[] { absoluteExpiration?.Ticks ?? NotPresent, options.SlidingExpiration?.Ticks ?? NotPresent, //如果對於一個快取同時設定了絕對過期時間和滑動過期時間,則取即將到期的時間,也就是最小的那個時間。 GetExpirationInSeconds(creationTime, absoluteExpiration, options) ?? NotPresent, value }); }
上面的新增快取中,使用了指令碼插入。 private const string SetScript = (@" redis.call('HMSET', KEYS[1], 'absexp', ARGV[1], 'sldexp', ARGV[2], 'data', ARGV[4])//設定key、絕對過期時間、滑動過期時間、和value的值 if ARGV[3] ~= '-1' then redis.call('EXPIRE', KEYS[1], ARGV[3])//設定快取的時間 end return 1");
如果absexp和sldexp都沒有設定值,預設為-1,表示永不過期,快取時間就是從設定的絕對過期時間和滑動過期時間中取,當時間到了,Redis自動刪除過期快取,這一點和MemoryCache不一樣,MemoryCahe是在對快取操作的時候,會掃描整個快取刪除,存在很大的延時,而Redis採用下面三種策略清理過期的key:
- 被動刪除:當讀/寫一個已經過期的key時,會觸發惰性刪除策略,直接刪除掉這個過期key
- 主動刪除:由於惰性刪除策略無法保證冷資料被及時刪掉,所以Redis會定期主動淘汰一批已過期的key
- 當前已用記憶體超過maxmemory限定時,觸發主動清理策略
這就保證了過期快取的及時清理。關於Redis清理過期key的策略可以看這篇文章。
當插入一條Hash型別資料時,開啟RedisManager會看到下面這樣,absexp:絕對過期時間,sldexp:滑動過期時間,data:就是我們程式碼中設定的value。
3.2 Get方法中,實現的主要獲取功能呼叫了下面程式碼。
internal static class RedisExtensions { private const string HmGetScript = (@"return redis.call('HMGET', KEYS[1], unpack(ARGV))");//通過指令碼HMGET命令獲取key的值 //Get方法中呼叫此方法,memebers為固定值 data,也就是獲取欄位data的值 internal static RedisValue[] HashMemberGet(this IDatabase cache, string key, params string[] members) { var result = cache.ScriptEvaluate( HmGetScript, new RedisKey[] { key }, GetRedisMembers(members)); return (RedisValue[])result; } internal static async Task<RedisValue[]> HashMemberGetAsync( this IDatabase cache, string key, params string[] members) { var result = await cache.ScriptEvaluateAsync( HmGetScript, new RedisKey[] { key }, GetRedisMembers(members)); // TODO: Error checking? return (RedisValue[])result; } private static RedisValue[] GetRedisMembers(params string[] members) { var redisMembers = new RedisValue[members.Length]; for (int i = 0; i < members.Length; i++) { redisMembers[i] = (RedisValue)members[i]; } return redisMembers; } }
Remove方法就直接呼叫了StackExchange的API,這裡就不做解釋。
相比MemoryCache的程式碼,Redis程式碼相對簡單,主要是微軟的開發人員"偷工減料"吧(我自己感覺),很多重要的方法,比如Redis連線、新增、設定過期時間、都呼叫了StackExchange的API,沒有實現自己的連結池等等。更像是對StackExchangeAPI中的Hash型別的再次封裝。