ABP - 缓存(Caching)[IDistributedCache、ICacheManager、ICacheKeyNormalizer、[Cache]、[CacheInvalidate]]

news/2025/10/24 22:09:42/文章来源:https://www.cnblogs.com/tangge/p/19164320

(一)缓存(Caching)

核心辅助类

  • IDistributedCache:分布式缓存(基于Redis等)。
  • ICacheManager:缓存管理器(支持多级缓存)。
  • [Cache]:方法缓存特性。
  • ICacheKeyNormalizer:缓存键标准化器,自动添加租户前缀(多租户场景)或应用前缀。
  • [CacheInvalidate]:缓存失效特性(修改数据时自动清除对应缓存)。

缓存(Caching)核心类详解与实战示例

缓存是提升系统性能的关键技术,通过将频繁访问的数据暂时存储在内存(或Redis等中间件)中,减少对数据库的访问次数。ABP框架提供了一套灵活的缓存机制,以下结合IDistributedCacheICacheManager[Cache]等核心类。

一、核心概念:为什么需要缓存?(生活例子)

想象你开了一家奶茶店:

  • 顾客经常问“珍珠奶茶多少钱?”(高频查询),你每次都去查价目表(数据库),效率低;
  • 你把价目表贴在吧台(缓存),顾客再问时直接看吧台,不用反复查价目表,速度快多了。

程序中的缓存也是同理:对“不常变化但经常查询的数据”(如商品分类、字典表),第一次查询后存到缓存,后续直接从缓存取,减少数据库压力。

二、核心类说明

类/特性 核心作用 通俗理解
IDistributedCache 分布式缓存接口(支持Redis、Memcached等) 多服务器共享的“公共缓存”(如集群部署时用)
ICacheManager 缓存管理器(支持内存缓存、多级缓存) 管理不同类型的缓存,方便统一操作
[Cache] 方法缓存特性(自动缓存方法返回值) 给方法加个标签,自动缓存结果,不用写代码
[CacheInvalidate] 缓存失效特性(修改数据时自动删缓存) 数据变了,自动删掉旧缓存,避免返回脏数据
ICacheKeyNormalizer 缓存键标准化(自动加前缀) 给缓存键加“标签”(如租户ID),避免键冲突

三、实战示例:从基础到进阶

1. [Cache]:最简单的方法缓存(一行代码搞定)

给查询方法加[Cache]特性,框架会自动缓存方法的返回值,下次调用时直接返回缓存结果,不执行方法体。

示例:缓存商品分类列表(高频查询,低频修改)

using Volo.Abp.Caching;
using Volo.Abp.Application.Services;public class ProductCategoryAppService : ApplicationService
{private readonly IRepository<ProductCategory, Guid> _categoryRepo;public ProductCategoryAppService(IRepository<ProductCategory, Guid> categoryRepo){_categoryRepo = categoryRepo;}// 加[Cache]特性:自动缓存返回结果,默认缓存10分钟[Cache(SlidingExpiration = 10)] // SlidingExpiration:10分钟内没人访问就失效public async Task<List<ProductCategoryDto>> GetAllCategoriesAsync(){// 第一次调用会执行:查数据库var categories = await _categoryRepo.GetListAsync();return ObjectMapper.Map<List<ProductCategory>, List<ProductCategoryDto>>(categories);}
}

效果:

  • 第一次调用GetAllCategoriesAsync():查数据库,结果存到缓存;
  • 10分钟内再次调用:直接从缓存返回,不查数据库;
  • 10分钟内没人调用:缓存自动失效,下次重新查数据库。

2. [CacheInvalidate]:数据更新时自动删缓存

当数据被修改(新增/更新/删除)时,需要删除旧缓存,否则会返回过时数据。[CacheInvalidate]特性可自动完成这个操作。

示例:修改分类后删除缓存

