一、目錄
- 環境及需求
- 問題
- 解決方案
二、環境及需求
每個任務都會有1萬線程訪問這個方法,這個方法里面又會訪問驗證碼驗證服務的方法,這樣就會解開驗證碼限制,后續的訪問達成有效訪 問。然后過了一個時間單位,訪問又會受限,多個線程同時請求驗證碼驗證服務,解封后,后面的訪問就會正常。這樣一直循環。
因為驗證碼服務收費,並且解封一次后續就可正常訪問。怎樣讓這1萬個並發線程進入方法后,隊列去請求驗證碼服務,請求成功后其它線程不訪問驗證碼服務。
1、這里用示例程序模擬一下環境,模擬1萬線程並發請求:
static void Main(string[] args) { int count = 10000; List<string> urls = PageSourceController.GetUrls(count); List<Product> products = PageSourceController.CollectedProducts(urls); }
2、模擬count=10000個url
public static List<string> GetUrls(int count) { List<string> urls = new List<string>(); for (int index = 0; index < count; index++) { string url = string.Format("http://congcong.jd.com/{0}.html", index + 1); if (!urls.Contains(url)) urls.Add(url); } return urls; }
3、根據上面獲取到1萬個url集合進行並發請求
public static List<Product> CollectedProducts(List<string> urls) { //一萬線程並發 System.Threading.Tasks.Parallel.ForEach(urls, u => { PageSourceController.GetProductInfo(u); // ...... // do something... }); return null; }
4、並發請求的方法GetProductInfo(string url)
//根據URL獲取商品詳細信息 public static string GetProductInfo(string url) { //請求--返回封ip的情況 string html = PageSourceController.GetPageSource(url); //封IP情況 if (!string.IsNullOrEmpty(html) && html.Contains("Input CheckCode")) { string imgurl = "http://img5.congcong.com/jjooxx.jpg";
//---------------------------------[分割線]------------------------------------------------- //解析驗證碼--這是重點:解析驗證碼收費,但是現在是多線程並發,如果封ip,同時會有許多線程同時進入請求,花費較大 string result = CheckCodeServer.GetCheckCode(imgurl); if (!string.IsNullOrEmpty(result)) { //獲取解析后的驗證碼,重新提交請求,獲取正確的商品信息 html = PageSourceController.PostCheckCodeData(result, url); }
//---------------------------------[/分割線]------------------------------------------------- } return html; }
5、請求和提交方法
public static string GetPageSource(string url) { return "CheckCodeFlag:Input CheckCode"; } public static string PostCheckCodeData(string resultCheckCode, string url) { string html = "解封成功並成功請求回正確的商品信息:XXOOXX-XXOOXX"; ; return html; }
綜上所述,現在的問題就是如何在並發請求驗證服務時,如何保證驗證服務(分割線內的內容)只能進入一次。后面的其它線程都不進入。
三、解決方案
1)加鎖
首先,定義一個鎖
public class LockKey { public bool flag { get; set; } }
然后,創建一個鎖
public static LockKey lockkey = new LockKey() { flag = false };
其次,使用此鎖
public static string GetProductInfo(string url) { //請求 string html = PageSourceController.GetPageSource(url); //封IP情況 if (!string.IsNullOrEmpty(html) && html.Contains("Input CheckCode")) { string imgurl = "http://img5.congcong.com/jjooxx.jpg";
//默認為flase if (!lockkey.flag) { lock (lockkey) { //解析驗證碼 string result = CheckCodeServer.GetCheckCode(imgurl); if (!string.IsNullOrEmpty(result)) { //解封 html = PageSourceController.PostCheckCodeData(result, url); lockkey.flag = true; } } } } return html; }
問題:運行程序發現,還是有多個線程同時進入
原因:判斷在鎖的外層,這樣同時會有多個線程首先獲取到flag=false,這樣就會有多個符合條件的線程進入。
解決:調整鎖的位置
//根據URL獲取商品詳細信息 public static string GetProductInfo(string url) { //請求 string html = PageSourceController.GetPageSource(url); //封IP情況 if (!string.IsNullOrEmpty(html) && html.Contains("Input CheckCode")) { string imgurl = "http://img5.congcong.com/jjooxx.jpg"; lock (lockkey) { if (!lockkey.flag) { //解析驗證碼 string result = CheckCodeServer.GetCheckCode(imgurl); if (!string.IsNullOrEmpty(result)) { //解封 html = PageSourceController.PostCheckCodeData(result, url); lockkey.flag = true; } } } } return html; }
問題:第一次進入后修改flag=true,后面的線程確實不會進入,頁面也會正常請求訪問。正常請求訪問的情況下不會走下面里面的方法。
if (!string.IsNullOrEmpty(html) && html.Contains("Input CheckCode")) { //驗證碼解析驗證 }
但是如果,訪問又一輪被封ip它就會進入這個方法,但是之前我們已經給靜態l對象的ockkey.flag = true;賦值,這樣連下面的方法都不會進入:
lock (lockkey) { //上一輪已經賦值為true,所以后面所有的所有都不會再進入去驗證了,頁面也會一直得不到解封。除非關閉程序,重新運行 if (!lockkey.flag) { //解析驗證碼 string result = CheckCodeServer.GetCheckCode(imgurl); if (!string.IsNullOrEmpty(result)) { //解封 html = PageSourceController.PostCheckCodeData(result, url); lockkey.flag = true; } } }
問題:那問題又來了,我如何在第一次進入后將 lockkey.flag = true;而且在本輪的所有線程請求結束后將 lockkey.flag = false。我怎樣獲取最后一個線程都已經結束的標志?
方案:計數?修改得也不少,方法傳參也要變;System.Threading.CancellationTokenSource?看書的時候看過,用的不熟,還要現看資料,貌似對於System.Threading.Tasks.Parallel.ForEach不是很管用。怎么辦?時間戳?
1、增加鎖鍵屬性:
public class LockKey { public bool flag { get; set; } public DateTime datetime { get; set; } }
2、聲明鎖:
public static LockKey lockkey = new LockKey() { flag = false,datetime=DateTime.Now.AddDays(-1) };
3、使用此鎖:
public static LockKey lockkey = new LockKey() { flag = false,datetime=DateTime.Now.AddDays(-1) }; //根據URL獲取商品詳細信息 public static string GetProductInfo(string url) { //請求 string html = PageSourceController.GetPageSource(url); //封IP情況 if (!string.IsNullOrEmpty(html) && html.Contains("Input CheckCode")) { string imgurl = "http://img5.congcong.com/jjooxx.jpg"; lock (lockkey) { TimeSpan ts = DateTime.Now - lockkey.datetime;
//首次進入lockkey.datetime是前一天,時間差肯定大於30分鍾 if (ts.TotalMinutes>30) { //解析驗證碼 string result = CheckCodeServer.GetCheckCode(imgurl); if (!string.IsNullOrEmpty(result)) { //解封 html = PageSourceController.PostCheckCodeData(result, url); }
//首次訪問后重置lockkey.datetime時間為當前時間,后續進入的線程時間差小於30分鍾,所以就不會進入。
//30分鍾后,又被封ip,時間差大於30分鍾,又會進入。 lockkey.datetime = DateTime.Now; } } } return html; }
加入時間戳之后,如果封ip就30分鍾解封一次。
Demo下載地址:http://yun.baidu.com/share/link?shareid=882934955&uk=4027290153
