学习

如何在ASP.NET Core应用中添加基本的API缓存

Redis 是缓存的代名词,这是有充分理由的,Redis 速度快且易于启动和运行,并且作为缓存表现出色。

使用缓存而不是真实数据源有两个主要原因。

  1. 1.时间 - 缓存速度快得多
  2. 2. 成本 - 有时访问真实数据源会产生金钱成本。例如,API 端点有时会按请求次数收费。这意味着我们希望限制对特定端点的不必要请求。

在第二种情况下,对API端点的不必要请求是浪费的,并且随着时间的推移会给应用带来高昂的财务成本。因此,在本教程中,我们将研究如何缓存API请求的结果,以避免我们必须往返访问API。

在本示例中,我们将使用美国国家气象局 (NWS) 的天气API——它是免费的,除了用户代理外不需要任何认证。我们将使用ASP.NET Core构建一个API,根据经纬度获取天气预报。

先决条件#

  • 用于编写C#代码的IDE - Visual Studio, Rider, VS Code等...
  • .NET 6 SDK
  • Docker

启动 Redis#

首先启动Redis;出于开发目的,你可以直接使用Docker

docker run -p 6379:6379 redis

如果你正准备部署到生产环境,你可能希望利用 Redis Cloud

创建项目#

接下来,我们将使用 .NET CLI 创建 ASP.NET Core API 项目。

dotnet new webapi -n BasicWeatherCacheApp

然后我们将 cd 进入我们刚刚创建的 BasicWeatherCacheApp 目录,并向项目添加 StackExchange.Redis 包:

dotnet add package StackExchange.Redis

将 Redis 缓存添加到 ASP.NET Core 应用#

打开 program.cs 文件。这里定义并注入了所有服务到项目中。添加以下代码将 StackExchange.Redis 的 ConnectionMultiplexer Redis 添加到 ASP.NET Core 应用以及一个 HttpClient:

builder.Services.AddSingleton<IConnectionMultiplexer>(ConnectionMultiplexer.Connect("localhost"));
builder.Services.AddHttpClient();

创建数据结构来保存结果#

从NWS获取的结果结构有点冗长,但我们将努力只捕获特定区域的未来预报。

我们将创建两个结构,第一个包含实际的预报,第二个包含给定请求的预报列表以及累积预报所花费的时间。对于第一个结构,我们将使用模板中创建的默认 WeatherForecast 类,打开 WeatherForecast.cs,并将其内容替换为

public class WeatherForecast
{
    [JsonPropertyName("number")]
    public int Number { get; set; }

    [JsonPropertyName("name")]
    public string Name { get; set; }

    [JsonPropertyName("startTime")]
    public DateTime StartTime { get; set; }

    [JsonPropertyName("endTime")]
    public DateTime EndTime { get; set; }

    [JsonPropertyName("isDayTime")]
    public bool IsDayTime { get; set; }

    [JsonPropertyName("temperature")]
    public int Temperature { get; set; }

    [JsonPropertyName("temperatureUnit")]
    public string? TemperatureUnit { get; set; }

    [JsonPropertyName("temperatureTrend")]
    public string? TemperatureTrend { get; set; }

    [JsonPropertyName("windSpeed")]
    public string? WindSpeed { get; set; }

    [JsonPropertyName("windDirection")]
    public string? WindDirection { get; set; }

    [JsonPropertyName("shortForecast")]
    public string? ShortForecast { get; set; }

    [JsonPropertyName("detailedForecast")]
    public string? DetailedForecast { get; set; }
}

接下来,创建文件 ForecastResult.cs 并向其添加以下内容:

public class ForecastResult
{
    public long ElapsedTime { get; }
    public IEnumerable<WeatherForecast> Forecasts { get; }

    public ForecastResult(IEnumerable<WeatherForecast> forecasts, long elapsedTime)
    {
        Forecasts = forecasts;
        ElapsedTime = elapsedTime;
    }
}

