ASP.NET Core Web API中EF Core数据更新时自动更新MemoryCache方案咨询
这问题太常见了,手动到处调用缓存更新确实容易漏写,也不优雅。我给你分享两种最常用的方案,都能彻底解决这个痛点:
方案一:重写DbContext的
SaveChanges/SaveChangesAsync方法 这是最直接的实现方式,不需要额外依赖,适合大多数中小型项目。核心思路是在EF Core保存数据的统一入口处,自动检测变更的实体并触发缓存同步。
步骤1:定义标记接口(筛选需要缓存同步的实体)
先给需要缓存同步的实体加一个标记接口,方便后续在DbContext中识别它们:
public interface ICacheableEntity { // 返回该实体对应的缓存键前缀,用于批量清理缓存 string CacheKeyPrefix { get; } } // 示例实体:比如Product表对应的实体 public class Product : ICacheableEntity { public int Id { get; set; } public string Name { get; set; } public string CacheKeyPrefix => "products:"; }
步骤2:在DbContext中注入缓存服务并重写保存方法
把你的缓存服务(不管是IMemoryCache还是IDistributedCache的封装)注入到DbContext,然后重写保存方法,自动处理缓存失效:
public class AppDbContext : DbContext { private readonly ICacheService _cacheService; public AppDbContext(DbContextOptions<AppDbContext> options, ICacheService cacheService) : base(options) { _cacheService = cacheService; } // 重写同步保存方法 public override int SaveChanges(bool acceptAllChangesOnSuccess) { HandleCacheInvalidation(); return base.SaveChanges(acceptAllChangesOnSuccess); } // 重写异步保存方法 public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) { HandleCacheInvalidation(); return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); } // 核心:处理缓存失效逻辑 private void HandleCacheInvalidation() { // 获取所有新增/修改/删除的、需要缓存同步的实体 var changedCacheableEntities = ChangeTracker.Entries() .Where(e => e.Entity is ICacheableEntity && (e.State == EntityState.Added || e.State == EntityState.Modified || e.State == EntityState.Deleted)) .Select(e => (ICacheableEntity)e.Entity); foreach (var entity in changedCacheableEntities) { // 根据前缀批量清理对应缓存(比如删除所有"products:"开头的缓存键) _cacheService.RemoveByPrefix(entity.CacheKeyPrefix); // 如果你需要主动更新缓存而不是删除,可以在这里重新查询最新数据并写入缓存 // 不过通常删除缓存让下次请求自动加载更简单,避免并发更新的一致性问题 } } }
步骤3:缓存服务封装示例
这里以分布式缓存为例,封装一个支持按前缀删除的服务:
public interface ICacheService { void RemoveByPrefix(string prefix); // 其他缓存操作方法,比如Get、Set等 } public class CacheService : ICacheService { private readonly IDistributedCache _distributedCache; public CacheService(IDistributedCache distributedCache) { _distributedCache = distributedCache; } public void RemoveByPrefix(string prefix) { // 注意:如果用Redis,生产环境不要用KEYS命令,改用SCAN迭代删除 // 这里是简化示例,具体实现根据你的缓存中间件调整 // 比如内存缓存可以遍历所有键匹配前缀后删除 // 分布式缓存建议提前规划键结构,用哈希表或前缀分组管理 } }
方案二:使用领域事件(更解耦,适合复杂业务场景)
如果你的项目采用领域驱动设计(DDD),或者需要将缓存逻辑与数据访问层解耦,领域事件是更优雅的选择。核心思路是在实体变更时发布事件,由专门的事件处理器来处理缓存更新。
步骤1:定义领域事件
// 通用的实体变更事件 public record EntityChangedEvent<T>(T Entity, EntityState ChangeState) where T : ICacheableEntity;
步骤2:在DbContext中发布事件
用事件总线(比如MediatR)在保存数据前发布实体变更事件:
public class AppDbContext : DbContext { private readonly IMediator _mediator; public AppDbContext(DbContextOptions<AppDbContext> options, IMediator mediator) : base(options) { _mediator = mediator; } public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) { await PublishEntityChangedEvents(cancellationToken); return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); } private async Task PublishEntityChangedEvents(CancellationToken cancellationToken) { var changedEntities = ChangeTracker.Entries() .Where(e => e.Entity is ICacheableEntity && (e.State == EntityState.Added || e.State == EntityState.Modified || e.State == EntityState.Deleted)) .Select(e => new { Entity = (ICacheableEntity)e.Entity, e.State }); foreach (var item in changedEntities) { await _mediator.Publish(new EntityChangedEvent<ICacheableEntity>(item.Entity, item.State), cancellationToken); } } }
步骤3:编写事件处理器
专门的处理器负责缓存更新逻辑,完全与DbContext解耦:
public class EntityChangedCacheHandler<T> : INotificationHandler<EntityChangedEvent<T>> where T : ICacheableEntity { private readonly ICacheService _cacheService; public EntityChangedCacheHandler(ICacheService cacheService) { _cacheService = cacheService; } public Task Handle(EntityChangedEvent<T> notification, CancellationToken cancellationToken) { // 根据实体前缀清理缓存 _cacheService.RemoveByPrefix(notification.Entity.CacheKeyPrefix); // 如需主动更新缓存,可在此处查询最新数据并写入 return Task.CompletedTask; } }
注意事项
- 缓存策略选择:优先选择删除缓存而非主动更新,避免并发场景下的缓存不一致问题(比如更新缓存时刚好有请求读取旧数据)。
- 事务一致性:如果SaveChanges失败,缓存操作也应该回滚。可以把缓存操作放在事务内,或者设计补偿机制。
- 缓存键设计:一定要有清晰的前缀规则,比如
"products:{Id}",方便批量清理。 - 异步性能:用领域事件时,可选择异步发布事件,不阻塞SaveChanges的执行,提升接口响应速度,但要考虑缓存更新失败的容错机制。
内容的提问来源于stack exchange,提问作者PNR
相关产品推荐
相关产品推荐

