.NET Core WebAPI 认证授权之JWT
@@.NET Core WebAPI 认证授权之JWT --google
from --->NET Core WebAPI 认证授权之JWT(二)
在上一篇 《.NET缓存系列(一):缓存入门》中实现了基本的缓存,接下来需要对缓存进行改进,解决一些存在的问题。
一、缓存过期策略
问 题:
当源数据更改或删除时,服务器程序并不知道,导致缓存中存在脏数据,如何避免?
解决方案:
①让数据只能通过缓存所在的程序进行更改或删除(禁止数据源通过其他方式更改)②缓存程序对外提供接口,当数据源更改或删除时,调用接口告知③容忍脏数据,指定时间后缓存过期
方案1、2常使用,可以说不太现实,所以通常会给缓存添加过期策略。
1.实现三种过期策略
首先,我们需要做一些基础准备,新建一个缓存实体类和一个过期策略枚举:
class CacheModel{ /// <summary> /// 缓存值 /// </summary> public object Value { get; set; } /// <summary> /// 过期类型 /// </summary> public ExpireType ExpireType { get; set; } /// <summary> /// 过期时间 /// </summary> public DateTime DeadLine { get; set; } /// <summary> /// 滑动时间段 /// </summary> public TimeSpan Duration { get; set; }}enum ExpireType{ /// <summary> /// 永不过期 /// </summary> Nerver, /// <summary> /// 绝对过期 /// </summary> Absolutely, /// <summary> /// 滑动过期 /// </summary> Relative}
其次,对添加数据的方法进行改造,指定过期策略(为了方便调用,这里使用方法重载)。
①永不过期
永不过期不需要添加任何参数,方法签名不变:
public static void AddWithExpire<T>(string key, T value){ Cache[key] = new CacheModel { Value = value, ExpireType = ExpireType.Nerver };}
缓存的对象不再是value,而是CacheModel对象,Value就是原本的缓存值,然后指定过期类型为永不过期。
②绝对过期(指定时间后过期)
public static void AddWithExpire<T>(string key, T value, int timeoutSecend){ Cache[key] = new CacheModel { Value = value, ExpireType = ExpireType.Absolutely, DeadLine = DateTime.Now.AddSeconds(timeoutSecend) };}
绝对过期增加了一个int类型参数,该参数表示缓存的数据经过多少秒过期。
③滑动过期(指定时间后过期,但在有效期内如果使用缓存,则会刷新过期时间)
public static void AddWithExpire<T>(string key, T value, TimeSpan timespan){ Cache[key] = new CacheModel { Value = value, ExpireType = ExpireType.Relative, DeadLine = DateTime.Now.Add(timespan), Duration = timespan };}
滑动过期增加了一个TimeSpan,这个参数表示缓存的时间,同时还表示滑的时间。
然后,需要在获取缓存中的数据时,进行是否过期的判断:
//获取缓存中的数据
public static T GetWithExpire<T>(string key, Func<T> func) { T res; if (Exist(key)) { res = (T)Cache[key]; } else { res = func.Invoke(); Cache[key] = res; } return res; } //缓存中是否存在数据 包含有效期筛 public static bool Exist(string key) { if (!Cache.ContainsKey(key)) return false; var res = false; var cacheModel = Cache[key] as CacheModel; switch (cacheModel.ExpireType) { case ExpireType.Nerver: res = true; break; case ExpireType.Absolutely: res = cacheModel.DeadLine > DateTime.Now; break; case ExpireType.Relative: if (cacheModel.DeadLine > DateTime.Now) { cacheModel.DeadLine.Add(cacheModel.Duration); res = true; } else res = false; break; } return res; } 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.
这里对获取缓存数据的方法进行了小小的改进,使用了委托。当缓存中没有数据时,在委托中将数据查询出来并返回,同时将数据保存在缓存中。
在Exist()方法中,先获取Key对应的缓存对象,然后根据对象的ExpireType属性,对相应的过期策略进行判断,如果数据有效,则返回true,否则返回false。
2.被动清理过期数据
缓存策略是添加了,但是过期的数据依然存在于缓存中,如何进行清理?
switch (cacheModel.ExpireType){ case ExpireType.Nerver: res = true; break; case ExpireType.Absolutely: res = cacheModel.DeadLine > DateTime.Now; break; case ExpireType.Relative: if (cacheModel.DeadLine > DateTime.Now) { cacheModel.DeadLine.Add(cacheModel.Duration); res = true; } else res = false; break;}if (!res) Cache.Remove(key); //无效的数据移除
将Exist()方法进行小小的改动即可,当每次调用Exist()方法时,判断如果数据无效,则移除。这种方法需要每次获取对应数据时才会去判断是否过期,然后清除,只达到了被动清理的效果。
3.主动清理过期数据
只有被动清理时,如果一直没有去获取相应的数据,那么它还是会一直存在缓存中。
所以我们需要主动去清理过期缓存:
static CustomCache()
{
#region 主动清理过期缓存
Task.Run(() =>
{
while (true)
{
Thread.Sleep(1000 * 60 * 10); //每10分钟清理
List<string> removeList = new List<string>();
foreach (var key in Cache.Keys)
{
var cacheModel = Cache[key] as CacheModel;
if (cacheModel.ExpireType != ExpireType.Nerver && cacheModel.DeadLine < DateTime.Now)
{
//不能在集合遍历时删除集合项
//Cache.Remove(key);
removeList.Add(key);
}
}
foreach (var key in removeList)
Cache.Remove(key);
}
});
#endregion
}
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
添加一个静态构造函数,在里面使用单独的一个线程,每过一定时间,遍历缓存中所有项,判断过期是否过期,过期则清除。这样就达到了一个主动清理的效果。
二、线程安全缓存
1.线程安全问题
模拟多线程访问缓存:
//模拟多线程var taskList = new List<Task>();for (int i = 0; i < 1000; i++){ var key = $"{i}_key"; taskList.Add(Task.Run(() => { CustomCache.AddWithExpire(key, "你好", timeoutSecend: 10); }));}Task.WaitAll(taskList.ToArray()); //同时执行1000个线程,执行添加数据操作
将主动清理缓存中Thread.Sleep()去掉,方便测试:
可以看到,在catch中抛出一个异常:集合已经更改,枚举操作无法执行。也就是说,在遍历集合的同时,又有多个线程同时往集合中添加数据,导致异常。
2.解决线程安全问题
①线程安全集合
将存储缓存数据的集合由Dictionary更改为ConcurrentDictionary,这是一个线程安全的集合,然后将相关方法修改一下即可。
②使用锁
定义一个锁对象,在所有对缓存进行增、删、改、遍历的地方加上锁,能够有效解决线程安全问题。
public static object LockObj { get; set; } = new object(); //锁对象
public static void AddWithExpire<T>(string key, T value, int timeoutSecend)
{
lock (LockObj) //添加时加锁
{
Cache[key] = new CacheOption
{
Value = value,
ExpireType = ExpireType.Absolutely,
DeadLine = DateTime.Now.AddSeconds(timeoutSecend)
};
}
}
//Thread.Sleep(1000 * 60 * 10); //每10分钟清理
List<string> removeList = new List<string>();
lock (LockObj) //清理时加锁
{
foreach (var key in Cache.Keys)
{
var cacheModel = Cache[key] as CacheOption;
if (cacheModel.ExpireType != ExpireType.Nerver && cacheModel.DeadLine < DateTime.Now)
{
//不能在集合遍历时删除集合项
//Cache.Remove(key);
removeList.Add(key);
}
}
foreach (var key in removeList)
{
Cache.Remove(key);
}
}
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
③锁 + 数据分片
数据分片,顾名思义,就是将缓存分片存储。然后各自加锁,减少锁的使用,提高性能:
/// <summary>/// 缓存集合分片列表/// </summary>public static List<Dictionary<string, object>> Cache = new List<Dictionary<string, object>>();/// <summary>/// 每个缓存片区对应的锁/// </summary>private static List<object> LockList = new List<object>();/// <summary>/// 缓存片区数量/// </summary>private static int areaCount = 0;
在构造函数中根据片区数量初始化缓存集合和对应的锁:
areaCount = 3;//设置缓存片区数量
for (int i = 0; i < areaCount; i++)
{
Cache.Add(new Dictionary<string, object>());
LockList.Add(new object());
}
1.
1.
1.
1.
1.
1.
1.
添加数据方法中,根据key的hashcode来分配缓存片区:
public static void Add<T>(string key, T value, int timeoutSecend)
{
var index = Math.Abs(key.GetHashCode()) % areaCount; //根据key的HashCode,分配均匀
lock (LockList[index]) //锁对应片区数据
{
Cache[index][key] = new CacheOption
{
Value = value,
ExpireType = ExpireType.Absolutely,
DeadLine = DateTime.Now.AddSeconds(timeoutSecend)
}; //添加到对应片区
}
}
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
获取方法和清理方法等都可以通过key的hashcode找到对应数据所在的片区,然后进行操作,整体逻辑相同,就懒得写了。
3.性能对比
普通加锁和数据分片性能对比:
{
Console.WriteLine("------------普通加锁------------");
Stopwatch sw = new Stopwatch();
sw.Start();
//模拟多线程
var taskList = new List<Task>();
for (int i = 0; i < 100_000; i++)
{
var key = $"{i}_key";
taskList.Add(Task.Run(() =>
{
CustomCache.AddWithExpire(key, "你好", timeoutSecend: 10);
}));
}
Task.WaitAll(taskList.ToArray());
sw.Stop();
Console.WriteLine($"耗时:{sw.ElapsedMilliseconds}");
}
{
Console.WriteLine("------------数据分片------------");
Stopwatch sw = new Stopwatch();
sw.Start();
var taskList = new List<Task>();
for (int i = 0; i < 100_000; i++)
{
var key = $"{i}_key";
taskList.Add(Task.Run(() =>
{
CustomCacheSlicing.Add(key, "你好", timeoutSecend: 10);
}));
}
Task.WaitAll(taskList.ToArray());
sw.Stop();
Console.WriteLine($"耗时:{sw.ElapsedMilliseconds}");
}
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
1.
同时添加10万条数据,运行结果:
运行多次,数据分片耗时始终要低于普通加锁。
添加了过期策略,解决了线程安全问题,一个简单的缓存类封装完毕!
-----------------------------------
©著作权归作者所有:来自51CTO博客作者mob604756f953bb的原创作品,请联系作者获取转载授权,否则将追究法律责任
.NET缓存系列(二):缓存进阶
https://blog.51cto.com/u_15127615/2755488