Cache缓存
什么是缓存
缓存:实际上是一种效果&目标,就是获取数据的时候,第一次获取之后找个地方存起来,后面直接用,这样一来可以提升后面每次获取数据的效率;像我们在代码中使用的静态字段;都是缓存; 缓存无处不在!
一般来说:系统性能优化的第一步就是使用缓存!效果立竿见影!
什么情况下使用缓存?
二八原则 查询频繁;修改少!
客户端缓存
缓存其实只能是服务器指定的! 只要在服务器里面指定以后,浏览器就可以自动缓存起来!
上图大致使用缓存的情况下的一个流程,这里面有一个很重要问题:缓存协商
缓存协商
将一段数据进行缓存之后,但是如果再次请求的缓存的数据已经不是最新的话就会造成展示的数据有误,所以这就涉及到了缓存协商。
协商缓存会发起请求,以验证本地副本是否可以使用。也称为再验证的过程。
· 如果验证后显示文档内容变化,浏览器会获取一份新的副本,替代旧副本,缓存在本地
· 如果验证后显示文档内容无变化,浏览器只用获取新的首部和新的过期日期
Etag是属于HTTP 1.1属性,它是由服务器生成返回给前端, 当你第一次发起HTTP请求时,服务器会返回一个Etag,并在你第二次发起同一个请求时,客户端会同时发送一个If-None-Match,而它的值就是Etag的值(此处由发起请求的客户端来设置)。 然后,服务器会比对这个客服端发送过来的Etag是否与服务器的相同, 如果数据源有更新,那么在更新缓存中的数据的时候Etag的数据也会变化,如果相同,就将If-None-Match的值设为false,返回状态为304,客户端继续使用本地缓存,不解析服务器返回的数据(这种场景服务器也不返回数据,因为服务器的数据没有变化嘛) 如果不相同,就将If-None-Match的值设为true,返回状态为200,客户端重新解析服务器返回的数据 说白了, ETag 实体标签: 一般为资源实体的哈希值 ,即ETag就是服务器生成的一个标记,用来标识返回值是否有变化。且Etag的优先级高于Last-Modified。
浏览器请求页面时Etag和cache的区别
etag:简单的说就是服务器收到客户端的请求后,根据返回的内容计算出来一个etag值,返回给客户端,当客户端下次再请求相同路径的时候会带上之前的etag值,服务器端会根据这次请求应该返回的内容计算出新的etag值,如果内容没有变化的话,etag值应该也不会改变,如果etag值没有改变,那么直接返回304,本次请求本应该返回的内容就无需再次返回给客户端了,客户端收到304则直接使用本地的缓存数据.由于这次返回的时候并没有返回实际的数据,所以节省了大量的网络带宽,但是仅对GET方式有效.
cache:第一次请求后会把返回的数据存入本地缓存中,下次请求同内容时,如果启用了cache,那么首先会尝试从cache中获取内容,cache中没有时才会向服务器端请求数据.
区别:这样区别也很清楚了,cache主要是按照请求连接和是否启用cache来判断是否优先从本地cache中获取数据,但是由于有些时候请求链接虽然不变,但是返回的数据可能会变化,这样如果启用cache的话就无法获取到真实的数据了,而如果简单的禁用cache,返回的数据又可能和上次没有任何变化,白白浪费网络带宽,此时就是etag的意义所在.
代码实现
首先我们必须要明确的是如果符合下面两条规则就可以使用缓存:
1.会重复调用方法获取数据
2.只要传入的参数不变,每次调用方法获取的数据都不变!如果传入的参数不变但是返回值经常变也没必要做缓存
我们看下面几个查询方法
模拟数据库查询:
/// <summary> ///模拟数据库查询 /// </summary> public class DBHelper { /// <summary> /// 1 耗时耗资源 /// 2 参数固定时,结果不变 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="index"></param> /// <returns></returns> public static List<T> Query<T>(int index) { Console.WriteLine("This is {0} Query", typeof(DBHelper)); long lResult = 0; //耗时耗资源 for (int i = index; i < 1_000_000_000; i++) { lResult += i; } ///只要Index不变 返回值是不变的! List<T> tList = new List<T>(); for (int i = 0; i < index % 3; i++) { tList.Add(default(T)); } return tList; } }
模拟读硬盘文件
/// <summary> /// 模拟读硬盘文件 /// </summary> public class FileHelper { public static List<T> Query<T>(int index) { Console.WriteLine("This is {0} Query", typeof(FileHelper)); long lResult = 0; for (int i = index; i < 1_000_000_000; i++) { lResult += i; } ///只要Index不变 返回值是不变的! List<T> tList = new List<T>(); for (int i = 0; i < index % 3; i++) { tList.Add(default(T)); } return tList; } }
模拟远程接口
/// <summary> /// 模拟远程接口 /// </summary> public class RemoteHelper { public static List<T> Query<T>(int index) { Console.WriteLine("This is {0} Query", typeof(RemoteHelper)); long lResult = 0; for (int i = index; i < 1_000_000_000; i++) { lResult += i; } List<T> tList = new List<T>(); for (int i = 0; i < index % 3; i++) { tList.Add(default(T)); } return tList; } }
我们可以看到上述做的模拟的数据库查询,磁盘文件查询和远程接口查询的代码都符合传入的参数不变的话得到的结果也不变,所以我们可以用缓存来提高读取速度。
自定义静态字典缓存(放到内存中)
注意:我们在定义key值的时候最好用业务前缀+参数,或者其它能唯一确定的值,因为在不同的查询方法中查询的参数可能是一样的,这可能会导致存储的数据被替换或者破坏。
CustomCache:
public class CustomCache { /// <summary> /// static:不会被Gc回收; /// Private:不让外部访问他 /// </summary> private static Dictionary<string, object> CustomCacheDictionary = new Dictionary<string, object>(); public static void Add(string key, object value) { CustomCacheDictionary.Add(key, value); } public static T Get<T>(string key) { return (T)CustomCacheDictionary[key]; } public static bool Exists(string key) { return CustomCacheDictionary.ContainsKey(key); } /// <summary> /// 泛型+委托 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <param name="func"></param> /// <returns></returns> public static T GetT<T>(string key, Func<T> func) { T t = default(T); if (!Exists(key)) { t = func.Invoke(); Add(key, t); } else { t = Get<T>(key); } return t; } }
调用:
static void Main(string[] args) { try { { //1.会重复调用方法获取数据 //2.只要传入的参数不变,每次调用方法获取的数据都不变!如果传入的参数不变但是返回值经常变也没必要做缓存 //以上两个特点就非常适合使用缓存! Console.WriteLine("***************DBHelper*****************"); for (int i = 0; i < 5; i++) //会重复查询数据 { Console.WriteLine($"获取{nameof(DBHelper)} {i}次 {DateTime.Now.ToString("yyyyMMdd HHmmss.fff")}"); List<Program> programList = null; string key = $"{nameof(Program)}_DBHelper.Query_{123}"; //if (!CustomCache.Exists(key)) //{ // programList = DBHelper.Query<Program>(123); // CustomCache.Add(key, programList); //} //else //{ // programList = CustomCache.Get<List<Program>>(key); //} //委托:去掉重复代码 programList = CustomCache.GetT<List<Program>>(key, () => { return DBHelper.Query<Program>(123); }); //List<Program> programList = DBHelper.Query<Program>(123); } Console.WriteLine("*************FileHelper*******************"); for (int i = 0; i < 5; i++) { Console.WriteLine($"获取{nameof(FileHelper)} {i}次 {DateTime.Now.ToString("yyyyMMdd HHmmss.fff")}"); List<Program> programList = null; string key = $"{nameof(Program)}_FileHelper.Query_{234}"; //if (!CustomCache.Exists(key)) //{ // programList = FileHelper.Query<Program>(234); // CustomCache.Add(key, programList); //} //else //{ // programList = CustomCache.Get<List<Program>>(key); //} programList = CustomCache.GetT<List<Program>>(key, () => { return FileHelper.Query<Program>(234); }); // List<Program> programList = FileHelper.Query<Program>(234); //委托:去掉重复代码 } Console.WriteLine("****************RemoteHelper****************"); for (int i = 0; i < 5; i++) { Console.WriteLine($"获取{nameof(RemoteHelper)} {i}次 {DateTime.Now.ToString("yyyyMMdd HHmmss.fff")}"); List<Program> programList = null; string key = $"{nameof(Program)}_RemoteHelper.Query_{345}"; //if (!CustomCache.Exists(key)) //{ // programList = RemoteHelper.Query<Program>(345); // CustomCache.Add(key, programList); //} //else //{ // programList = CustomCache.Get<List<Program>>(key); //} programList = CustomCache.GetT<List<Program>>(key, () => { return RemoteHelper.Query<Program>(345); }); //List<Program> programList = RemoteHelper.Query<Program>(345); } } } catch (Exception ex) { Console.WriteLine(ex.Message); } Console.Read(); }
这样的话参数一样的情况下只需要去内存中取缓存的数据就可以,速度提高很多,当然前提是忽略数据过大放到内存中带来的其它问题。
浏览器缓存、DNS缓存、CDN缓存
参考:
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 字符编码:从基础到乱码解决
· 提示词工程——AI应用必不可少的技术