背景介紹:數據庫
爲了平衡社區成員的貢獻和索取,一塊兒幫引入了幫幫幣。當用戶積分(幫幫點)達到必定數額以後,就會「掉落」必定數量的「幫幫幣」。爲了增長趣味性,幫幫幣「掉落」以後全部用戶均可以「撿取」,誰先撿到歸誰。 編程
但這樣就產生了一個問題,由於這個「幫幫幣」是能夠買賣有價值的,因此不免會有惡意用戶用爬蟲不斷的掃描,致使這樣的狀況出現: 緩存
注:經覈實,喬布斯的同窗 其實沒有用爬蟲,就是手工點,點出來的!還能說什麼呢?只能表示佩服啊佩服…… 服務器
因此咱們須要一種機制,阻止這種爬蟲的行爲。 數據結構
大體思路: ide
這個問題咱們有一個很便利的前提:只有註冊用戶纔可以「撿起」幫幫幣。因此,咱們不須要經過「封IP」(需獲取真實IP)這種方式來阻斷爬蟲爬行,而是直接封註冊用戶,很是方便。 性能
那麼如何判斷一個請求是真實用戶,仍是爬蟲呢?咱們決定使用最簡單的方法:記錄訪問頻次。當某一個用戶的訪問頻次高於設定值時(好比:5分鐘10次),就斷定該用戶「有爬蟲嫌疑」。 spa
此外,爲了防止誤判(確實有用戶手快),咱們還應該給用戶一個「解鎖」的功能:經過輸入驗證碼來肯定不是爬蟲。 設計
細節設計: 調試
一個最核心的問題是:用什麼來記錄用戶的訪問頻次?
數據庫?感受不必,這個數據又不須要長期保留,訪問一次就作一次I/O操做在性能上接受不了,因此咱們決定使用內存。
可是,具體須要記錄那些數據,又用什麼樣的數據結構呢?
最後咱們選擇使用緩存,記錄最簡單的「用戶ID -> 訪問次數」鍵值對,來解決這個問題,由於:
固然,這裏其實仍是有那麼點問題的。好比,假設緩存時間是5分鐘,最多訪問次數是10次。0:10,開始緩存訪問次數,一直累加,到0:14,共記錄訪問次數7次,沒有問題;然而,一過0:15,緩存被清空,0:16的時候,緩存裏只有0:15到0:16這一分鐘的數據,沒有過去5分鐘(從0:11到0:16)的數據。因此用戶能夠控制一直爬蟲,訪問9次,而後就歇着,5分鐘事後,再繼續訪問9次,而後再歇5分鐘……
唉~~真這麼拼,我還真沒什麼辦法?但若是這麼一個頻次他能接受的話,我其實也無所謂,你就慢慢爬唄。或者,咱們後臺作更大的監控,把每一個用戶的每次訪問都記錄下來,進行統計,找出異常。那時候可能就真的須要數據庫了(爲了提升性能能夠內存裏放一個DataTable,定時同步到Database)。但暫時來講,沒有這個必要。
此外,還有一個問題,是否是隻須要記錄用戶訪問頻次?
若是按上述方案,在緩存裏記錄訪問頻次,經過緩存數據來判斷是否容許繼續訪問,會有一個問題:緩存到期失效以後,這個用戶就又能夠自由訪問目標頁面了!至關於到期自動解鎖。
我以爲這仍是不科學,若是認定是爬蟲,只能是人工解鎖(識別碼驗證)。因此在數據庫用戶表裏添加一個「已鎖定」(Locked)字段,若是用戶被鎖定,Update其爲當前時間;未鎖定時(解鎖後)爲NULL。
具體實現:
爲了重用,咱們須要利用 Authorize Fitler,在它的OnAuthorization()方法裏面進行檢查和記錄。
代碼自己應該比較簡單,if...else...的邏輯:
///1. 先根據數據庫撿查當前用戶是否被鎖定 ///2. 若是被鎖定,直接攔截。不然: ///3. 在緩存中檢查有無當前用戶的訪問次數記錄 /// 3.1 沒有,新建一條他的緩存。不然: /// 3.2 檢查該用戶已訪問次數 /// 3.2.1 若是已到達訪問次數限制,攔截並在數據庫中鎖定該用戶。不然 /// 3.2.2 累加用戶的訪問次數
精簡註釋代碼以下:
public class NeedLogOn : AuthorizeAttribute { public override void OnAuthorization(AuthorizationContext filterContext) { HttpContextBase context = filterContext.HttpContext; ///Autofac相關操做,獲取正取的ISharedService實例 ISharedService service = AutofacConfig.Container.Resolve<ISharedService>(); _NavigatorModel model = service.Get(); //從數據庫獲取當前User的信息 ///截斷式編程,減小if...else的{}嵌套 if (model.Locked.HasValue) { ///model.Locked 來自數據庫,用戶已經被鎖定,攔截 visitTooMuch(filterContext); return; } string cacheKey = CacheKey.MAX_VISIT + model.Id; ///很是有意思,不能直接使用int值類型,必須使用引用類型的 VisitCounter amount; if (context.Cache[cacheKey] == null) { amount = new VisitCounter { Value = 1 }; ///新創建一條Cache context.Cache.Add(cacheKey, amount, null, DateTime.Now.AddSeconds(Config.Seconds), Cache.NoSlidingExpiration, CacheItemPriority.Normal, null); } else { amount = context.Cache[cacheKey] as VisitCounter; if (amount.Value >= Config.MaxVisit) { ///在數據庫中鎖定該用戶 service.LockCurrentUser(); BaseService.Commit(); ///當即清除Cache context.Cache.Remove(cacheKey); visitTooMuch(filterContext); return;} else { ///不能使用:currentVisitAmount++; ///context.Cache[cacheKey] = currentVisitAmount; ///見:https://stackoverflow.com/questions/2118067/cached-item-never-expiring amount.Value++; } } } } public class VisitCounter { public int Value { get; set; } }
仔細觀察代碼,你會發現兩個問題。這就是飛哥我曾經掉的坑啊!o(╥﹏╥)o
一、爲何要引入VisitCounter類?
緩存裏就存放着這個類的實例,而這個類其實就包裹一個int Value;幹嗎呢,這是?爲何不直接用int呢?直接把int存到Cache裏不行嗎?
不行啊!艹。
存進去,沒問題;取出來,也沒問題;但更新(累加)的時候有問題啊。你怎麼更新?
//取出緩存 currentVisitAmount = Convert.ToInt32(context.Cache[cacheKey]); //累加 currentVisitAmount++; //再存進去 context.Cache[cacheKey] = currentVisitAmount;
這樣不行的,具體的解釋看這裏:Cached item never expiring。
簡單的說,context.Cache[cacheKey] = currentVisitAmount; 這一句,等於從新插入了一條永不過時的緩存。萬萬沒想到啊!這個bug把飛哥都差點搞瘋了,原本cache的調試都很是麻煩,還搞個這種幺蛾子。
因此解決的辦法是什麼呢?在Cache裏存一個引用類型值,而後不改Cache,只改引用類實例裏的值就OK了。代碼就不重複了。
二、在鎖定用戶的同時,清除該用戶的cache
這裏啊,曾經走了點彎路。
我最開始是在解鎖用戶的時候清除該用戶的Cache。
[NeedLogOn] public ActionResult Unlock() { string userId = getCurrentUserId(); string cacheKey = CacheKey.MAX_VISIT + userId; HttpContext.Cache.Remove(cacheKey); return View(new ImageCodeModel()); }
結果不知道咋回事,時靈時不靈。我把本地代碼,鏈接服務器數據庫,開着Debug模式,一步一會的進去看,OK,沒問題;但把本地代碼發佈到服務器,duang,不行了?!無法調試,只有寫log啥的,坑得我不要不要的……
後來忽然發現,這裏有「壞代碼的味道」:重複。你看這個cacheKey的構建,是否是在 NeedLogOn.OnAuthorization()裏構建過一次?重複使用的代碼是否是就應該封裝?因此呢,開始呢,是想弄一個方法出來得到cacheKey,好比striing GetVisitLimitCacheKey()啥的,但這個方法要讓Controller裏的UnLock()和Filter裏的OnAuthorization()都能調用,放在哪裏呢?
忽然靈光一閃:爲何 Cache.Remove 要寫在UnLock()裏面呢?
其實只要用戶被鎖定,他的緩存信息就沒用了。由於咱們已經在數據庫中標明瞭他被Locked,因此NeedLogOn.OnAuthorization()攔截住他,不須要Cache呀!儘早的清除這個Cache,還能提升那麼一點點的性能。
最關鍵的是,這樣代碼更緊湊了:cacheKe在同一個方法裏被使用,cache操做在同一個方法類完成,避免了代碼分散耦合,優雅多了!
++++++++++++++++++++
最後的最後,請你們幫個小忙,我作的一個小調查:你願不肯意成爲「好心人」?
忘了給註冊人和邀請碼:葉飛,1786。或者直接點擊註冊。