You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用System.Text.Json爬取数据时触发重复键ArgumentException异常

问题:System.Text.Json解析重复键异常排查与解决

问题描述

在使用System.Text.Json爬取数据至SQL Server的过程中,需从JSON的skus字段(未知键名的对象)中取值,通过JsonNode skus = item["skus"].AsObject().ElementAtOrDefault(0).Value;获取第一个值解析。前几千次循环正常,随后抛出System.ArgumentException,提示重复键存在,异常中的键正是skus下的某个键。

完整错误栈

System.ArgumentException: An item with the same key has already been added. Key: 0rdfs1976wzsx8lj (Parameter 'propertyName')
         at System.Text.Json.ThrowHelper.ThrowArgumentException_DuplicateKey(String paramName, String propertyName)
         at System.Text.Json.Nodes.JsonObject.InitializeIfRequired()
         at System.Text.Json.Nodes.JsonObject.GetEnumerator()
         at System.Linq.Enumerable.TryGetElement[TSource](IEnumerable`1 source, Int32 index, TSource& element)
         at System.Linq.Enumerable.ElementAtOrDefault[TSource](IEnumerable`1 source, Int32 index)
         at BlazorApp1.Server.Jobs.HFJob.Execute(IJobExecutionContext context) in D:\repos\BlazorApp1\Server\Jobs\HFJob.cs:line 67

核心代码逻辑

while (true)
{
    using HttpClient client = new(new HttpClientHandler() { CookieContainer = cookies }) { BaseAddress = baseUri };
    var post = await client.PostAsync(url, content);
    JsonNode jsonNode = JsonNode.Parse(await post.Content.ReadAsStringAsync());
    JsonArray jsonArray = jsonNode["results"].AsArray();
    if (jsonArray.Count != 0)
    {
        foreach (var item in jsonArray)
        {
            JsonNode skus = item["skus"].AsObject().ElementAtOrDefault(0).Value;
            Product product = new()
            {
                // 解析逻辑
            };
            Product product1 = await _context.Products.FindAsync(keyValues: product.Id);
            if (product1 != null)
            {
                // 更新实体
            }
            else
            {
                await _context.Products.AddAsync(product);
                await _context.SaveChangesAsync();
            }
        }
    }
    else
    {
        break;
    }
}

疑问与猜测

  • 未存储JSON对象,解析后应被GC回收,新对象为何与已销毁对象出现键冲突?
  • 爬取数据的键理论上唯一,不应存在重复
  • 怀疑Quartz.net托管的服务存在并发问题,重复请求相同数据,但难以排查
  • 无法手动销毁JSON对象,GC自动管理生命周期,并发问题难以追踪

JSON数据示例

skus:{
  "5c5cnedd7mo7m9qf": {
    "allowBargain": false,
    "bigPackAmount": 1,
    "buyAmount": 1,
    "canSell": true,
    "count": 0,
    "extendCode": "",
    "hasStock": false,
    "innerSellCtrlTags": "",
    "lastAmount": 0,
    "limitBuyAmount": 2147483647,
    "middlePackAmount": 1,
    "minAmount": 1,
    "minOrder": 1,
    "modCount": 1,
    "oldPrice": 0,
    "orgOldPrice": -1,
    "orgPrice": -1,
    "packAmount": 1,
    "packDesc": "",
    "packUnits": "",
    "photo": null,
    "price": 99999,
    "priceEndDiff": "",
    "priceEndTime": "",
    "priceOfOrg": null,
    "priceTip": "",
    "priceType": "",
    "sellCtrlTags": "(041)",
    "sellStateTip": "",
    "skuCode": "22459",
    "skuId": "5c5cnedd7mo7m9qf",
    "specDesc": "",
    "specValueDef": "",
    "specialPrice": -1,
    "stepPriceDesc": "",
    "stockTag": {
      "amount": 0,
      "stockState": "",
      "stockType": ""
    },
    "tokenPrice": 0,
    "units": "",
    "weight": 0
  }
}

可能的原因与解决办法

1. 源JSON存在重复键

实际爬取的JSON可能在skus对象中包含重复键(接口返回异常数据),System.Text.Json默认不允许重复键,解析时触发异常。

  • 解决:解析时配置允许重复键,或自定义处理:
    JsonNode jsonNode = JsonNode.Parse(await post.Content.ReadAsStringAsync(), new JsonNodeOptions { AllowDuplicateNames = true });
    
  • 也可在解析skus前检查并过滤重复键。

2. Quartz.net并发执行冲突

若Job被同时触发多次,多个线程共享_context(DbContext)或cookies等资源,会导致解析或数据库操作并发异常。

  • 解决:给Job添加DisallowConcurrentExecutionAttribute确保单线程执行:
    [DisallowConcurrentExecution]
    public class HFJob : IJob
    {
        // Job逻辑
    }
    
  • 同时,DbContext应在每次Job执行时创建,避免共享实例引发并发冲突。

3. JsonObject枚举器线程不安全

JsonObject.GetEnumerator()并非线程安全,多线程同时枚举同一对象会导致内部状态异常,出现重复键错误。

  • 解决:获取skus时加锁,或转换为字典后取第一个元素:
    // 方式1:加锁
    lock (item)
    {
        JsonNode skus = item["skus"].AsObject().ElementAtOrDefault(0).Value;
        // 解析逻辑
    }
    
    // 方式2:转字典取值
    var skusDict = item["skus"].AsObject().ToDictionary(kv => kv.Key, kv => kv.Value);
    JsonNode skus = skusDict.Values.FirstOrDefault();
    

4. HttpClient资源浪费与连接池问题

每次循环新建HttpClient会导致资源浪费,甚至连接池异常,建议复用实例。

  • 解决:将HttpClient作为单例注入,或在Job外部创建复用:
    // 在Job构造函数注入HttpClient
    private readonly HttpClient _httpClient;
    public HFJob(HttpClient httpClient)
    {
        _httpClient = httpClient;
        _httpClient.BaseAddress = baseUri;
        if (_httpClient.GetHandler() is HttpClientHandler handler)
        {
            handler.CookieContainer = cookies;
        }
    }
    

额外排查步骤

  • 捕获异常时记录当前解析的JSON内容,确认是否存在重复键
  • 监控Quartz.net的Job触发记录,检查是否有并发执行情况
  • 检查数据库中是否存在重复的product.Id,确认是否为数据重复导致的数据库操作冲突

内容的提问来源于stack exchange,提问作者spigelspike0812

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 17:11:11