浅谈缓存写法:内存缓存该如何设计(代码片段)

author author     2022-12-02     355

关键词:

分析设计

假设有个项目有比较高的并发量,要用到多级缓存,如下:

技术图片

在实际设计一个内存缓存前,需要考虑的问题:

1:内存与Redis的数据置换,尽可能在内存中提高数据命中率,减少下一级的压力。

2:内存容量的限制,需要控制缓存数量。

3:热点数据更新不同,需要可配置单个key过期时间。

4:良好的缓存过期删除策略。

5:缓存数据结构的复杂度尽可能的低。

关于置换及命中率:采用LRU算法,因为它实现简单,缓存key命中率也很好。

LRU即是:把最近最少访问的数据给淘汰掉,经常被访问到即是热点数据。

关于LRU数据结构:因为key优先级提升和key淘汰,所以需要顺序结构,网上大多实现都采用的这种链表结构。

即新数据插入到链表头部、被命中时的数据移动到头部,添加复杂度O(1),移动和获取复杂度O(N)。

有没复杂度更低的呢? 有Dictionary,复杂度为O(1),性能最好。 那如何保证缓存的优先级提升呢?

O(1)LRU实现

定义个LRUCache<TValue>类,构造参数maxKeySize 来控制缓存最大数量。

使用ConcurrentDictionary来作为我们的缓存容器,并能保证线程安全。

<pre style="margin:0px;
    padding:0px;
    white-space:pre-wrap;
    overflow-wrap:break-word;
    font-family:&quot;
    Courier New&quot;
    !important;
    font-size:12px !important;
    "> public class LRUCache<TValue>:IEnumerable<KeyValuePair<string,TValue>> 
    private long ageToDiscard = 0;
    //淘汰的年龄起点
private long currentAge = 0;
    //当前缓存最新年龄
private int maxSize = 0;
    //缓存最大容量
private readonly ConcurrentDictionary<string,TrackValue> cache;
    public LRUCache(int maxKeySize) 
    cache = new ConcurrentDictionary<string,TrackValue>();
    maxSize = maxKeySize;


</pre>

上面定义了?ageToDiscard、currentAge?这2个自增值参数,作用是标记缓存列表中各个key的新旧程度。

实现步骤如下:

每次添加key时,currentAge自增并将currentAge值分配给这个缓存值的age,currentAge一直自增。

<pre style="margin:0px;
    padding:0px;
    white-space:pre-wrap;
    overflow-wrap:break-word;
    font-family:&quot;
    Courier New&quot;
    !important;
    font-size:12px !important;
    "> public void Add(string key,TValue value) 
    Adjust(key);
    var result = new TrackValue(this,value);
    cache.AddOrUpdate(key,result,(k,o) => result);

public class TrackValue 
    public readonly TValue Value;
    public long Age;
    public TrackValue(LRUCache<TValue> lv,TValue tv) 
    Age = Interlocked.Increment(ref lv.currentAge);
    Value = tv;


</pre>

在添加时,如超过最大数量,检查字典里是否有ageToDiscard年龄的key,如没有循环自增检查,有则删除、添加成功。

其ageToDiscard+maxSize=?currentAge?,这样设计就能在O(1)下保证可以淘汰旧数据,而不是使用链表移动。?

<pre style="margin:0px;
    padding:0px;
    white-space:pre-wrap;
    overflow-wrap:break-word;
    font-family:&quot;
    Courier New&quot;
    !important;
    font-size:12px !important;
    ">  public void Adjust(string key) 
    while (cache.Count >= maxSize) 
    long ageToDelete = Interlocked.Increment(ref ageToDiscard);
    var toDiscard = cache.FirstOrDefault(p => p.Value.Age == ageToDelete);
    if (toDiscard.Key == null) continue;
    TrackValue old;
    cache.TryRemove(toDiscard.Key,out old);

</pre>

获取key的时候表示它又被人访问,将最新的currentAge赋值给它,增加它的年龄:

<pre style="margin:0px;
    padding:0px;
    white-space:pre-wrap;
    overflow-wrap:break-word;
    font-family:&quot;
    Courier New&quot;
    !important;
    font-size:12px !important;
    ">  public TValue Get(string key) 
    TrackValue value=null;
    if (cache.TryGetValue(key,out value)) 
    value.Age = Interlocked.Increment(ref currentAge);

return value.Value;
</pre>

过期删除策略

