ABP演示 - 缓存

1. 缓存功能概述

在 ABP 框架中实现缓存功能主要涉及以下核心组件:

  • 分布式缓存接口 IDistributedCache<T>
  • 缓存项定义 (如 BookCacheItem)
  • 缓存键生成器 (如 BookCacheKeyGenerator)
  • 批量缓存操作 (如 BookCacheBulkOperations)
  • 缓存失效处理器 (如 BookCacheInvalidator)

2. 实现步骤

基础版:IDistributedCache<TCacheItem>

  • 缓存键只能是 string 类型,所有场景都需要手动拼接(如 Product:1Order:2026:1001);
  • 无类型约束,容易因键格式错误导致缓存命中失败(如把 Guid 直接转字符串、多参数拼接顺序错误)。

2.1 定义缓存项

不加 [CacheName]{缓存项类型的全限定名}:{缓存键}(如 Acme.BookStore.CacheItems.BookCacheItem:1);

[CacheName("Books")]{CacheName值}:{缓存键}(如 Books:1

特性 不加 [CacheName] 加 [CacheName]
键长度 较长(含命名空间) 短(仅指定名称)
可读性 较低 更高
重构影响 类重命名会导致缓存失效 类重命名不影响缓存键
多模块冲突风险 不同模块的同名类会冲突 可全局统一命名避免冲突
Redis可视化 键名冗长 更清晰的层级结构(如 Books:*
public class BookCacheItem
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public BookType Type { get; set; }
    public DateTime PublishDate { get; set; }
    public float Price { get; set; }
}

2.2 创建缓存键生成器

public class BookCacheKeyGenerator : ITransientDependency
{
    public const string CachePrefix = "Book:";
    
    public string GetCacheKey(string id) => $"{CachePrefix}{id}";
    public string GetCacheKey(Guid id) => $"{CachePrefix}{id}";
}

2.3 实现批量缓存操作

ABP的分布式缓存接口定义了以下批量操作方法,当你需要在一个方法中调用多次缓存操作时,这些方法可以提高性能

  • SetManyAsync 和 SetMany 方法可以用来向缓存中设置多个值.
  • GetManyAsync 和 GetMany 方法可以用来从缓存中获取多个值.
  • GetOrAddManyAsync 和 GetOrAddMany 方法可以用来从缓存中获取并添加缺少的值.
  • RefreshManyAsync 和 RefreshMany 方法可以来用重置多个值的滚动过期时间.
  • RemoveManyAsync 和 RemoveMany 方法可以用来从缓存中删除多个值.

这些不是标准的ASP.NET Core缓存方法, 所以某些提供程序可能不支持. [ABP Redis集成包]实现了它们. 如果提供程序不支持,会回退到 SetAsync 和 GetAsync ... 方法(循环调用).

namespace Acme.BookStore.Caching
{
    public class BookCacheBulkOperations : ITransientDependency
    {
        private readonly IDistributedCache<BookCacheItem> _cache;
        private readonly BookCacheKeyGenerator _cacheKeyGenerator;

        public BookCacheBulkOperations(
            IDistributedCache<BookCacheItem> cache,
            BookCacheKeyGenerator cacheKeyGenerator)
        {
            _cache = cache;
            _cacheKeyGenerator = cacheKeyGenerator;
        }

        public async Task SetManyAsync(Dictionary<Guid, BookCacheItem> items)
        {
            var cacheEntries = items.ToDictionary(
                x => _cacheKeyGenerator.GetCacheKey(x.Key),
                x => x.Value);
            
            await _cache.SetManyAsync(cacheEntries, new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30)
            });
        }

        public async Task<Dictionary<Guid, BookCacheItem>> GetManyAsync(IEnumerable<Guid> ids)
        {
            var cacheKeys = ids.Select(_cacheKeyGenerator.GetCacheKey).ToList();
            var cachedItems = await _cache.GetManyAsync(cacheKeys);
            
            return cachedItems.ToDictionary(
                x => Guid.Parse(x.Key.Replace(BookCacheKeyGenerator.CachePrefix, "")),
                x => x.Value);
        }

        public async Task RemoveManyAsync(IEnumerable<Guid> ids)
        {
            var cacheKeys = ids.Select(_cacheKeyGenerator.GetCacheKey).ToList();
            await _cache.RemoveManyAsync(cacheKeys);
        }
    }
}

2.4 在应用服务中使用缓存

public override async Task<BookDto> GetAsync(Guid id)
{
    var cacheKey = _cacheKeyGenerator.GetCacheKey(id);
    
    // 尝试从缓存获取
    var cachedItem = await _cache.GetAsync(cacheKey);
    if (cachedItem != null)
    {
        return ObjectMapper.Map<BookCacheItem, BookDto>(cachedItem);
    }

    // 缓存未命中,从数据库获取
    var book = await base.GetAsync(id);
    
    // 存入缓存
    var cacheItem = ObjectMapper.Map<BookDto, BookCacheItem>(book);
    await _cache.SetAsync(
        cacheKey,
        cacheItem,
        new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30)
        });

    return book;
}

