Redis 是缓存的代名词,这是有充分理由的,Redis 速度快且易于启动和运行,并且作为缓存表现出色。
使用缓存而不是真实数据源有两个主要原因。
在第二种情况下,对API端点的不必要请求是浪费的,并且随着时间的推移会给应用带来高昂的财务成本。因此,在本教程中,我们将研究如何缓存API请求的结果,以避免我们必须往返访问API。
在本示例中,我们将使用美国国家气象局 (NWS) 的天气API——它是免费的,除了用户代理外不需要任何认证。我们将使用ASP.NET Core构建一个API,根据经纬度获取天气预报。
首先启动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
打开 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。相反,每个地理位置都被分配到一个特定的办公室进行监测,并且每个办公室都有一个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毫秒。