用NetCore + ReactJS 实现一个前后端分离的网站 (6) 缓存机制

1. 前言

对于实时性要求不高的资源,我们一般可以利用缓存机制来降低数据库的请求压力。
轻量级的应用可以用自带的MemoryCache,如果对缓存的高并发、持久化有要求的可以用Redis。
本节用MemoryCache来实现缓存机制。

2. 自定义属性

先创建一个ApiCacheAttribute,接受一个缓存时间。
给方法添加属性,这里传了一个参数60,意思是返回值会在缓存里存在60秒。

ApiCacheAttribute.cs
using Microsoft.AspNetCore.Mvc.Filters;

namespace NovelTogether.Core.API.Attributes
{
    // IFilterMetadata 是为了在过滤器中能够获取到这个自定义属性
    public class ApiCacheAttribute: Attribute, IFilterMetadata
    {
        public int DurationSeconds { get; set; }

        public ApiCacheAttribute(int durationSeconds)
        {
            DurationSeconds = durationSeconds;
        }
    }
}

NovelController.cs
[HttpGet("{id}")]
[ApiAuthorize]
[ApiCache(60)]
public async Task<ResponseModel<Novel>> Get(int id)
{
    var novel = await _novelService.SelectAsync(x => x.ID == id);
    return new ResponseModel<Novel>().Ok(novel);
}

3. 缓存的存取实现

ICacheHelper.cs
namespace NovelTogether.Core.API.Helpers.Cache
{
    public interface ICacheHelper
    {
        public object? Get(object key);
        public void Set(object key, object value, TimeSpan relativeTimeToNow);
    }
}

MemoryCacheHelper
using Microsoft.Extensions.Caching.Memory;

namespace NovelTogether.Core.API.Helpers.Cache
{
    public class MemoryCacheHelper : ICacheHelper
    {
        private readonly IMemoryCache _cache;
        public MemoryCacheHelper(IMemoryCache cache)
        {
            _cache = cache;
        }

        public object? Get(object key)
        {
            return _cache.Get(key);
        }

        public void Set(object key, object value, TimeSpan relativeTimeToNow)
        {
            _cache.Set(key, value, relativeTimeToNow);
        }
    }
}

3. 过滤器(MemoryCache)

通过过滤器过滤请求,把方法名和参数值作为key,返回值作为value存入缓存中。
当过滤器收到相同请求,并且缓存没有超时,那么就直接返回缓存中的value,并且记log。

CacheFilter.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using NovelTogether.Core.API.Attributes;
using NovelTogether.Core.API.Helpers.Cache;
using NovelTogether.Core.API.Utils;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Unicode;

namespace NovelTogether.Core.API.Filters
{
    public class CacheFilter : IActionFilter
    {
        private readonly ICacheHelper _cache;
        private readonly ILogger<CacheFilter> _logger;
        
        public  CacheFilter(ICacheHelper cache, ILogger<CacheFilter> logger)
        {
            _cache = cache;
            _logger = logger;
        }

        public void OnActionExecuted(ActionExecutedContext context)
        {
            var cacheKey = context.HttpContext.Items[Consts.CACHE_KEY];
            if (cacheKey != null)
            {
                cacheKey = cacheKey.ObjToString();
                var duration = int.Parse(context.HttpContext.Items[Consts.CACHE_DURATION_SECONDS].ObjToString());
                _cache.Set(cacheKey, context.Result, new TimeSpan(0, 0, duration));
            }
        }

        public void OnActionExecuting(ActionExecutingContext context)
        {
            // 如果方法声明了属性[ApiCache],就把资源加入缓存。
            var filters = context.ActionDescriptor.FilterDescriptors;
            foreach (var filter in filters)
            {
                if (filter.Filter is ApiCacheAttribute)
                {
                    var cacheAttribute = (ApiCacheAttribute)filter.Filter;

                    var action = (Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor)context.ActionDescriptor;
                    var controllerName = action.ControllerTypeInfo.FullName;
                    var actionName = action.ActionName;

                    var option = new JsonSerializerOptions()
                    {
                        Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
                    };

                    var parameters = JsonSerializer.Serialize(context.ActionArguments, option);

                    var cacheKey = $"{controllerName}.{actionName}_{parameters}";

                    var result = _cache.Get(cacheKey);

                    if (result == null)
                    {
                        // 提取方法名和参数,给OnActionExecuted使用
                        context.HttpContext.Items.Add(Consts.CACHE_KEY, cacheKey);
                        context.HttpContext.Items.Add(Consts.CACHE_DURATION_SECONDS, cacheAttribute.DurationSeconds);
                    }
                    else
                    {
                        // 从缓存中取出数据,直接返回
                        context.Result = (IActionResult)result;
                        var uniqueId = context.HttpContext.Items[Consts.UNIQUE_ID].ObjToString();
                        _logger.LogInformation($"Action Executing\r\n唯一标识:{uniqueId}\r\n触发缓存,键值为: {cacheKey}");
                    }

                    return;
                }
            }
        }
    }
}

4. 添加服务

Program.cs
builder.Services.AddControllers(option =>
{
    // 添加日志过滤器
    option.Filters.Add(typeof(LogFilter));
    // 添加缓存过滤器
    option.Filters.Add(typeof(CacheFilter));
    // 添加全局异常过滤器
    option.Filters.Add(typeof(GlobalExceptionFilter));
});
// 添加缓存服务
builder.Services.AddMemoryCache();
// 其他代码
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory(containerBuilder =>
{
    // 其他代码

    // 注册自定义缓存服务
    containerBuilder.RegisterType<MemoryCacheHelper>().As<ICacheHelper>().SingleInstance();

    // 其他代码
}));

5. 执行方法

我执行三次方法,前两次参数一样,从图中可以看出,第二次是从缓存中获取的。第三次换了参数,就没有从缓存中取值。
image

posted @ 2022-12-08 13:58  王一乙  阅读(119)  评论(0编辑  收藏  举报