缓存,在程序设计中的应用非常常见,Linq也是广受大家欢迎,但这二者一起配合使用的时候,就要格外注意了,很容易造成内存泄露和高CPU。

Linq的一大特点是延后执行,即在使用到该对象的时候才会执行相关的代码。这就意味着如果你多次使用到这个Linq对象,那它相关的代码就会重复执行多次,这样会增加计算量从而提高了CPU降低了性能。

一起来看一下下面这个代码:

public IEnumerable<CalendarEvent> GetByCalendarId(int inCalendarId)
{
    try
    {
        return
            _cacheManager.Get(
                "GetByCalendar_{0}_{1}".FormatWith(inCalendarId, _workContext.CurrentCulture.CultureId),
                acquire =>
                {
                    acquire.Monitor(_signals.When(Content.CacheSignalContentChange));
                    return _calendarEventRepository.GetAll()
                        .Where(
                            c =>
                                c.CalendarID == inCalendarId && c.Status == (int)CodeTable.RecordStatus.Active &&
                                c.CultureID == _workContext.CurrentCulture.CultureId)
                        .Where(c =>
                        {
                            var content =
                                _contentManager.Get(c.ContentItemID, _workContext.CurrentCulture.CultureId,
                                    VersionOptions.Latest).Content;
                            return content.DisplayRegions.GeoAllowed(_workContext.CurrentRegions);
                        });
                });

    }
    catch (Exception ex)
    {
        _logger.Error(ex.ToString());
    }
    return Enumerable.Empty<CalendarEvent>();
}

代码很简单,按照不同的区域和语言,获取相关的日历事件,并缓存它。

_workContext对象是与当前用户相关的一些上下文信息

  • _workContext.CountryCode //国家
  • _workContext.CurrentCulture.CultureId //语言
  • _workContext.CurrentRegions //当前区域

所以结果是与当前访问者的国家区域和语言相关的。初步看这样的代码似乎没什么问题。但真的是这样吗?

这个代码会导致缓存内部的两个Where反复执行,并且_contentManager对象和_workContext对象不能被释放,_workContext.CurrentRegions也不是当前访问者的区域,而是第一个访问者的区域,这会导致结果错误。为什么会这样?因为Linq的延后执行特性。当然解决的方式也很简单,在最后调用.ToList()方法即可。修改如下:

_cacheManager.Get(
    "GetByCalendar_{0}_{1}_{2}".FormatWith(inCalendarId, _workContext.CountryCode, _workContext.CurrentCulture.CultureId),
    acquire =>
    {
        acquire.Monitor(_signals.When(Content.CacheSignalContentChange));
        return _calendarEventRepository.GetAll()
            .Where(
                c =>
                    c.CalendarID == inCalendarId && c.Status == (int)CodeTable.RecordStatus.Active &&
                    c.CultureID == _workContext.CurrentCulture.CultureId)
            .Where(c =>
            {
                var content =
                    _contentManager.Get(c.ContentItemID, _workContext.CurrentCulture.CultureId,
                        VersionOptions.Latest).Content;
                return content.DisplayRegions.GeoAllowed(_workContext.CurrentRegions);
            }).ToList();
    });

再来看看下面这个代码,同样,这个result是会被缓存的:

Dictionary<string, IEnumerable<Region>> result = new Dictionary<string, IEnumerable<Region>>();
var settings = _settingManager.GetCustomSetting(settingKey);
if (!string.IsNullOrWhiteSpace(settings))
{
	var serializer = new JavaScriptSerializer();
	var geoDescriptions = serializer.Deserialize<List<GeoDescription>>(settings);
	geoDescriptions.Each(m =>
	{
		var regions = m.Regions.SelectMany(s => _regionService.GetByName(s));
		if (!regions.Any())
			regions = m.Regions.Select(s => new Region { RegionID = -1, Name = s });

		result.Add(m.Text, regions);
	});
}

代码的大致意思是解析字符串中的配置信息,并调用_regionService来获取Region对象,最后缓存起来。这个代码看起来同样也是没有问题,而且执行结果也很正确。但实际上,SelectMany方法中的_regionService不能被释放,并且会被反复执行。当然使用ToList()即可解决这个问题。

像这样的问题,在开发过程中是很容易发生和被忽略的,如何避免这样的问题发生还是值得思考的。我觉得需要慎用IEnumerable<T>面转用明确的数组或者是List类型可以在很大程度上避免该问题的发生。比如第一个例子,如果返回对象是数组CalendarEvent[]或者是IList<CalendarEvent>这个问题就不会发生。同样在第二个示例中也是如此。