缓存行星数据的服务长时间闲置后触发IndexOutOfRangeException求助
问题分析
你的PlanetService存在线程安全问题,这是触发IndexOutOfRangeException的核心原因:
List<T>并非线程安全集合,当多个线程同时执行FindAll或FindByName时,可能出现一个线程在执行planets.AddRange(修改集合内部结构),另一个线程同时枚举planets(比如调用Any()、Where或Select)的冲突场景,这会破坏List<T>的内部数组结构,引发索引越界异常。- 异常触发后,
List<T>的内部状态会处于损坏状态,后续所有访问都会持续报错,直到应用重启或缓存被重置。 - 生产环境的并发访问场景是问题触发的关键,开发环境因并发量低难以复现。
修复方案
以下是几种可行的修复方式,按推荐程度排序:
1. 使用Lazy<T>实现线程安全的惰性初始化
Lazy<T>默认具备线程安全性,能确保缓存仅被初始化一次,彻底避免并发修改/枚举的冲突:
public class PlanetService { private readonly Lazy<List<Planet>> _planets = new Lazy<List<Planet>>(() => GetAllPlanetsFromDataBase().ToList()); public List<Planet> FindAll() { return _planets.Value; } public List<Planet> FindByName(string name) { return _planets.Value.Where(x => x.Name == name).ToList(); } // 假设GetAllPlanetsFromDataBase是返回IEnumerable<Planet>的数据库查询方法 private static IEnumerable<Planet> GetAllPlanetsFromDataBase() { // 数据库查询逻辑 } }
2. 加锁实现线程安全的缓存初始化
通过lock关键字确保缓存初始化过程的原子性,避免并发冲突:
public class PlanetService { private List<Planet> _planets = new List<Planet>(); private readonly object _lockObj = new object(); public List<Planet> FindAll() { if (!_planets.Any()) { lock (_lockObj) { // 双重检查锁定,避免重复初始化 if (!_planets.Any()) { _planets.AddRange(GetAllPlanetsFromDataBase()); } } } return _planets; } public List<Planet> FindByName(string name) { if (!_planets.Any()) { lock (_lockObj) { if (!_planets.Any()) { _planets.AddRange(GetAllPlanetsFromDataBase()); } } } return _planets.Where(x => x.Name == name).ToList(); } private static IEnumerable<Planet> GetAllPlanetsFromDataBase() { // 数据库查询逻辑 } }
3. 使用线程安全集合替换List<T>
如果需要支持缓存的动态更新(当前场景不需要,但可扩展),可以使用ConcurrentBag<T>或ConcurrentDictionary<TKey, TValue>:
public class PlanetService { private readonly ConcurrentBag<Planet> _planets = new ConcurrentBag<Planet>(); private readonly object _lockObj = new object(); public IEnumerable<Planet> FindAll() { if (_planets.IsEmpty) { lock (_lockObj) { if (_planets.IsEmpty) { foreach (var planet in GetAllPlanetsFromDataBase()) { _planets.Add(planet); } } } } return _planets; } public IEnumerable<Planet> FindByName(string name) { return FindAll().Where(x => x.Name == name); } private static IEnumerable<Planet> GetAllPlanetsFromDataBase() { // 数据库查询逻辑 } }
补充说明
- 无论采用哪种方案,需确保
GetAllPlanetsFromDataBase方法本身线程安全(通常数据库查询方法是安全的,但要避免共享数据库连接等问题)。 - 如果需要定期刷新缓存,可以在方案中添加过期逻辑,比如记录缓存初始化时间,每次访问时检查是否需要重新加载。
内容的提问来源于stack exchange,提问作者FLash
相关产品推荐
相关产品推荐

