.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

posted @ 2023-06-02 18:44  dreamw  阅读(170)  评论(0编辑  收藏  举报