public class ProductCategoryAppService : ApplicationService
{// 新增分类后,删除GetAllCategoriesAsync方法的缓存[CacheInvalidate(MethodName = nameof(GetAllCategoriesAsync))]public async Task CreateCategoryAsync(CreateCategoryInput input){var category = new ProductCategory { Name = input.Name };await _categoryRepo.InsertAsync(category);}// 更新分类后,删除GetAllCategoriesAsync方法的缓存[CacheInvalidate(MethodName = nameof(GetAllCategoriesAsync))]public async Task UpdateCategoryAsync(Guid id, UpdateCategoryInput input){var category = await _categoryRepo.GetAsync(id);category.Name = input.Name;await _categoryRepo.UpdateAsync(category);}// 之前的查询方法(带[Cache])[Cache(SlidingExpiration = 10)]public async Task<List<ProductCategoryDto>> GetAllCategoriesAsync(){// ...}
}

效果:

  • 调用CreateCategoryAsyncUpdateCategoryAsync后,框架会自动删除GetAllCategoriesAsync方法的缓存;
  • 下次查询时会重新从数据库获取最新数据,并更新缓存,保证数据一致性。

3. ICacheManager:手动控制缓存(灵活场景)

当需要更灵活的缓存控制(如缓存部分数据、条件性缓存)时,用ICacheManager手动操作缓存。

示例:手动缓存用户信息

using Volo.Abp.Caching;
using Volo.Abp.DependencyInjection;public class UserCacheService : ITransientDependency
{private readonly ICacheManager _cacheManager;private readonly IRepository<IdentityUser, Guid> _userRepo;// 注入缓存管理器public UserCacheService(ICacheManager cacheManager, IRepository<IdentityUser, Guid> userRepo){_cacheManager = cacheManager;_userRepo = userRepo;}// 获取用户信息(优先从缓存取)public async Task<IdentityUser> GetUserByIdAsync(Guid userId){// 1. 获取一个缓存实例(可理解为“缓存容器”,名称自定义,如"UserCache")var cache = _cacheManager.GetCache("UserCache");// 2. 尝试从缓存获取:key是userId,不存在则执行委托查数据库并缓存return await cache.GetOrAddAsync(key: userId.ToString(), // 缓存键(用userId当唯一标识)factory: async () => await _userRepo.GetAsync(userId), // 缓存不存在时执行的逻辑options: () => new CacheOptions { AbsoluteExpiration = DateTime.Now.AddHours(1) } // 1小时后绝对失效);}// 手动删除用户缓存(如用户信息更新后)public async Task RemoveUserCacheAsync(Guid userId){var cache = _cacheManager.GetCache("UserCache");await cache.RemoveAsync(userId.ToString()); // 删除指定key的缓存}
}

核心方法:

  • GetOrAddAsync:尝试从缓存获取,不存在则执行factory方法查询并缓存;
  • RemoveAsync:删除指定键的缓存;
  • SetAsync:手动设置缓存(GetOrAddAsync已包含此逻辑,一般不用单独调用)。

4. IDistributedCache:分布式缓存(集群部署必备)

当系统部署在多台服务器(集群)时,内存缓存(ICacheManager默认是内存缓存)无法共享,需用IDistributedCache(如Redis)实现缓存共享。

示例:用Redis缓存全局配置

using Microsoft.Extensions.Caching.Distributed;
using System.Text.Json;
using Volo.Abp.DependencyInjection;public class GlobalConfigService : ITransientDependency
{private readonly IDistributedCache _distributedCache;private readonly IRepository<GlobalConfig, Guid> _configRepo;// 注入分布式缓存(默认是Redis,需在配置文件中配置)public GlobalConfigService(IDistributedCache distributedCache, IRepository<GlobalConfig, Guid> configRepo){_distributedCache = distributedCache;_configRepo = configRepo;}// 获取全局配置(从Redis缓存)public async Task<GlobalConfigDto> GetGlobalConfigAsync(){// 1. 从Redis获取缓存(键是"GlobalConfig")var cachedData = await _distributedCache.GetStringAsync("GlobalConfig");if (!string.IsNullOrEmpty(cachedData)){// 缓存存在:反序列化为对象并返回return JsonSerializer.Deserialize<GlobalConfigDto>(cachedData);}// 2. 缓存不存在:查数据库var config = await _configRepo.GetAsync(x => x.IsDefault);var configDto = ObjectMapper.Map<GlobalConfig, GlobalConfigDto>(config);// 3. 存入Redis缓存(设置1小时过期)await _distributedCache.SetStringAsync(key: "GlobalConfig",value: JsonSerializer.Serialize(configDto),options: new DistributedCacheEntryOptions{AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1) // 1小时后失效});return configDto;}
}

