我有一个通过 API 调用与第三方服务集成的微服务。为了处理集成,我创建了一个类型化的
HttpClient
,其中 HttpMessageHandler
注册为 transient
服务,以根据传入请求设置标头(因为标头特定于微服务的调用者)。
流程如下:
问题: 当我们将微服务部署到生产环境并开始处理多个客户端时,我们遇到了并发问题:记录了账户 X 的数据,但对第三方的请求是在账户 Y 下处理的。
为了调查这一点,我尝试了:
我在发送请求之前和之后立即在 HttpMessageHandler 中记录了标头信息(帐户)
我使用 Serilog 作为记录器,因此我安装了 NuGet 包 serilog-httpclient 并记录了所有内容。
我直接从控制器使用了 HttpClient 的单例实例,并在其中一个 API 中使用了它。
第一种和第二种方式总是产生相同的结果,即在向第三方发送请求之前和之后,标头是正确的,但第三方响应帐户中不存在数据的错误,而它事实上确实如此。但是,当我采用第三种解决方案时,该特定 API 中不再发生该问题,但其他 API 仍然遇到相同的问题。
我不确定问题是由于我的代码中的并发性还是其他原因造成的。日志始终显示正确的标头,但不知何故,请求到达第三方服务时帐户信息不正确。这是否仍然是我的服务中的并发问题,或者问题是否可能出在第三方服务上?
这是我的代码:
Startup.cs
services.AddScoped<IMySession, MySession>();
services.AddScoped<IAccountService, AccountService>();
services.AddTransient<MyAuthenticationMessageHandler>();
services.AddHttpClient<MyAPI>(c =>
{
c.BaseAddress = new Uri(configuration["MyAPIUrl"]);
})
.LogRequestResponse(p =>
{
p.LogMode = LogMode.LogAll;
p.RequestHeaderLogMode = LogMode.LogAll;
p.RequestBodyLogMode = LogMode.LogAll;
p.ResponseHeaderLogMode = LogMode.LogAll;
p.ResponseBodyLogMode = LogMode.LogAll;
p.MaskedProperties.Clear();
})
.AddHttpMessageHandler<MyAuthenticationMessageHandler>();
MySession.cs
public class MySession : IMySession
{
private readonly IHttpContextAccessor _httpContextAccessor;
public MySession(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public int TenantId
{
get
{
return int.Parse(_httpContextAccessor.HttpContext.User.FindFirst(Consts.ClaimTenantId).Value);
}
}
public int SystemId
{
get
{
return int.Parse(_httpContextAccessor.HttpContext.User.FindFirst(Consts.ClaimSystemId).Value);
}
}
}
MyController.cs
public class MyController : MyControllerBase
{
private readonly MyAPI _myAPI;
public MyController(MyAPI myAPI)
: base(serviceProvider)
{
_myAPI = myAPI;
}
[HttpGet("Status")]
public async Task<CallResult<int?>> GetStatusAsync(int myId)
{
try
{
var response = await _myAPI.GetContractAsync();
//rest of the code
}
catch (Exception ex)
{
return PrepareFailedCallResult<int?>(ex);
}
}
}
MyAuthenticationMessagehandler.cs
public class MyAuthenticationMessageHandler : DelegatingHandler
{
private readonly IHttpContextAccessor _httpContextAccessor;
public MyAuthenticationMessageHandler(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var accountService = _httpContextAccessor.HttpContext.RequestServices.GetRequiredService<IAccountService>();
var accountInfo = await accountService.GetAccountInfo();
request.Headers.Add(APIKeys.APIId, accountInfo.APIId);
request.Headers.Add(APIKeys.APIKey, accountInfo.APIKey);
request.Headers.Add(APIKeys.APIAuthorizationToken, accountInfo.APIAuthorizationToken);
return await base.SendAsync(request, cancellationToken);
}
}
ApplicationServiceBase.cs
public abstract class ApplicationServiceBase
{
protected readonly IMySession Session;
public ApplicationServiceBase(IMapper mapper, IServiceProvider serviceProvider)
{
Session = serviceProvider.GetService<ITajeerSession>();
}
}
AccountService.cs
public class AccountService : ApplicationServiceBase, IAccountService
{
private readonly IRepository<Account> _accountRepository;
public AccountService(IRepository<Account> accountRepository, IMapper mapper, IServiceProvider serviceProvider) : base(mapper, serviceProvider)
{
_accountRepository = accountRepository;
}
public async Task<AccountInfoDto> GetAccountInfo()
{
Expression<Func<Account, bool>> predicate =
account =>
account.TenantId == Session.TenantId &&
account.SystemId == Session.SystemId;
var account = (await _accountRepository.All(predicate)).Select(a => _mapper.Map<AccountInfoDto>(a)).FirstOrDefault();
if (account == null)
throw new Exception($"Account with TenantId:{Session.TenantId}, SystemId:{Session.SystemId} is invalid");
return account;
}
}
我编写的代码结果是没有错误的,并且永远不会导致数据并发问题。问题来自于我正在集成的服务提供商。他们承认他们遇到了并发问题,过了一段时间,他们就解决了这个问题。问题解决了。