代码编织梦想

原文链接

系列文章目录

一、简单的CQRS实现与原始SQL和DDD
二、使用EF的领域模型的封装和持久化透明(PI)
三、REST API数据验证
四、领域模型验证
五、如何发布和处理领域事件
六、处理领域事件:缺失的部分
七、发件箱模式(The Outbox Pattern)
八、.NET Core中的旁路缓存模式(Cache-Aside Pattern)

前言

通常情况下,我们需要专注于优化应用程序的性能。有很多方法可以做到这一点,其中一种方法是缓存一些数据。在这篇文章中,我将简要描述旁路缓存模式(Cache-Aside Pattern)及其在 .NET Core中的简单实现。

旁路缓存模式

这个模式非常简单和直接。当我们需要特定的数据时,我们首先尝试从缓存中获取它。如果数据不在缓存中,则从源中获取数据,将其添加到缓存中并返回。得益于此,在下一个查询中,数据将从缓存中获得。在向缓存添加数据时,我们需要确定数据应该在缓存中存储多长时间。下面是算法框图:

image

第一个实现

在 .NET Core中实现这个模式就像它的理论部分一样简单。首先,我们需要注册IMemoryCache接口:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMemoryCache();
}

然后,我们需要添加 Microsoft.Extensions.Caching.Memory NuGet包。

就这些。假设我们想要缓存用户的基本信息,该模式的实现如下所示:

[Route("api/[controller]")]
[ApiController]
public class UsersController : ControllerBase
{
    private readonly IMemoryCache _memoryCache;

    public UsersController(IMemoryCache memoryCache)
    {
        _memoryCache = memoryCache;
    }

    [HttpGet]
    [Route("{userId:int}")]
    public ActionResult<UserInfo> Get(int userId)
    {
        UserInfo userInfo;

        if (!this._memoryCache.TryGetValue($"UserInfo_{userId}", out userInfo))
        {
            userInfo = this.GetFromDatabase(userId);

            this._memoryCache.Set($"UserInfo_{userId}", userInfo, new TimeSpan(0, 5, 0));
        }

        return userInfo;
    }
}

首先,我们正在注入 .NET Core框架的IMemoryCache接口实现。然后在第18行中,我们检查数据是否在缓存中。如果它不在缓存中,我们从源(即数据库)获取它,添加到缓存并返回。

代码异味(Code smells)

这种实现方式你可以在MSDN网站上找到。我可以在这里实现这篇文章,但我必须承认,这段代码有一些地方我不喜欢。

首先,我认为接口IMemoryCache不够抽象。它建议数据保存在应用程序内存中,但客户端代码不应该关心数据存储在哪里。此外,如果我们希望将来将缓存保留在数据库中,则该接口的名称将不正确。

其次,客户端代码不应该负责命名缓存键的逻辑。这违反了单一责任原则。它应该只提供创建这个键名所需的数据。

最后,客户端代码不应该关心缓存过期。它应该在其他地方配置——应用程序配置

在下一节中,我将介绍如何消除这3种代码异味。

改进的实现

第一步也是最重要的一步是定义一个新的、更抽象的接口:ICacheStore

public interface ICacheStore
{
    void Add<TItem>(TItem item, ICacheKey<TItem> key);

    TItem Get<TItem>(ICacheKey<TItem> key) where TItem : class;
}

然后我们需要为我们的缓存键类定义接口:

public interface ICacheKey<TItem>
{
    string CacheKey { get; }
}

这个接口有CacheKey字符串属性,用于在MemoryCacheStore实现中解析缓存键:

public class MemoryCacheStore : ICacheStore
{
    private readonly IMemoryCache _memoryCache;
    private readonly Dictionary<string, TimeSpan> _expirationConfiguration;

    public MemoryCacheStore(
        IMemoryCache memoryCache,
        Dictionary<string, TimeSpan> expirationConfiguration)
    {
        _memoryCache = memoryCache;
        this._expirationConfiguration = expirationConfiguration;
    }

    public void Add<TItem>(TItem item, ICacheKey<TItem> key)
    {
        var cachedObjectName = item.GetType().Name;
        var timespan = _expirationConfiguration[cachedObjectName];

        this._memoryCache.Set(key.CacheKey, item, timespan);
    }

    public TItem Get<TItem>(ICacheKey<TItem> key) where TItem : class
    {
        if (this._memoryCache.TryGetValue(key.CacheKey, out TItem value))
        {
            return value;
        }

        return null;
    }
}

