使用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
相关产品推荐
相关产品推荐