配置Redis(appsettings.json):

"Redis": {"Configuration": "localhost:6379", // Redis服务器地址"InstanceName": "MyApp_" // 实例名(避免多个应用共用Redis时键冲突)
}

5. ICacheKeyNormalizer:避免缓存键冲突

在多租户系统或模块化应用中,不同租户/模块可能用相同的缓存键(如都用"Config"),导致冲突。ICacheKeyNormalizer会自动给缓存键加前缀(如租户ID、模块名)。

示例:自动添加租户前缀

ABP默认的CacheKeyNormalizer会处理多租户场景:

  • 租户1的缓存键:Tenant_1:UserCache:123
  • 租户2的缓存键:Tenant_2:UserCache:123
  • 避免不同租户的数据互相干扰。

自定义缓存键前缀(可选):

public class MyCacheKeyNormalizer : CacheKeyNormalizerBase
{public MyCacheKeyNormalizer(ICurrentTenant currentTenant) : base(currentTenant){}public override string NormalizeKey(CacheKeyNormalizeContext context){// 自定义前缀:模块名 + 租户ID + 原始键return $"MyModule:{CurrentTenant.Id}:{context.Key}";}
}// 注册自定义缓存键标准化器(在模块中)
public override void ConfigureServices(ServiceConfigurationContext context)
{context.Services.Replace(ServiceDescriptor.Singleton<ICacheKeyNormalizer, MyCacheKeyNormalizer>());
}

四、缓存的核心配置(appsettings.json)

"AbpCaching": {"CacheExpirationDefaults": {"Default": 60, // 默认缓存时间(分钟)"Entity": 360, // 实体缓存默认时间(分钟)"Permission": 360, // 权限缓存默认时间(分钟)"Setting": 360, // 设置缓存默认时间(分钟)"User": 120 // 用户信息缓存默认时间(分钟)}
}

五、新手避坑指南

  1. 缓存数据必须可序列化:存入分布式缓存(如Redis)的数据需要能序列化为字符串(避免存入复杂对象如DataTable);
  2. 缓存失效策略要合理
    • 高频访问、低频修改的数据:缓存时间可以长(如1小时);
    • 低频访问、高频修改的数据:不建议缓存(或缓存时间短,如1分钟);
  3. 避免缓存“热点数据”:如首页Banner、热门商品,更新时要及时用[CacheInvalidate]删除缓存;
  4. 分布式缓存配置:集群部署必须用IDistributedCache(Redis),否则多服务器缓存不一致;
  5. 不要缓存太大的数据:如整个表的10万条记录,会占用过多内存,建议分页缓存。

总结

  • 简单场景:用[Cache]+[CacheInvalidate],零代码实现缓存;
  • 灵活场景:用ICacheManager手动控制缓存(如部分数据缓存);
  • 集群场景:用IDistributedCache(Redis)实现缓存共享;
  • 多租户/模块化:依赖ICacheKeyNormalizer避免键冲突。

通过合理使用缓存,能显著减少数据库压力,提升系统响应速度。需要“缓存与数据库一致性保障”的深入讲解可以告诉我。

(二)ABP 配置 Redis

抱歉之前没详细讲Redis配置!在ABP中使用分布式缓存(Redis)需要几个关键步骤,包括安装依赖、配置连接、注册服务,下面手把手你一步一步讲清楚,确保能跑通。

1、准备工作:安装Redis服务器

首先需要在你的服务器(或本地)安装Redis:

  • Windows:推荐用 Redis for Windows(下载.msi文件安装,默认端口6379);
  • Linux:直接用命令安装 sudo apt-get install redis-server
  • 安装后启动Redis,默认不需要密码(生产环境需配置密码)。

2、ABP项目中配置Redis的完整步骤

步骤1:安装Redis依赖包

在你的 应用层项目(如MyApp.Application)或 Web层项目(如MyApp.Web)中,安装ABP的Redis缓存包:

# 通过NuGet安装(Package Manager控制台)
Install-Package Volo.Abp.Caching.StackExchangeRedis# 或通过.NET CLI
dotnet add package Volo.Abp.Caching.StackExchangeRedis

步骤2:在模块中引入Redis模块

在你的 核心模块(如MyAppWebModule)中,通过[DependsOn]引入AbpCachingStackExchangeRedisModule,开启Redis缓存支持:

using Volo.Abp.Caching.StackExchangeRedis;
using Volo.Abp.Modularity;[DependsOn(typeof(AbpAspNetCoreModule),typeof(AbpCachingStackExchangeRedisModule) // 引入Redis缓存模块
)]
public class MyAppWebModule : AbpModule
{// 后续配置写在这里
}

步骤3:配置Redis连接(appsettings.json)

appsettings.json中添加Redis连接配置,告诉ABP如何连接你的Redis服务器:

{"Redis": {"Configuration": "localhost:6379", // Redis服务器地址+端口(默认6379)"InstanceName": "MyApp_" // 实例名(可选,用于区分多个应用的缓存键)},"AbpDistributedCacheOptions": {"KeyPrefix": "MyAppCache_" // 缓存键统一前缀(可选,进一步避免冲突)}
}

配置说明:

  • Configuration:Redis连接字符串,格式为"服务器地址:端口[,password=密码]"
    • 示例(带密码):"192.168.1.100:6379,password=myredis123"
    • 示例(集群):"192.168.1.101:6379,192.168.1.102:6379"
  • InstanceName:多个应用共用一个Redis时,用实例名区分(如"ShopApp_""AdminApp_");
  • KeyPrefix:所有缓存键会自动加上这个前缀(如"MyAppCache_User_123"),避免键重复。

步骤4:验证Redis是否生效

配置完成后,ABP会自动将IDistributedCache的实现替换为Redis缓存,无需额外代码。可以通过以下方式验证:

示例:存入缓存并在Redis中查看

using Microsoft.Extensions.Caching.Distributed;
using Volo.Abp.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc;public class RedisTestController : AbpController
{private readonly IDistributedCache _distributedCache;public RedisTestController(IDistributedCache distributedCache){_distributedCache = distributedCache;}// 访问此接口:将数据存入Redis[HttpGet("set-cache")]public async Task<IActionResult> SetCacheAsync(){// 存入缓存:键为"TestKey",值为"Hello Redis"await _distributedCache.SetStringAsync("TestKey", "Hello Redis",new DistributedCacheEntryOptions{AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10) // 10分钟后过期});return Ok("缓存已存入Redis");}// 访问此接口:从Redis读取缓存[HttpGet("get-cache")]public async Task<IActionResult> GetCacheAsync(){var value = await _distributedCache.GetStringAsync("TestKey");return Ok($"从Redis读取到的值:{value}");}
}

验证步骤:

  1. 启动项目,访问 https://localhost:端口/set-cache,返回“缓存已存入Redis”;
  2. 打开Redis客户端(如Windows的redis-cli.exe),输入命令 keys *,会看到类似 MyApp_MyAppCache_TestKey 的键(前缀由配置的InstanceNameKeyPrefix组成);
  3. 输入 get MyApp_MyAppCache_TestKey,会返回 Hello Redis,说明缓存成功存入Redis;
  4. 访问 https://localhost:端口/get-cache,会返回“从Redis读取到的值:Hello Redis”,验证读取成功。

步骤5:高级配置(可选)

如果需要更精细的Redis配置(如连接池大小、超时时间),可以在模块的ConfigureServices中手动配置:

public override void ConfigureServices(ServiceConfigurationContext context)
{var configuration = context.Services.GetConfiguration();// 手动配置RedisConfigure<AbpRedisCacheOptions>(options =>{// 从配置文件读取连接字符串options.Configuration = configuration["Redis:Configuration"];options.InstanceName = configuration["Redis:InstanceName"];// 高级选项:连接池大小(默认50)options.ConnectionPoolSize = 100;// 连接超时时间(毫秒)options.ConnectTimeout = 5000;// 同步超时时间(毫秒)options.SyncTimeout = 3000;});
}

3、常见问题与解决办法

  1. 无法连接Redis

    • 检查Configuration中的地址和端口是否正确(默认localhost:6379);
    • 确保Redis服务器已启动(Windows可在服务中查看“Redis”服务状态);
    • 关闭防火墙或开放6379端口。
  2. 缓存键重复

    • 务必配置InstanceNameKeyPrefix,尤其是多个应用共用一个Redis时;
    • 多租户系统中,ABP的ICacheKeyNormalizer会自动添加租户ID前缀,无需额外处理。
  3. 缓存数据序列化问题

    • IDistributedCache默认用JSON序列化,复杂对象(如DateTimeOffset)可能有兼容性问题;

    • 可自定义序列化器(如用Protobuf),提升性能和兼容性:

      Configure<AbpDistributedCacheOptions>(options =>
      {options.Serializer = new MyProtobufCacheSerializer(); // 自定义序列化器
      });
      

通过以上步骤,你的ABP项目就能成功集成Redis分布式缓存了。分布式缓存特别适合集群部署(多台服务器共享缓存),或需要持久化缓存(避免应用重启后缓存丢失)的场景。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/945708.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

好想成为人类啊——2025 . 10 . 24

好想成为人类啊。 今天应该是比较著名的程序员节,貌似大家都在庆祝。但我其实没啥感觉,因为我的理想并不是成为一个程序员。 那我的理想是什么呢,也好像并没有什么理想,可能跟艺术方面沾点儿边吧,当然,这怎么看都…

10 24(+第14场补题)

14.3维护 \(cur\), \(ans\) , \(dif = cur - ans\) 。 每次执行 \(cur = cur + a_i, \quad dif = dif + a_i\)。如果 \(cur < 0\),则 \(dif -= cur, \quad cur = 0\)。 如果 \(dif > 0\),则 \(dif = 0\)。考虑…

详细介绍:C++ 位运算 高频面试考点 力扣 268. 丢失的数字 题解 每日一题

详细介绍:C++ 位运算 高频面试考点 力扣 268. 丢失的数字 题解 每日一题pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family:…

详细介绍:第十六届蓝桥杯软件赛C组省赛C++题解(京津冀)

详细介绍:第十六届蓝桥杯软件赛C组省赛C++题解(京津冀)pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consola…

《打造自己的 DeepSeek》第 1 期:为什么要打造自己的 DeepSeek?

近些年 AI 大火,不过在国内真正火起来还是源于今年春节期间的 DeepSeek。 个人认为有两方面原因:一方面是 DeepSeek 使用方便。由于众所周知的原因,国内对国外网站的访问是有诸多限制的,其中就包括各大 AI 模型的官…

ret2text

from pwn import * io = remote("39.106.48.123", 29826) payload = b"A"*264 + p64(0x401202) io.sendline(payload) io.interactive() 264即258+8,258是rbp的位置,ida里一般是16位,0x401202为…

ABP - 异常处理(Exception Handling)[AbpExceptionFilter、UserFriendlyException、IExceptionSubscriber]

一、异常处理(Exception Handling) 常用核心辅助类:AbpExceptionFilter:自动捕获并处理异常。 UserFriendlyException:用户友好异常(直接返回给前端)。 IExceptionSubscriber:自定义异常订阅。1、核心类全解析…

2025年沸腾干燥机厂家权威推荐榜单:专业直销与高效节能技术深度解析,提供优质沸腾干燥设备及定制方案

2025年沸腾干燥机厂家权威推荐榜单:专业直销与高效节能技术深度解析,提供优质沸腾干燥设备及定制方案 沸腾干燥技术作为现代工业干燥领域的重要工艺,凭借其高效的传热传质效率和均匀的干燥效果,在制药、化工、食品…

CF Round 1046(#2135) 总结

CF Round 1046(#2135) 总结 A 可以 DP,用 vector 存下这个数出现的位置。 B 考虑移动到无限远处,如果移到左下角,容易发现离的最近的点就是离 \((-10^9,-10^9)\) 最近的点。这样就能确定一条直线(确定 \(x+y\))。…

重组蛋白表达的几种类型介绍

一、引言 重组蛋白表达(Recombinant Protein Expression) 是分子生物学与蛋白质工程中的核心技术。通过将目标基因导入特定宿主系统,可以在细胞内合成外源蛋白。不同的表达类型反映了蛋白在细胞中的定位方式、折叠状…

Luogu P5479 [BJOI2015] 隐身术 题解 [ 紫 ] [ 多维 DP ] [ 交换维度 ] [ 后缀数组 ] [ 哈希 ]

隐身术:挺巧妙的一道题。 首先考虑一个暴力:将“子串”的条件转化为对每一个后缀的前缀考虑,枚举每一个后缀。然后对每一个后缀做一个编辑距离的 DP,统计答案即可。 具体地,编辑距离的 DP 状态定义为:\(dp_{i, j…

2025年10月23日

A. 萌萌甜酱 洛谷原题:https://www.luogu.com.cn/problem/P3621题目 你准备给弟弟 Ike 买一件礼物,但是,Ike 挑选礼物的方式很特别:他只喜欢那些能被他排成有序形状的东西。 你准备给 Ike 买一个风铃。风铃是一种多…

大象《Thinking in Projects》读书笔记2

大象《Thinking in Projects》第二部分聚焦项目执行阶段的核心逻辑与实践方法,与第一部分强调的项目思维搭建形成紧密衔接,为大三学生从理论学习转向实践落地提供了关键指引。​ 这部分内容首先深入剖析了项目进度管…

06_DNS解析:从域名到IP地址

本文将通过C语言,手写实现一个基于UDP编程的DNS域名解析。一.DNS是什么? DNS(Domain Name System)的作用将我们容易记忆的域名转换为计算机可以处理的IP地址,DNS 使用 TCP 和 UDP 端口 53。 常用的nslookup命令,可…

ABP - 接口授权 [Authorize、AllowAnonymous、IPermissionChecker]

接口授权(Authorization) 核心辅助类:[Authorize]:标记需要授权的接口。 [AllowAnonymous]:允许匿名访问。 IPermissionChecker:手动检查权限。接口授权(Authorization)核心类示例与讲解 接口授权是在“身份认…

日总结 17

对比维度 BS 架构(Browser/Server,浏览器 / 服务器) CS 架构(Client/Server,客户端 / 服务器) 部署方式 仅需部署服务器,用户通过浏览器(如 Chrome、Edge)访问,无需安装客户端。 需同时部署服务器和客户端,…

杂题选做-3

杂题选做-3 #21 P9755 题目传送门 首先,看上去这个题直接不是很友好,我们考虑二分转为判断性问题。 然后一个这类在树上 topo 遍历每一个节点的最优方案类问题有一个 Trick:我们找出当前最优的节点,将其与其父亲合…

10.24每日总结

今天主要的课程有人机交互和机器学习,软考网课已经学到第六章了,前两天没有上传笔记,一会儿把这两三天的都传到这篇,距离考试还有半个多月,加油!

利用Eval Villain挖掘CSPT漏洞的完整指南

本文详细介绍了如何使用Eval Villain工具来发现和利用客户端路径遍历(CSPT)漏洞。通过Doyensec的CSPT演练环境,逐步演示了从漏洞发现到利用的完整过程,包括工具配置、漏洞检测、响应分析和最终利用链构建。CSPT the …

Button按钮插入图片后仍有白色边框的解决办法

别人的大道再好,那也是别人的道路,不妨埋头做事,但问耕耘莫问收获,偶尔抬头,左右看两眼其它路上的人物风光,就够了。起因:我设置完背景图片后 ,边框仍有白色边框 ,看起来很别扭 如图所示:解决方法: 将FlatA…