2.5 实现缓存失效(监听,按需求)

缓存失效是什么?

想象一个场景:

  • 你第一次查图书"三国演义",系统从数据库读取并存入缓存

  • 你第二次查"三国演义",系统直接从缓存读取,很快

  • 现在有人修改了书名改成"三国演义之赤壁之战"

  • 问题:缓存里还是"三国演义",不是最新的!

缓存失效就是解决这个问题:当数据更新时,自动删除旧缓存,下次查询时重新加载。

它是自动工作的吗?
是的!只要:

  1. BookCacheInvalidator 类存在(已创建)
  2. 继承了 ILocalEventHandler<EntityUpdatedEventData<Book>>
  3. 注册为 ITransientDependency(ABP会自动扫描并注册)

EntityCreatedEventData<TEntity>:实体创建后触发;

EntityDeletedEventData<TEntity>:实体删除后触发;

三者共同继承 EntityChangedEventData<TEntity>,处理逻辑可复用。

参考:
ABP - 实体变更监视事件 [EntityCreatedEventData、EntityUpdatedEventData、EntityDeletedEventData]
[ABP - 事件总线(Event Bus)IEventBus、LocalEventBus、IntegrationEvent]

满足以上条件后,每当 Book 实体更新时,ABP 会自动调用这个处理器清理缓存。

namespace Acme.BookStore.Caching
{
    public class BookCacheInvalidator :
        ILocalEventHandler<EntityUpdatedEventData<Book>>,
        ITransientDependency
    {
        private readonly IDistributedCache<BookCacheItem> _cache;
        private readonly BookCacheKeyGenerator _cacheKeyGenerator;

        public BookCacheInvalidator(
            IDistributedCache<BookCacheItem> cache,
            BookCacheKeyGenerator cacheKeyGenerator)
        {
            _cache = cache;
            _cacheKeyGenerator = cacheKeyGenerator;
        }

        public async Task HandleEventAsync(EntityUpdatedEventData<Book> eventData)
        {
            var cacheKey = _cacheKeyGenerator.GetCacheKey(eventData.Entity.Id);
            await _cache.RemoveAsync(cacheKey);
        }
    }
}

2.7 为应用程序设置缓存键前缀 AbpDistributedCacheOptions

AbpDistributedCacheOptions 是配置缓存的主要[Option类].

示例:为应用程序设置缓存键前缀

Configure<AbpDistributedCacheOptions>(options =>
{
    options.KeyPrefix = "MyApp1";
});

在[模块类]的ConfigureServices方法中添加代码.

可用选项

  • HideErrors (bool, 默认: true): 启用/禁用隐藏从缓存服务器写入/读取值时的错误.
  • KeyPrefix (string, 默认: null): 如果你的缓存服务器由多个应用程序共同使用, 则可以为应用程序的缓存键设置一个前缀. 在这种情况下, 不同的应用程序不能覆盖彼此的缓存内容.
  • GlobalCacheEntryOptions (DistributedCacheEntryOptions): 用于设置保存缓内容却没有指定选项时, 默认的分布式缓存选项 (例如 AbsoluteExpirationSlidingExpiration). SlidingExpiration的默认值设置为20分钟.

增强版:IDistributedCache<TCacheItem, TCacheKey>

  • 缓存键支持任意自定义类型(如 Guidint、自定义 ProductCacheKey 类);
  • 框架内置键的序列化 / 反序列化逻辑,无需手动拼接字符串键。

IDistributedCache<TCacheItem> 接口默认了缓存键string 类型 (如果你的键不是string类型需要进行手动类型转换). 但当缓存键的类型不是string时, 可以使用IDistributedCache<TCacheItem, TCacheKey>.

示例: 在缓存中存储图书名称和价格

示例缓存项

using Volo.Abp.Caching;

namespace MyProject
{
    [CacheName("Books")]
    public class BookCacheItem
    {
        public string Name { get; set; }

        public float Price { get; set; }
    }
}
  • 在本例中使用CacheName特性给BookCacheItem类设置缓存名称.

你可以注入 IDistributedCache<BookCacheItem, Guid> 服务用于 get/set BookCacheItem 对象.

using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;
using Volo.Abp.Caching;
using Volo.Abp.DependencyInjection;

namespace MyProject
{
    public class BookService : ITransientDependency
    {
        private readonly IDistributedCache<BookCacheItem, Guid> _cache;

        public BookService(IDistributedCache<BookCacheItem, Guid> cache)
        {
            _cache = cache;
        }

        public async Task<BookCacheItem> GetAsync(Guid bookId)
        {
            return await _cache.GetOrAddAsync(
                bookId, //Guid类型作为缓存键
                async () => await GetBookFromDatabaseAsync(bookId),
                () => new DistributedCacheEntryOptions
                {
                    AbsoluteExpiration = DateTimeOffset.Now.AddHours(1)
                }
            );
        }
        private Task<BookCacheItem> GetBookFromDatabaseAsync(Guid bookId)
        {
            //TODO: 从数据库获取数据
        }
    }
}
  • 示例服务中 GetOrAddAsync() 方法获取缓存的图书项.
  • 我们采用了 Guid 做为键, 在 _cache_GetOrAddAsync() 方法中传入 Guid 类型的bookid.

