问题是这样的,
我为web页面写了个获取图片的方法,
可以根据所传递的图片大小,是否加水印等参数,
根据上传的原图进行缩放和水印处理后生成临时图片保存到指定的目录,
并返回图片对外可访问的URL.
一个页面如果有10张图片,就会去请求10次这个接口.
就是浏览器的并发请求了,接口请求间隔时间相当相当小.
接口也返回了URL,整个过程没有发生任何异常...
但是发现生成的临时图片,不一定每次都能保存成功,最终导致了这个URL指定的文件不存在(404错误)
我在想应该是多线程读取原图或者保存图片的时候,发生了冲突或资源被占用的问题.(按理说读取不能的原图,保存在不同的目标路径,是不会发生资源被占用的情况的)
于是,
分别在读取原图,保存临时图片等每个涉及文件操作的地方,
都加了lock,lock的变量是全局的,
可是仍然没有效果.
当我跟踪调试时,每次都能成功的,
因为请求间隔很大(比起浏览器的并发请求来说).
后来在无奈的情况下,我将整个方法全lock了,
这样每张图片都能保存成功,返回的URL都能访问..
但是必然会导致效率低下的问题,只能一张一张的处理图片..一张一张的返回...完全失去了web并发处理的能力.
我将代码放上来,请大牛帮忙看下是哪的问题..谢谢了
using Common; using ImgOutAPI.Models; using SoEasy.Common; using SoEasy.DB; using SoEasy.Logic; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Web; using System.Web.Caching; using System.Web.Mvc; namespace ImgOutAPI.Controllers { public class ImageController : Controller { static Cache cache = HttpRuntime.Cache; CommonBL bl = CommonBL.CreateInstance(); static object lockObj = new object(); /// <summary> /// 获取图片 /// </summary> /// <param name="imageID">图片ID</param> /// <param name="width">图片宽度</param> /// <param name="height">图片高度</param> /// <param name="addWater">是否添加水印true或1为是</param> /// <param name="waterFontSize">水印字号大小</param> /// <param name="waterText">水印内容</param> /// <returns>操作结果的json字符串</returns> [HttpPost] public string GetImage(string imageID, int width, int height, int addWater, int waterFontSize, string waterText) { OPResult opRes = Utility.CheckArgsNotNullOrEmpty("imageID", imageID); lock (lockObj)//不锁整个方法会导致生成的图片不一定能保存成功 { if (opRes.State == Enums.OPState.Success) { opRes.State = Enums.OPState.Fail; string cacheKey = imageID + "|" + width + "|" + height + "|" + addWater + "|" + waterFontSize + "|" + waterText; //判断缓存中是否存在 object o = cache.Get(cacheKey); if (o != null) { opRes = (OPResult)o; } else { //最终图片的物理存放路径,定义在这里是为了放进缓存对象,本应该定义在if(model!=null){内的 string imagePhysicsPath = ""; ImageModel model = new ImageModel(); model.Id = imageID; model = bl.SelectFirst(model, opRes, null); if (model != null) { //用GUID做缓存图片的名称 string fileGuid = Guid.NewGuid().ToString() + Path.GetExtension(model.Url); //最终图片对外可访问的URL string imageAccessURL = Vars.DoMain + "/" + Path.Combine(VarsEx.CacheImageRootPath.Replace("~", ""), fileGuid).Replace(@"\", "/"); //最终图片的物理存放路径 imagePhysicsPath = System.Web.HttpContext.Current.Server.MapPath(Path.Combine(VarsEx.CacheImageRootPath, fileGuid).Replace(@"\", "/")); //原始图片 string originalImage = Path.Combine(VarsEx.ImageSrcRootPath, model.Url.Replace("~/", "").Replace("/", @"\")); if (System.IO.File.Exists(originalImage)) { List<string> listTempFile = new List<string>(); //图片是否改变 bool changeImage = false; if (width > 1 || height > 1 || (addWater == 1)) { if (addWater == 1 && waterFontSize > 0 && waterFontSize < 72 && !string.IsNullOrWhiteSpace(waterText))//加水印 { //临时水印图片存放路径 string waterTmpFileName = imagePhysicsPath.Replace(".", "w."); if (ImageHelper.AddWatermarkText(originalImage, waterTmpFileName, waterFontSize, waterText, opRes)) { //将水印图片做为后续缩放的原图片 originalImage = waterTmpFileName; listTempFile.Add(waterTmpFileName); } else { opRes.Data = "图片添加水印失败."; return opRes.ToJsonString(); } } if (width > 0 && height > 0 && width < model.Width && height < model.Height)//缩放 { //临时缩放图片存放路径 string zoomTmpFileName = imagePhysicsPath.Replace(".", "r."); if (ImageHelper.CreateThumbnail(originalImage, zoomTmpFileName, width, height, opRes)) { originalImage = zoomTmpFileName; listTempFile.Add(zoomTmpFileName); } else { opRes.Data = "图片缩放失败."; return opRes.ToJsonString(); } } changeImage = true; } try { System.IO.File.Copy(originalImage, imagePhysicsPath, true);//将生成的文件复制到缓存文件目录 opRes.State = Enums.OPState.Success; opRes.Data = imageAccessURL; if (changeImage)//删除图片改变过程中产生的临时文件 { foreach (string item in listTempFile) { System.IO.File.Delete(item); } } } catch (Exception ex) { Utility.Logger.Error("复制生成的图片到缓存目录时发生异常:" + ex); opRes.State = Enums.OPState.Exception; } } else { opRes.Data = "图片文件不存在"; } } else { opRes.Data = "图片信息不存在."; Utility.Logger.Error("图片信息不存在,imageID=" + imageID); } CacheItemRemovedCallback onRemove = new CacheItemRemovedCallback(DeleteCacheFile); OPResultCache cacheData = new OPResultCache { State = opRes.State, Data = opRes.Data, FilePhysicalFullPath = imagePhysicsPath }; cache.Insert(cacheKey, cacheData, null, Cache.NoAbsoluteExpiration, TimeSpan.FromMinutes(60), CacheItemPriority.High, onRemove); } } } return JsonHelper.ToJsonString(opRes); } private void DeleteCacheFile(string key, Object value, CacheItemRemovedReason reason) { //删除实际生成的文件 OPResultCache res = (OPResultCache)value; if (res.State == Enums.OPState.Success) { string cacheFileName = res.FilePhysicalFullPath; if (System.IO.File.Exists(cacheFileName)) { try { System.IO.File.Delete(cacheFileName); } catch (Exception ex) { Utility.Logger.Error("移除图片缓存时,删除生成的缓存图片" + cacheFileName + "发生异常:" + ex); } } } } class OPResultCache : OPResult { /// <summary> /// 缓存文件的真实路径 /// </summary> public string FilePhysicalFullPath { get; set; } } } }
我把前端调用的方式也放上来..
<img src="Http://192.168.1.100/static/img/loading.gif" onload="getImg(this,' imgIDxxxx');" onerror="imgLoadFail(this);" style="max-height:120px;max-width:120px;"/>
JS
//获取图片URL并绑定到图片控件上 //imgCtr 图片控件 //imgID 图片ID //hasWater 是否添加水印,添加传1 //waterFontNum 水印字体大小 //waterString 水印字符串 function getImg(imgCtr, imgID, hasWater, waterFontNum, waterString) { var imgWidth = 0; var imgHeight = 0; var style = $(imgCtr).attr("style"); if (style != undefined) { style = style.toLowerCase(); try { imgWidth = parseInt(style.match(/max-width:\s*(\d+)px/)[1]); imgHeight = parseInt(style.match(/max-height:\s*(\d+)px/)[1]); } catch (e) { } } //设置默认值 hasWater = hasWater == undefined ? 0 : 1; waterFontNum = waterFontNum == undefined ? 12 : waterFontNum; waterString = waterString == undefined ? config.site.Name : waterString; imgWidth = parseInt(imgWidth); imgHeight = parseInt(imgHeight); ajaxRequest({ url: config.api.getImageAPI, data: { imageID: imgID, width: imgWidth, height: imgHeight, addWater: hasWater, waterFontSize: waterFontNum, waterText: waterString }, successCallback: function (data) { $(imgCtr).removeAttr("onload"); imgCtr.src = data; }, failCallback: function (data) { imgCtr.src = config.image.errorImageUrl; writeLog("图片加载失败:" + data + "imgID=" + imgID); }, sendCredential: false, }); }
你的原图 被资源占用了吧
我开始也这么想过,
于是,在获取原图的地方加了lock....
可是没有效果
if (opRes.State == Enums.OPState.Success) { opRes.State = Enums.OPState.Fail; //... }
会不会是这里的原因?
不是的,所有的opRes.State == Enums.OPState.Success
只是一个检查上一步操作是否成功的判断.
@hexllo: 我的意思是opRes.State = Enums.OPState.Fail可能在某些情况下(比如后面的生成水印的操作耗时)会导致opRes.State的值不同步,使得后面的操作无法执行
@jello chen: 生成小图和加水印是在同一个线程里的,
也就是说这个GetImage方法,
以及它内部调用的方法,都没有另开线程.
所以,不会导致opRes.State的值不同步的问题...
只是web服务器本身是多线程的而已.
这样的实现方式,给服务器很大的压力呢。IO操作本身就是比较耗费资源的。
1、从设计上,给每次的结果做一次缓存,如果有同等的请求,给予直接返回。
2、每次对原图先复制一份,再进行操作
目前已经使用缓存了的.
尝试对原图复制后再操作,还是没有效果..
请再想想有其它办法吗?
@hexllo: 这样的话,那我觉得你可能没找到真正的bug成因。如果仅仅是开一个页面,又没有请求同一个图片url地址,那就可以排除文件占用。
猜测 AddWatermarkText()方法可能存在以下问题:
(1)图片文件在打开之后没有及时地释放文件句柄。
(2)在读取文件的时候采用了排它锁。
如果是要获取图片对象,最简单的做法是使用Image的FromFile方法
System.Drawing.Image originalImage = System.Drawing.Image.FromFile(originalImagePath);
我就是用.Image.FromFile(originalImagePath);的
而且在加水印和生成缩略图的finally里都调用了 img.dispose();
@hexllo: 本身Image.FromFile是会保持文件锁定的。
我觉得你现在只是猜测问题可能发生在哪里,并没有真正找到问题。找问题的方法:
(1)线上。你提到了本机测试没有问题,但线上就有问题了,那线上有没有日志呢?日志中的异常怎么说?
(2)本地。如果要在本机进行问题重现,可以编写一个客户端程序,短时间内密集循环请求,这样就看出哪里抛出异常了。
@何德海: 还没上线,在本机测试时就发现问题.
当浏览器中的Img标签过多时,请求的次数就多了..此时就会发生问题.
如果此时在后台加断点,稍作停留,就不会出现这样的问题.
用浏览器请求,和用程序密集循环效果是一样的.
另外,程序日志中没有发现任何异常.
IIS日志,windows系统日志 中,也没有发现异常...
即使FromFile方法保持锁,那么处理完后会释放的啊.
而且如果是锁的原因,那么肯定会有日志记录图片文件打开失败的.
最重要的,我页面的img中请求的是不同图片呀..所以不会是获取源图的问题..但是我分别在获取源图,和保存目标图的代码外都加了lock.也是没有效果的.必须像代码中那样,把lock加到最外边,才有效果 ...也不知道为什么
这bug太幽灵了...
@hexllo: 我的意思是说,在出错的时候,有没有抛出异常?
我看你有用Utility.Logger.Error(),你确定日志组件能正常工作?
如果都没有问题的话,看一下是不是生成的地址出错了?有没有进行urlencoding?
用chrome调试工具看一下错误的地址的格式。
@何德海: log4是能记录的,
我直接在程序一开始就Utility.Logger.Error("test")测试过.
在测试的URL中没有中文的,(到目前为止,除了测试方法里的URL有中文的水印字符)
实际页面只传入一个imgID,宽,高这三个参数.
@hexllo: 能将你的出错的这些代码打成一个工程发给我吗?要包含那些引用的组件。我跑起来看一下。
我的邮箱:254213048@qq.com