4.3 可配置的分布式缓存(上)
为了加快系统运行效率,一般情况下系统会采用缓存技术,将常用信息存放到缓存中,避免频繁的从数据库、文件中读写,造成系统瓶颈,从而提高响应速度。缓存分为客户端缓存和服务器端缓存。
目前随着系统的扩展,服务器端缓存一般采取两级缓存技术,本地缓存和分布式缓存。部分常用、公共或者小数据量的信息保存在分布式缓存中,运行在不同资源上的系统均从分布式缓存中获取同样的数据。相反,常用、私有或者数据量大的信息则保存在本地缓存中,避免了大数据量信息频繁网络传输、序列化和反序列化造成的系统瓶颈。
大家在使用分布式缓存中需要注意的是,应用在将数据存入分布式缓存或者读取时,数据需要序列化才能存入,读取时反序列化,这样对于大的对象占用的网络/CPU等资源会比较多。
通常情况下,.net core的缓存使用是这样的:
1 public void ConfigureServices(IServiceCollection services) 2 { 3 services.AddMemoryCache(); 4 // Add framework services. 5 services.AddMvc(); 6 } 7 8 public class HomeController : Controller 9 { 10 private IMemoryCache _memoryCache; 11 public HomeController(IMemoryCache memoryCache) 12 { 13 _memoryCache = memoryCache; 14 } 15 16 public IActionResult Index() 17 { 18 string cacheKey = "key"; 19 string result; 20 if (!_memoryCache.TryGetValue(cacheKey, out result)) 21 { 22 result = DateTime.Now.ToString(); 23 _memoryCache.Set(cacheKey, result); 24 } 25 ViewBag.Cache = result; 26 return View(); 27 } 28 }
第一段是在startup.cs中注册缓存,第二段是具体在某个controller中的使用。
对于分布式缓存来说,以Redis为例则需要写成这样:
1 public void ConfigureServices(IServiceCollection services) 2 { 3 services.AddDistributedRedisCache(options => 4 { 5 options.Configuration = "localhost"; 6 options.InstanceName = "SampleInstance"; 7 }); 8 } 9 10 public class HomeController : Controller 11 { 12 private IDistributedCache _memoryCache; 13 public HomeController(IDistributedCache memoryCache) 14 { 15 _memoryCache = memoryCache; 16 } 17 18 public IActionResult Index() 19 { 20 string cacheKey = "key"; 21 string result; 22 if (!_memoryCache.TryGetValue(cacheKey, out result)) 23 { 24 result = DateTime.Now.ToString(); 25 _memoryCache.Set(cacheKey, result); 26 } 27 ViewBag.Cache = result; 28 return View(); 29 } 30 }
然而,这种分布式缓存编程方式在大型可扩展的系统中存在一些问题。一是在不同的环境中可能缓存产品不同,例如阿里云是memcached,微软是appfabirc,我们本地集群的是redis等。不能每次部署修改一遍程序吧?这违反了可替换、可配置的软件质量原则了。二是IDistributedCache接口方法用起来比较繁琐,对于过期时间等还得自行编写DistributedCacheEntryOptions。其实过期时间一般都是滑动时间,应该可以将这个时间直接写在配置文件中,而不要每个缓存自己写。
为此,对分布式缓存的封装首先是建立更好的使用接口,其次是可配置化。接口和抽象类的代码如下:
1 public interface ICacheHander 2 { 3 /// <summary> 4 /// 如果不存在缓存项则添加,否则更新 5 /// </summary> 6 /// <param name="catalog">缓存分类</param> 7 /// <param name="key">缓存项标识</param> 8 /// <param name="value">缓存项</param> 9 void Put<T>(string catalog, string key, T value); 10 11 /// <summary> 12 /// 如果不存在缓存项则添加,否则更新 13 /// </summary> 14 /// <param name="catalog">缓存分类</param> 15 /// <param name="key">缓存项标识</param> 16 /// <param name="value">缓存项</param> 17 /// <param name="timeSpan">缓存时间,滑动过期</param> 18 void Put<T>(string catalog, string key, T value, TimeSpan timeSpan); 19 20 /// <summary> 21 /// 获取缓存项 22 /// </summary> 23 /// <param name="catalog">缓存分类</param> 24 /// <param name="key">cacheKey</param> 25 T Get<T>(string catalog, string key); 26 27 /// <summary> 28 /// 获取缓存项,如果不存在则使用方法获取数据并加入缓存 29 /// </summary> 30 /// <typeparam name="T"></typeparam> 31 /// <param name="catalog">缓存分类</param> 32 /// <param name="key">缓存项标识</param> 33 /// <param name="func">获取要缓存的数据的方法</param> 34 /// <returns>缓存的数据结果</returns> 35 T GetOrAdd<T>(string catalog, string key, Func<T> func); 36 37 /// <summary> 38 /// 获取缓存项,如果不存在则使用方法获取数据并加入缓存 39 /// </summary> 40 /// <typeparam name="T"></typeparam> 41 /// <param name="catalog">缓存分类</param> 42 /// <param name="key">缓存项标识</param> 43 /// <param name="func">获取要缓存的数据的方法</param> 44 /// <param name="timeSpan">缓存时间,滑动过期</param> 45 /// <returns>缓存的数据结果</returns> 46 T GetOrAdd<T>(string catalog, string key, Func<T> func, TimeSpan timeSpan); 47 48 /// <summary> 49 /// 移除缓存项 50 /// </summary> 51 /// <param name="catalog">缓存分类</param> 52 /// <param name="key">cacheKey</param> 53 void Remove(string catalog, string key); 54 55 /// <summary> 56 /// 刷新缓存项 57 /// </summary> 58 /// <param name="catalog">缓存分类</param> 59 /// <param name="key">cacheKey</param> 60 void Refresh(string catalog, string key); 61 } 62 63 64 public abstract class BaseCacheHandler : ICacheHander 65 { 66 protected abstract IDistributedCache _Cache { get; } 67 protected CachingConfigInfo _ConfigInfo { get; private set; } 68 69 private TimeSpan _DefaultTimeSpan; 70 71 private ConcurrentDictionary<string, object> _LockObjsDic; 72 73 public BaseCacheHandler(CachingConfigInfo configInfo) 74 { 75 this._DefaultTimeSpan = new TimeSpan(0, configInfo.DefaultSlidingTime, 0); 76 this._LockObjsDic = new ConcurrentDictionary<string, object>(); 77 78 this._ConfigInfo = configInfo; 79 } 80 81 public void Put<T>(string catalog, string key, T value) => Put(catalog, key, value, _DefaultTimeSpan); 82 83 public virtual void Put<T>(string catalog, string key, T value, TimeSpan timeSpan) 84 { 85 string cacheKey = GenCacheKey(catalog, key); 86 87 string str = SerializerHelper.ToJson<T>(value); 88 89 _Cache.SetString(cacheKey, str, new DistributedCacheEntryOptions().SetSlidingExpiration(timeSpan)); 90 } 91 92 public T Get<T>(string catalog, string key) 93 { 94 MicroStrutLibraryExceptionHelper.TrueThrow(string.IsNullOrWhiteSpace(catalog) || string.IsNullOrWhiteSpace(key), this.GetType().FullName, LogLevel.Error, "缓存分类或者标识不能为空"); 95 96 string cacheKey = GenCacheKey(catalog, key); 97 98 string str = _Cache.GetString(cacheKey); 99 100 return SerializerHelper.FromJson<T>(str); 101 } 102 103 public T GetOrAdd<T>(string catalog, string key, Func<T> func) => GetOrAdd(catalog, key, func, _DefaultTimeSpan); 104 105 public T GetOrAdd<T>(string catalog, string key, Func<T> func, TimeSpan timeSpan) 106 { 107 MicroStrutLibraryExceptionHelper.TrueThrow(string.IsNullOrWhiteSpace(catalog) || string.IsNullOrWhiteSpace(key), this.GetType().FullName, LogLevel.Error, "缓存分类或者标识不能为空"); 108 109 T result = Get<T>(catalog, key); 110 111 if (result == null) 112 { 113 string cacheKey = GenCacheKey(catalog, key); 114 115 object lockObj = _LockObjsDic.GetOrAdd(cacheKey, n => new object()); 116 lock (lockObj) 117 { 118 result = Get<T>(catalog, key); 119 120 if (result == null) 121 { 122 result = func(); 123 Put(catalog, key, result, timeSpan); 124 } 125 } 126 } 127 128 if (result == null) 129 return default(T); 130 131 return result; 132 } 133 134 /// <summary> 135 /// 删除缓存 136 /// </summary> 137 /// <param name="catalog"></param> 138 /// <param name="key"></param> 139 public void Remove(string catalog, string key) 140 { 141 string cacheKey = GenCacheKey(catalog, key); 142 143 _Cache.Remove(cacheKey); 144 } 145 146 /// <summary> 147 /// 清空缓存 148 /// </summary> 149 public void Refresh(string catalog, string key) 150 { 151 string cacheKey = GenCacheKey(catalog, key); 152 153 _Cache.Refresh(cacheKey); 154 } 155 156 /// <summary> 157 /// 生成缓存键 158 /// </summary> 159 /// <param name="catalog"></param> 160 /// <param name="key"></param> 161 /// <returns></returns> 162 private string GenCacheKey(string catalog, string key) 163 { 164 return $"{catalog}-{key}"; 165 } 166 }
抽象类又对接口进行了进一步的实现。大家可以注意下缓存Get时我们使用了两次lock,还有GetOrAdd方法,我们用了一个func。
缓存配置信息的代码,配置基类和配置的介绍请见:多级可换源的配置实现。
1 public sealed class CachingConfigInfo : ConfigInfo 2 { 3 /// <summary> 4 /// 缓存滑动窗口时间(分钟) 5 /// </summary> 6 public int DefaultSlidingTime { get; set; } 7 8 /// <summary> 9 /// 分布式缓存类型TypeDescription,例如是Redis/Memcached/Default等分布式缓存对应的实现类信息。 10 /// </summary> 11 public string Type { get; set; } 12 13 /// <summary> 14 /// 缓存的参数,一般是服务器信息 15 /// </summary> 16 public List<CacheServer> Servers { get; set; } 17 18 public override string SectionName 19 { 20 get 21 { 22 return "MicroStrutLibrary:Caching"; 23 } 24 } 25 26 public override void RegisterOptions(IServiceCollection services, IConfigurationRoot root) 27 { 28 services.Configure<CachingConfigInfo>(root.GetSection(SectionName)); 29 } 30 } 31 32 /// <summary> 33 /// 分布式缓存 服务器配置项 34 /// </summary> 35 public sealed class CacheServer 36 { 37 /// <summary> 38 /// 服务器地址,可以使服务器网络名称也可是IP地址 39 /// </summary> 40 public string HostName { get; set; } 41 42 /// <summary> 43 /// 服务器分布式缓存服务提供端口 44 /// </summary> 45 public int Port { get; set; } 46 }
其中Type属性就是具体的实现,例如Redis分布式缓存的实现等。具体请见下节。
接下来需要在startup注册使用分布式缓存处理
1 public static IServiceCollection AddCacheHandler(this IServiceCollection services) 2 { 3 if (services == null) 4 { 5 throw new ArgumentNullException(nameof(services)); 6 } 7 8 IOptions<CachingConfigInfo> optionsAccessor = services.BuildServiceProvider().GetService<IOptions<CachingConfigInfo>>(); 9 10 ICacheHander handler = ReflectionHelper.CreateInstance(optionsAccessor.Value.Type, optionsAccessor.Value) as ICacheHander; 11 12 services.TryAddSingleton<ICacheHander>(handler); 13 14 return services; 15 }
在使用分布式缓存时,只需要在方法或者调用类的构造函数上增加ICacheHandler cacheHandler的属性即可。例如ParameterService 类的构造函数public ParameterService(ICacheHandler cacheHandler) {…}。