Redis 是缓存的代名词,这有充分的理由,Redis 速度快,易于上手,并且作为缓存表现出色。
使用缓存而不是真实来源有两个主要原因。
在第二种情况下,对 API 端点的无必要请求是浪费的,并且随着时间的推移会增加应用程序的高昂财务成本。因此,在本教程中,我们将研究缓存 API 请求的结果,以防止我们必须进行往返 API 的操作。
在我们的示例中,我们将使用美国国家气象局 (NWS) 的天气 API - 该 API 免费且除了用户代理之外不需要任何身份验证。我们将构建一个 API,使用 ASP.NET Core 根据纬度和经度获取天气预报。
让我们从启动 redis 开始;为了开发目的,您只需使用 docker
如果您准备部署到生产环境,您可能需要使用 Redis Cloud
接下来,我们将使用 .NET CLI 创建 ASP.NET Core API 项目。
然后,我们将 cd 到我们刚刚创建的 BasicWeatherCacheApp 目录,并将 StackExchange.Redis 包添加到项目中:
打开 program.cs 文件。这是定义所有服务并将其注入项目的位置。添加以下内容以将 StackExchange.Redis ConnectionMultiplexer Redis 添加到 ASP.NET Core 应用程序,以及一个 HttpClient:
NWS 生成的结构有点冗长,但我们将努力仅捕获特定区域的未来预报。
我们将创建两个结构,第一个将包含实际的预报,第二个将包含来自给定请求的预报列表,以及累积预报所需的时间。对于第一个,我们将使用模板中创建的默认 WeatherForecast 类,打开 WeatherForecast.cs,并将它的内容替换为
接下来,创建文件 ForecastResult.cs 并向其中添加以下内容:
现在我们已经设置了应用程序,我们需要配置我们的控制器。首先,打开 Controllers/WeatherForecastController (此控制器会与模板一起自动创建),并将以下代码添加到其中以将我们需要的注入到其中。
为了查询天气 API 以找到特定纬度和经度的预报,我们需要经历一个两步过程。首先,没有根据地理位置查询预报的自然 API。相反,每个地理位置都分配了一个特定的办公室,它负责监控该地理位置,并且每个办公室都有一个二维网格,特定纬度和经度将映射到该网格。幸运的是,有一个 points API 端点,您可以将您的纬度和经度传递到该端点。这将为您提供点有效的特定办公室以及该点的 x/y 网格坐标。您需要查询该办公室的该网格点的预报端点,然后提取预测的时间段。以下是所有这些的操作。
鉴于多个 API 调用,很明显为什么在我们的应用程序中使用缓存至关重要。这些预报并不经常更新,每 1-3 小时更新一次。这意味着两次连续的 API 请求在时间和金钱上都可能很昂贵。对于此 API,与请求无关的财务成本。但是,对于商业 API,通常会产生按请求收费的成本。当我们编写此操作时,我们将检查缓存。如果缓存包含相关的预报,我们将返回该预报。否则,我们将访问 API,保存结果,并将密钥设置为过期。我们将计时,然后回复结果并计时。
现在剩下要做的就是运行应用程序。在您的控制台中运行 dotnet run ,并打开 https://localhost:PORT_NUMBER/swagger/index.html 并使用 GUI 发送请求。或者,您可以使用 cURL 发送请求。第一次发送新的纬度和经度时,您会注意到发送请求需要相当长的时间,大约 1 秒。当您再次发出请求并命中缓存时,它将急剧下降到大约 1-5 毫秒。
docker run -p 6379:6379 redis
dotnet new webapi -n BasicWeatherCacheApp
dotnet add package StackExchange.Redis
builder.Services.AddSingleton<IConnectionMultiplexer>(ConnectionMultiplexer.Connect("localhost"));
builder.Services.AddHttpClient();
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; }
}
public class ForecastResult
{
public long ElapsedTime { get; }
public IEnumerable<WeatherForecast> Forecasts { get; }
public ForecastResult(IEnumerable<WeatherForecast> forecasts, long elapsedTime)
{
Forecasts = forecasts;
ElapsedTime = elapsedTime;
}
}
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();
}
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;
}
[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;
}