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

(一)缓存(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();

    // 手动配置Redis
    Configure<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分布式缓存了。分布式缓存特别适合集群部署(多台服务器共享缓存),或需要持久化缓存(避免应用重启后缓存丢失)的场景。

posted @ 2025-10-24 22:03  【唐】三三  阅读(2)  评论(0)    收藏  举报