将依赖项注入到天气预报控制器#

现在我们已经设置好了应用,我们需要配置控制器。首先,打开 Controllers/WeatherForecastController (这个控制器是随模板自动创建的)并添加以下代码注入我们所需的内容。

private readonly HttpClient _client;
private readonly IDatabase _redis;

public WeatherForecastController(HttpClient client, IConnectionMultiplexer muxer)
{
    _client = client;
    _client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("weatherCachingApp","1.0") );
    _redis = muxer.GetDatabase();
}

查询API#

要查询天气API以查找特定经纬度的预报,我们需要经历两个步骤。首先,没有直接基于地理位置查询预报的API。相反,每个地理位置都被分配到一个特定的办公室进行监测,并且每个办公室都有一个2D网格,特定的经纬度会映射到该网格。幸运的是,有一个 points API端点,你可以将经纬度传递给它。这会给你返回该点所属的有效办公室以及该点的x/y网格坐标。你需要查询该办公室对应网格点的预报端点,然后提取预报周期。以下代码完成了这一切。

private async Task<string> GetForecast(double latitude, double longitude)
{
    var pointsRequestQuery = $"https://api.weather.gov/points/{latitude},{longitude}"; //get the URI
    var result = await _client.GetFromJsonAsync<JsonObject>(pointsRequestQuery);
    var gridX = result["properties"]["gridX"].ToString();
    var gridY = result["properties"]["gridY"].ToString();
    var gridId = result["Properties"]["gridId"].ToString();
    var forecastRequestQuery = $"https://api.weather.gov/gridpoints/{gridId}/{gridX},{gridY}/forecast";
    var forecastResult = await _client.GetFromJsonAsync<JsonObject>(forecastRequestQuery);
    var periodsJson = forecastResult["properties"]["periods"].ToJsonString();
    return periodsJson;
}

编写预报操作#

考虑到多次API调用,很明显为什么使用缓存对我们的应用至关重要。这些预报更新不频繁,每1-3小时更新一次。这意味着连续两次发起API请求在时间和金钱上都可能代价高昂。就这个API而言,请求不涉及财务成本。然而,对于商业API,通常会有按请求次数收费的情况。在编写这个操作时,我们将检查缓存。如果缓存包含相关的预报,我们将直接返回。否则,我们将访问API,保存结果,并设置缓存键的过期时间。我们将记录时间,然后返回结果和所花费的时间。

[HttpGet(Name = "GetWeatherForecast")]
public async Task<ForecastResult> Get([FromQuery] double latitude, [FromQuery] double longitude)
{
    string json;
    var watch = Stopwatch.StartNew();
    var keyName = $"forecast:{latitude},{longitude}";
    json = await _redis.StringGetAsync(keyName);
    if (string.IsNullOrEmpty(json))
    {
        json = await GetForecast(latitude, longitude);
        var setTask = _redis.StringSetAsync(keyName, json);
        var expireTask = _redis.KeyExpireAsync(keyName, TimeSpan.FromSeconds(3600));
        await Task.WhenAll(setTask, expireTask);
    }

    var forecast =
        JsonSerializer.Deserialize<IEnumerable<WeatherForecast>>(json);
    watch.Stop();
    var result = new ForecastResult(forecast, watch.ElapsedMilliseconds);

    return result;
}

运行应用#

现在只剩下运行应用了。在控制台中运行 dotnet run ,然后打开 https://localhost:PORT_NUMBER/swagger/index.html 并使用GUI发送请求。或者,你可以使用cURL发送请求。第一次发送新的经纬度时,你会注意到发送请求需要相当长的时间,大约1秒。当你再次发送请求,并且请求命中缓存时,时间会急剧下降到大约1-5毫秒。

资源#

  • 此演示的源码位于 GitHub
  • StackExchange.Redis 库的更多文档位于其 文档站点