API的幂等性(Idempotent),是指调用某个方法1次或N次对资源产生的影响结果都是相同的。
GET请求默认是幂等的,因为它只是查询资源,而不会修改资源。
而POST请求默认是不幂等的,多次调用POST方法可能会产生不同的结果,并会创建多个资源。
想象一下,你在扫码支付时,输入金额后点击了2次“确定”按钮,肯定不希望扣2次款。
幂等性保证了操作只会执行一次。
1、思路
使用ASP.NET Core过滤器来处理POST请求,检查请求头【Headers】中的幂等键(IdempotencyKey)。
如果在缓存中未检查到IdempotencyKey,则真实执行操作并缓存响应数据,否则直接返回缓存的响应数据。
这样,操作只能对资源产生一次影响。
2、IdempotentAttributeFilter
创建自定义Filter,使用OnActionExecuting方法在执行操作前检查缓存,如有缓存直接返回context.Result;使用OnResultExecuted方法在执行操作后缓存响应。
- using Microsoft.AspNetCore.Http;
- using Microsoft.AspNetCore.Mvc;
- using Microsoft.AspNetCore.Mvc.Filters;
- using Microsoft.Extensions.Caching.Distributed;
- using Newtonsoft.Json;
- using System;
- using System.Collections.Generic;
- using System.IO;
- using System.Linq;
- using System.Threading.Tasks;
-
- namespace WebApi
- {
- ///
- ///
- ///
- public class IdempotentAttributeFilter : IActionFilter, IResultFilter
- {
- private readonly IDistributedCache _distributedCache;
- private bool _isIdempotencyCache = false;
- const string IdempotencyKeyHeaderName = "IdempotencyKey";
- private string _idempotencyKey;
-
- ///
- ///
- ///
- ///
- public IdempotentAttributeFilter(IDistributedCache distributedCache)
- {
- _distributedCache = distributedCache;
- }
-
- ///
- ///
- ///
- ///
- public void OnActionExecuting(ActionExecutingContext context)
- {
- Microsoft.Extensions.Primitives.StringValues idempotencyKeys;
- context.HttpContext.Request.Headers.TryGetValue(IdempotencyKeyHeaderName, out idempotencyKeys);
- _idempotencyKey = idempotencyKeys.ToString();
-
- var cacheData = _distributedCache.GetString(GetDistributedCacheKey());
- if (cacheData != null)
- {
- context.Result = JsonConvert.DeserializeObject
(cacheData); - _isIdempotencyCache = true;
- return;
- }
- }
-
- ///
- ///
- ///
- ///
- public void OnResultExecuted(ResultExecutedContext context)
- {
- //已缓存
- if (_isIdempotencyCache)
- {
- return;
- }
-
- var contextResult = context.Result;
-
- DistributedCacheEntryOptions cacheOptions = new DistributedCacheEntryOptions();
-
- //相对过期时间
- //cacheOptions.SlidingExpiration = TimeSpan.FromSeconds(10);
- //绝对过期时间
- cacheOptions.AbsoluteExpirationRelativeToNow = new TimeSpan(24, 0, 0);
-
- //缓存:
- _distributedCache.SetString(GetDistributedCacheKey(), JsonConvert.SerializeObject(contextResult), cacheOptions);
- }
-
- ///
- ///
- ///
- ///
- public void OnActionExecuted(ActionExecutedContext context)
- {
- }
-
- ///
- ///
- ///
- ///
- public void OnResultExecuting(ResultExecutingContext context)
- {
- }
-
- private string GetDistributedCacheKey()
- {
- return "Idempotency:" + _idempotencyKey;
- }
- }
- }
3、创建自定义Attribute
声明了IdempotentAttribute的Class或者Method,在运行时会创建IdempotentAttributeFilter。
- using Microsoft.AspNetCore.Http;
- using Microsoft.AspNetCore.Mvc;
- using Microsoft.AspNetCore.Mvc.Filters;
- using Microsoft.Extensions.Caching.Distributed;
- using Newtonsoft.Json;
- using System;
- using System.Collections.Generic;
- using System.IO;
- using System.Linq;
- using System.Threading.Tasks;
-
- namespace WebApi
- {
- ///
- ///
- ///
- [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
- public class IdempotentAttribute : Attribute, IFilterFactory
- {
- ///
- ///
- ///
- public bool IsReusable => false;
-
- ///
- ///
- ///
- ///
- ///
- public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
- {
- var distributedCache = (IDistributedCache)serviceProvider.GetService(typeof(IDistributedCache));
-
- var filter = new IdempotentAttributeFilter(distributedCache);
- return filter;
- }
- }
- }
4、新建ASP.NET Core Web API项目
创建 WeatherForecastController 控制器,为Post方法加上【Idempotent】
这里用一个静态变量模拟数据库,POST请求写入数据,GET请求读取数据
- using Microsoft.AspNetCore.Mvc;
- using Microsoft.Extensions.Logging;
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Net;
- using System.Threading.Tasks;
-
- namespace WebApi.Controllers
- {
- ///
- ///
- ///
- [Route("api/[controller]")]
- [ApiController]
- public class WeatherForecastController : ControllerBase
- {
- private static List
_db = new List(); -
- private static readonly string[] Summaries = new[]
- {
- "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
- };
-
- ///
- ///
- ///
- public WeatherForecastController()
- {
- }
-
- ///
- ///
- ///
- ///
- ///
- [Idempotent]
- [HttpPost]
- public WeatherForecast Post(int temperature)
- {
- var data = new WeatherForecast { TemperatureC = temperature };
- _db.Add(data);
-
- return data;
- }
-
- ///
- ///
- ///
- ///
- [HttpGet()]
- public IEnumerable
Get() - {
- var rng = new Random();
- return _db.Select(p => new WeatherForecast
- {
- TemperatureC = p.TemperatureC,
- Summary = Summaries[rng.Next(Summaries.Length)]
- })
- .ToArray();
- }
- }
-
- public class WeatherForecast
- {
- public DateTime Date { get; set; }
-
- public int TemperatureC { get; set; }
-
- public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
-
- public string Summary { get; set; }
- }
- }
5、注册分布式缓存
必须增加分布式缓存,用于保存幂等键的值和响应数据。
管理 NuGet 程序包(N)...
- Microsoft.Extensions.Caching.SqlServer
- Microsoft.Extensions.Caching.Redis
- Microsoft.Extensions.Caching.StackExchangeRedis
Startup.cs
- public void ConfigureServices(IServiceCollection services)
- {
- //分布式 SQL Server 缓存
- services.AddDistributedSqlServerCache(opt =>
- {
- opt.ConnectionString = Configuration.GetConnectionString("DefaultConnection");
- opt.SchemaName = "dbo";
- opt.TableName = "sys_distributed_cache";
- opt.DefaultSlidingExpiration = TimeSpan.FromMinutes(10);
- opt.ExpiredItemsDeletionInterval = TimeSpan.FromMinutes(5);
- });
-
- //分布式 Redis 缓存
- services.AddDistributedRedisCache(cfg =>
- {
- cfg.Configuration = Configuration.GetConnectionString("RedisConnection");
- });
-
- //分布式 StackExchangeRedis 缓存
- services.AddStackExchangeRedisCache(options =>
- {
- options.Configuration = "localhost";
- options.InstanceName = "SampleInstance";
- });
-
- //分布式缓存
- services.AddDistributedMemoryCache();
- }
在数据库中新建一个名叫“CacheDB”的数据库,然后以管理员身份cmd运行下面指令,会创建一张名叫“CacheTable”表,相应的缓存信息都存在于这张表中。
- dotnet sql-cache create <connection string> <schema> <table>
-
- dotnet sql-cache create "Server=localhost;User=sa;Password=000000;Database=CacheDB" dbo CacheTable
成功后会提示【Table and index were created successfully】
表结构
- CREATE TABLE [dbo].[CacheTable](
- [Id] [nvarchar](449) NOT NULL,
- [Value] [varbinary](max) NOT NULL,
- [ExpiresAtTime] [datetimeoffset](7) NOT NULL,
- [SlidingExpirationInSeconds] [bigint] NULL,
- [AbsoluteExpiration] [datetimeoffset](7) NULL,
- CONSTRAINT [pk_Id] PRIMARY KEY CLUSTERED
- (
- [Id] ASC
- )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
- ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
-
- CREATE NONCLUSTERED INDEX [Index_ExpiresAtTime] ON [dbo].[CacheTable]
- (
- [ExpiresAtTime] ASC
- )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
6、测试
运行Web API,使用不同IdempotencyKey执行POST请求,然后获取数据
code
- POST /api/WeatherForecast?temperature=1000 HTTP/1.1
- Host: localhost:8001
- IdempotencyKey: 1000
*
*