2.8 复杂类型的缓存键

IDistributedCache<TCacheItem, TCacheKey> 在内部使用键对象的 ToString() 方法转换类型为string. 如果你的将复杂对象做为缓存键,那么需要重写类的 ToString 方法.

举例一个作为缓存键的类:

public class UserInOrganizationCacheKey
{
    public Guid UserId { get; set; }
 
    public Guid OrganizationId { get; set; }

    //构建缓存key
    public override string ToString()
    {
        return $"{UserId}_{OrganizationId}";
    }
}

用法示例:

public class BookService : ITransientDependency
{
    private readonly IDistributedCache<UserCacheItem, UserInOrganizationCacheKey> _cache;

    public BookService(
        IDistributedCache<UserCacheItem, UserInOrganizationCacheKey> cache)
    {
        _cache = cache;
    }
    
    ...
}

3. 高级主题

工作单元级别的缓存

分布式缓存服务提供了一个有趣的功能. 假设你已经更新了数据库中某本书的价格, 然后将新价格设置到缓存中, 以便以后使用缓存的值. 如果设置缓存后出现异常, 并且更新图书价格的事务被回滚了, 该怎么办?在这种情况下, 缓存值是错误的.

IDistributedCache<..>方法提供一个可选参数, considerUow, 默认为false. 如果将其设置为true, 则你对缓存所做的更改不会应用于真正的缓存存储, 而是与当前的[工作单元]关联. 你将获得在同一工作单元中设置的缓存值, 但仅当前工作单元成功时更改才会生效.

IDistributedCacheSerializer

IDistributedCacheSerializer服务用于序列化和反序列化缓存内容. 默认实现是Utf8JsonDistributedCacheSerializer类, 它使用IJsonSerializer服务将对象转换为[JSON], 反之亦然. 然后, 它使用UTC8编码将JSON字符串转换为分布式缓存接受的字节数组.

如果你想实现自己的序列化逻辑, 可以自己实现并[替换] 此服务.

IDistributedCacheKeyNormalizer

默认情况下, IDistributedCacheKeyNormalizer是由DistributedCacheKeyNormalizer类实现的. 它将缓存名称、应用程序缓存前缀和当前租户id添加到缓存键中. 如果需要更高级的键规范化, 可以自己实现并[替换] 此服务.

4. Redis 缓存

ABP Framework缓存系统扩展了ASP.NETCore分布式缓存。因此,标准ASP.NETCore分布式缓存支持的任何提供程序都可以在您的应用程序中使用,并且可以像Microsoft记录的一样进行配置。

但是,ABP为Redis Cache提供了一个集成包Volo. Abp.Cching.StackExchange eRedis。使用这个包而不是标准的Microsoft.Exents.Cching.StackExchange eRedis包有两个原因。

  1. 它实现了SetManyAsyncGetManyAsync方法。这些不是微软缓存库的标准方法,而是由ABP框架缓存系统添加的。当您需要使用单个方法调用设置/获取多个缓存项时,它们显着提高了性能。
  2. 简化了Redis缓存配置(将在下面解释)。

Volo.Abp.Caching.StackExchangeRedis 已使用 Microsoft.Extensions.Caching.StackExchangeRedis 包,但对其进行了扩展和改进。

安装

如果使用Redis,则此包已安装在应用程序启动模板中。

.csproj文件的文件夹中打开命令行并键入以下ABP CLI命令:

abp add-package Volo.Abp.Caching.StackExchangeRedis

配置

Volo.Abp.Caching.StackExchangeRedis 包自动从IConfiguration获取Redis配置。因此,例如,您可以在appsettings.json中设置配置:

"Redis": { 
 "IsEnabled": "true",
 "Configuration": "127.0.0.1"
}

设置IsEnabled是可选的,如果未设置,将被视为true

或者,您可以在模块ConfigureServices方法中配置标准RedisCacheOptions选项类:

Configure<RedisCacheOptions>(options =>
{
    //...
});

5. 核心功能点

  1. 缓存配置:通过 DistributedCacheEntryOptions 配置缓存过期时间
  2. 自动失效:通过事件处理器自动处理缓存失效
  3. 批量操作:支持批量获取和设置缓存项
  4. 键生成策略:统一的缓存键生成规则

6. 最佳实践

  1. 缓存粒度:保持缓存项尽可能小,只包含必要数据
  2. 过期策略:根据业务需求设置合理的过期时间
  3. 批量操作:对于列表查询,优先使用批量操作减少IO
  4. 失效机制:确保数据变更时及时失效相关缓存

7. 注意事项

  1. 确保所有缓存操作都是线程安全的
  2. 处理缓存穿透和雪崩问题
  3. 监控缓存命中率和性能
  4. 考虑分布式环境下的缓存一致性
posted @ 2026-01-10 15:50  【唐】三三  阅读(2)  评论(0)    收藏  举报