大多数情况下,LRU算法对热点数据命中率是很高的。 但如果突然大量偶发性的数据访问,会让内存中存放大量冷数据,也即是缓存污染。

会引起LRU无法命中热点数据,导致缓存系统命中率急剧下降,也可以使用LRU-K、2Q、MQ等变种算法来提高命中率。

过期配置

通过设定最大过期时间来尽量避免冷数据常驻内存。

多数情况每个数据缓存的时间要求不一致的,所以需要再增加单个key的过期时间字段。

<pre style="margin:0px;
    padding:0px;
    white-space:pre-wrap;
    overflow-wrap:break-word;
    font-family:&quot;
    Courier New&quot;
    !important;
    font-size:12px !important;
    "> private TimeSpan maxTime;
    public LRUCache(int maxKeySize,TimeSpan maxExpireTime) 
    //TrackValue增加创建时间和过期时间
public readonly DateTime CreateTime;
    public readonly TimeSpan ExpireTime;
    </pre>

删除策略

关于key过期删除,最好的方式是使用定时删除,这样可以最快的释放被占用的内存,但很明显大量的定时器对CPU来说是非常不友好的。

所以需要采用惰性删除、在获取key的时检查是否过期,过期直接删除。

<pre style="margin:0px;
    padding:0px;
    white-space:pre-wrap;
    overflow-wrap:break-word;
    font-family:&quot;
    Courier New&quot;
    !important;
    font-size:12px !important;
    ">public Tuple<TrackValue,bool> CheckExpire(string key) 
    TrackValue result;
    if (cache.TryGetValue(key,out result)) 
    var age = DateTime.Now.Subtract(result.CreateTime);
    if (age >= maxTime || age >= result.ExpireTime) 
    TrackValue old;
    cache.TryRemove(key,out old);
    return Tuple.Create(default(TrackValue),false);

return Tuple.Create(result,true);
</pre>

惰性删除虽然性能最好,但对于冷数据来说还是没解决缓存污染的问题,所以还需增加个定期清理和惰性删除配合使用。

比如单开个线程每5分钟去遍历检查key是否过期,这个时间策略是可配置的,如果缓存数量较多可分批遍历检查。

<pre style="margin:0px;
    padding:0px;
    white-space:pre-wrap;
    overflow-wrap:break-word;
    font-family:&quot;
    Courier New&quot;
    !important;
    font-size:12px !important;
    ">public void Inspection() 
    foreach (var item in this) 
    CheckExpire(item.Key);


</pre>

惰性删除配合定期删除基本上能满足绝大多数要求了。

总结

本篇参考了redis、Orleans的相关实现。

如果继续完善下去就是内存数据库的雏形,类似redis,比如增加删除key的通知回调,支持更多的数据类型存储。

浅谈分布式缓存解决方案(代码片段)