最后,我们需要配置IoC容器,将MemoryCacheStore实例解析为ICacheStore,并从应用程序配置中获取过期配置:

var children = this.Configuration.GetSection("Caching").GetChildren();
Dictionary<string, TimeSpan> configuration = 
children.ToDictionary(child => child.Key, child => TimeSpan.Parse(child.Value));

services.AddSingleton<ICacheStore>(x => new MemoryCacheStore(x.GetService<IMemoryCache>(), configuration));

新实现结构如下:

image

在此设置之后,我们终于可以在客户端代码中使用这个实现了。每当我们想要存储新对象到缓存中,我们需要:

1)添加过期配置

"Caching": {
  "UserInfo": "00:05:00"
}

2)定义缓存键的类

public class UserInfoCacheKey : ICacheKey<UserInfo>
{
    private readonly int _userId;
    public UserInfoCacheKey(int userId)
    {
        _userId = userId;
    }

    public string CacheKey => $"UserId_{this._userId}";
}

最后,新的客户端代码是这样的:

[Route("api/[controller]")]
[ApiController]
public class Users2Controller : ControllerBase
{
    private readonly ICacheStore _cacheStore;

    public Users2Controller(ICacheStore cacheStore)
    {
        _cacheStore = cacheStore;
    }

    [HttpGet]
    [Route("{userId:int}")]
    public ActionResult<UserInfo> Get(int userId)
    {
        var userInfoCacheKey = new UserInfoCacheKey(userId);
        UserInfo userInfo = this._cacheStore.Get(userInfoCacheKey);

        if (userInfo == null)
        {
            userInfo = this.GetFromDatabase(userId);

            this._cacheStore.Add(userInfo, userInfoCacheKey);
        }

        return userInfo;
    }
}

在上面的代码中,我们使用了更抽象的ICacheStore接口,不关心缓存键的创建和过期配置。这是更优雅的解决方案,更不容易出错。

总结

在这篇文章中,我描述了旁路缓存模式及其在 .NET Core中的主要实现。我还提出了增强设计,以少量的工作实现更优雅的解决方案。缓存快乐!🙂

Vue + Spring Boot 项目实战(二十一):缓存的应用(转载)-爱代码爱编程

重要链接: 「系列文章目录」 「项目源码(GitHub)」 本篇目录 前言 一、缓存:工程思想的产物 二、Web 中的缓存 1.缓存的工作模式 2.缓存的常见问题 三、缓存应用实战 1.Redis 与 Spring Data Redis 2.Redis 安装 3.Spring Data Redis 配置 4.缓存实现 5.验证 小结 前言 大家好,这次

面试大保健-爱代码爱编程

文章目录 面试大保健JDK 和 JRE 有什么区别?Java 内存区域谈谈面对对象的理解?String 、StringBuilder 、StringBuffer 的区别?是否可以继承 String 类?== 和 equals 的区别是什么?final 在 Java 中有什么作用?String 类的常用方法都有那些?Java 容器都有哪些?当一个对象

《深入理解计算机系统》CSAPP第3版术语索引表-爱代码爱编程

《深入理解计算机系统》几乎做到了只讲对程序员“有用的”原理,所以这本书性价比非常高。并没有在体系结构和操作系统的许多实现问题上纠缠。 第三版的英文原版是有index表的,中文没有。这里刷中文书的同时,随手记一下。不用字典序,用页码序。方便自己进行记录和知识回顾。不解释术语,忘了就翻书或search online 我刷CSAPP的2、3、4三章没怎么做题,算

4.1.1 Redis基础,安装,数据类型及应用场景, 常用命令, 客户端Jedis, 缓存过期/淘汰策略, 删除策略, 淘汰机制LRU/LFU-爱代码爱编程

目录 缓存原理&设计 缓存基本思想 什么是缓存? 缓存的使用场景 缓存的优势、代价 使用缓存的代价 缓存的三种读写模式 Redis基础 Redis介绍 什么是Redis Redis应用场景 Redis单机版安装和使用 Redis数据类型和应用场景 Redis的Key的设计 string字符串类型 list列表类型

分布式缓存系统——《高性能分布式缓存Redis》-爱代码爱编程

文章目录 缓存原理&设计生产中遇到的缓存问题缓存基本思想缓存的使用场景什么是缓存大型网站中缓存的使用缓存分类页面缓存网络端缓存服务端缓存缓存的优势、代价使用缓存的优势使用缓存的代价缓存读写模式Cache Aside Pattern(常用)Read/Write Through PatternWrite Behind Caching Patte

Redis快速实战-爱代码爱编程

缓存原理与设计 缓存基本思想 缓存的使用场景 DB缓存,减轻DB服务器压力 一般情况下数据存在数据库中,应用程序直接操作数据库。 当访问量上万,数据库压力增大,可以采取的方案有: 读写分离,分库分表 当访问量达到10万、百万,需要引入缓存。 将已经访问过的内容或数据存储起来,当再次访问时先找缓存,缓存命中返回数据。 不命中再找数据库,并回填缓存。提高

Redis 进阶笔记-爱代码爱编程

文章目录 第一部分 Redis 快速实战第一节 缓存原理与设计1.1 缓存基本思想1.11 缓存的使用场景1.12 什么是缓存?1.13 大型网站中缓存的使用1.2 常见缓存的分类1.21 客户端缓存1.22 网络端缓存1.23 服务端缓存1.3 缓存的优势与代价1.31 使用缓存的优势1.32 使用缓存的代价1.4 缓存的读写模式1.41 Cac

JAVA面试必备-爱代码爱编程

一、基础篇 网络基础 TCP三次握手 1、OSI与TCP/IP 模型2、常见网络服务分层3、TCP与UDP区别及场景4、TCP滑动窗口,拥塞控制5、TCP粘包原因和解决方法6、TCP、UDP报文格式HTTP协议 1、HTTP协议1.0_1.1_2.02、HTTP与HTTPS之间的区别3、Get和Post请求区别4

JAVA面试-爱代码爱编程

文章目录 一、基础篇网络基础**TCP三次握手****1、OSI与TCP/IP 模型****2、常见网络服务分层****3、TCP与UDP区别及场景****4、TCP滑动窗口,拥塞控制****5、TCP粘包原因和解决方法****6、TCP、UDP报文格式****HTTP协议****1、HTTP协议1.0_1.1_2.0**** 2、HTTP与HTT

缓存的应用 : 缓存——工程思想的产物-爱代码爱编程

缓存一词最初主要指 CPU 与内存之间的高速静态随机存取存储器(SRAM)。 一 web中的缓存 在做项目的过程中,不知道你们有没有感叹过,一个平平无奇的应用,涉及的点实在是太多了。各个点之间需要衔接,要衔接就会有两个层次的不均衡:      一是性能的不均衡,包括速率、吞吐量等,造成这种不均衡的原因包括软件、硬件、网络、协议、策略等、位置多个维

第五阶段-第五阶段高性能分布式缓存redis_管程序猿的博客-爱代码爱编程

第五阶段 大型分布式系统缓存架构进阶 文章目录 第五阶段 大型分布式系统缓存架构进阶第一部分 Redis 快速实战第一节 缓存原理与设计1.1 缓存基本思想1.11 缓存的使用场景1.12 什么是缓存?1.13 大型网站中缓存的使用1.2 常见缓存的分类1.21 客户端缓存1.22 网络端缓存1.23 服务端缓存1.3 缓存的优势与代价1.31

面经-hangzhou_云f的博客-爱代码爱编程

目录 上篇 一、基础篇 网络基础 TCP三次握手 HTTP协议 浏览器输入URL过程 操作系统基础 进程和线程的区别 操作系统内存管理 Java基础 面向对象三大特性 数据结构 设计模式与原则 面试题 构造方法 初始化块 This 重写和重载的区别 Object类方法 基本数据类型和包装类 二、JVM篇 JVM内

celery包结构、celery任务、双写一致性_山上有个车的博客-爱代码爱编程

文章目录 一、celery包结构二、celery任务1.异步任务2.延迟任务3.定时任务三、双写一致性 一、celery包结构 celery编写为包结构后,可以随意拔插到任意项目中使用,也是celery官方推荐写法。 第一步: 创建一个celery包 第二步: 创建celery.py(固定名字不能自定义) from celery imp

结构型模式——组合模式(composite pattern)-爱代码爱编程

文章目录 组合模式(Composite Pattern)应用使用模板(Java)透明模式安全模式 实例分析(JDK) 简介UML图角色 目的:主要解决:何时使用:如何解决:关键