点击关注公众号,实用技术文章及时了解接口高并发的解决思路:加缓存数据静态化集群分布式同步转异步限流、降级适合加缓存的场景:读多写少的数据,不经常需要修改的数据、一致性要求不高(数据只... 查看详情

架构设计|缓存管理模式,监控和内存回收策略(代码片段)

本文源码:GitHub·点这里||GitEE·点这里一、缓存设计1、缓存的作用在业务系统中,查询时最容易出现性能问题的模块,查询面对的数据量大,筛选条件复杂,所以在系统架构中引入缓存层,则是非常必要的,用来缓存热点数据,... 查看详情

浅谈缓存最终一致性的解决方案(代码片段)

作者:clareguo,腾讯CSIG后台开发工程师到底是更新缓存还是删除缓存? 到底是先更新数据库,再删除缓存,还是先删除缓存,再更新数据库?或采用延迟队列。显而易见的是,无论这个值如何预估,都很难和读请求的完成时间... 查看详情

浅谈mysqlinnodb的内存组件(代码片段)

前言MySQL中执行一条SQL语句,相应表数据的读写都是由存储引擎去做(更新数据、查询数据)。在这个过程,存储引擎需要决策一些事情数据是从内存查还是从硬盘查数据是更新在内存,还是硬盘内存的数据什... 查看详情

浅谈缓存的理论与实践(代码片段)

...请点“在看”并加“星标”第一时间获取精彩技术分享在浅谈缓冲区这篇博客中,我们介绍了“缓冲”的相关知识,本文将介绍“缓冲”的孪生兄弟“缓存”。和缓冲类似,缓存可能是软件中使用最多的优化技术了&#x... 查看详情

高并发系统设计之缓存(代码片段)

...录至Github,推荐阅读👉Java随想录这篇文章来讲讲缓存。缓存是优化网站性能的第一手段,目的是让访问速度更快。说起缓存,第一反应可能想到的就是Redis。目前比较好的方案是使用多级缓存,如CPU→Ll/L2/L3... 查看详情

glide4.12图片框架之多级缓存源码设计分析(代码片段)

一、Glide缓存初识在上两篇文章中,我们从源码角度分析Glide框架加载图片的流程、以及Glide图片通过巧妙的空view的Fragment的设计实现的Glide的图片加载的三大生命周期函数onStart、onStop、onDestroy。Glide的框架的源码量确实比较... 查看详情

glide4.12图片框架之多级缓存源码设计分析(代码片段)

一、Glide缓存初识在上两篇文章中,我们从源码角度分析Glide框架加载图片的流程、以及Glide图片通过巧妙的空view的Fragment的设计实现的Glide的图片加载的三大生命周期函数onStart、onStop、onDestroy。Glide的框架的源码量确实比较... 查看详情

asp.netcore中如何使用缓存(代码片段)

...功能,也提供了灵活的键淘汰策略,所以,现在Redis用在缓存的场合非常多,也很多编程平台都将其作为分布式缓存的首选。如何安装Redis:方式一:Chocolatey是Windows平台下一款优秀的软件包管理工具(类似于NPM),该工具安装教... 查看详情

缓存应用tips(代码片段)

为了提高系统吞吐量,我们通常在业务架构中引入缓存层。缓存通常使用内存存储来实现,比如Redis/Memcached以及应用内缓存GuavaCache/DjangoCache。所有的查询操作先访问缓存,若未缓存该数据再从数据库中查询该数据并把它写入缓... 查看详情

glide缓存机制及源码(代码片段)

Glide里的缓存默认情况下,Glide会在开始一个新的图片请求之前检查以下多级的缓存:活动资源(ActiveResources)-现在是否有另一个View正在展示这张图片?内存缓存(Memorycache)-该图片是否最近被加载过并仍存在于内存中ÿ... 查看详情

mybatis学习笔记:一级缓存二级缓存(代码片段)

文章目录13.1、简介13.2、MyBatis缓存13.3、一级缓存测试查看一级缓存缓存失效的情况13.4、二级缓存13.1、简介1.什么是缓存(cache)?存在内存中的临时数据将用户经常查询的数据放在缓存(内存)中,用户... 查看详情

亿级系统的redis缓存如何设计???(代码片段)

...技会友。大家好,我是Tom哥。阅读本文大约需要15分钟。缓存设计可谓老生常谈了,早些时候都是采用memcache,现在大家更多倾向使用redis,除了知晓常用的数据存储类型,结合业务场景有针对性选择,好像其他也没有什么大的难... 查看详情

面试题16.25.lru缓存(代码片段)

...LeetCode题目:  设计和构建一个“最近最少使用”缓存,该缓存会删除最近最少使用的项目。缓存应该从键映射到值(允许你插入和检索特定键对应的值),并在初始化时指定最大容量。当缓存被填满时,它应该删... 查看详情

最佳内存缓存框架caffeine(代码片段)

Caffeine是一种高性能的缓存库,是基于Java8的最佳(最优)缓存框架。Cache(缓存),基于GoogleGuava,Caffeine提供一个内存缓存,大大改善了设计Guava‘scache和ConcurrentLinkedHashMap的体验。1LoadingCache<Key,Graph>graphs=Caffeine.newBuilder()2.m... 查看详情

高并发内存池项目(c++实战项目)(代码片段)

...指针方案◎第二阶段–高并发内存池整体框架设计1.线程缓存(threadcache)2.中心缓存(centralcache)3.页缓存࿰ 查看详情

day648.缓存设计问题-java业务开发常见错误(代码片段)

缓存设计问题Hi,阿昌来也!今天学习分享记录的是关于缓存设计的一系列问题。通常我们会使用更快的介质(比如内存)作为缓存,来解决较慢介质(比如磁盘)读取数据慢的问题,缓存是用空间换时... 查看详情

day648.缓存设计问题-java业务开发常见错误(代码片段)

缓存设计问题Hi,阿昌来也!今天学习分享记录的是关于缓存设计的一系列问题。通常我们会使用更快的介质(比如内存)作为缓存,来解决较慢介质(比如磁盘)读取数据慢的问题,缓存是用空间换时... 查看详情