轉載:https://mp.weixin.qq.com/s/s1cb9Ij6ouTYYCZovLhXTAcss
最近,想必你們的朋友圈都被「王菊」佔領了,打開朋友圈處處能夠見到「pick王菊」、「陶淵明」、「菊外人」等字眼,可謂是火的一塌糊塗。程序員
做爲一個「菊外人」的我「跟風」的去了解了一下究竟是怎麼回事兒。原來是最近很火的一個選秀節目,你們都在呼籲給一個叫「王菊」的人投票。而後就有各類媒體發文分析「到底王菊是誰?」、「王菊爲何火了?」、「pick王菊給這個社會帶來了什麼?」等等文章。redis
可是,做爲一個程序員,咱們看這個世界的角度永遠是那麼的獨特:算法
「咱們是那個瀏覽網頁的時候常常會按ctrl + s的人。數據庫
咱們是那個按下電梯的以後就會忍不住想電梯調度算法的人。後端
咱們是那個每次支付寶付款的時候都會考慮二維碼的生成邏輯的人。跨域
」 無論是做爲「菊外人」仍是「陶淵明」,對於這個「pick王菊」事件,咱們的角度是:如何實現一個高併發的投票系統?咱們須要一個怎樣的投票系統?筆者分別瀏覽了目前創造101的各個投票通道,基本的要求有如下幾個:瀏覽器
一、只有登陸用戶才能投票 緩存
二、每一個用戶投票數有限 安全
三、不一樣用戶可投票數不一樣,如Vip會比普通用戶的票數多
四、投票是限時的,只有在有效時段內才能投票
除了以上幾個功能性要求外,做爲一個開發人員,還須要考慮如下幾個非功能性需求:
一、計數要準確
二、能夠處理高併發投票
三、能夠處理大量的投票數據
四、要有很好的可用性
五、要把每一個人的投票記錄下來
投票網站都是須要登陸驗證的,用戶想要進行投票,須要先登陸。目前不少大型網站的登陸都採用單點登陸(SSO,Single Sign On)技術,SSO是在多個應用系統中,用戶只須要登陸一次就能夠訪問全部相互信任的應用系統。它是目前比較流行的企業業務整合的解決方案之一。
實現SSO的技術主要有如下幾種:
(1)基於cookies實現。 最簡單的單點登陸實現方式,是使用cookie做爲媒介,存放用戶憑證。可是存在幾個問題;一、cookies並不安全,二、cookies自己不跨域。三、瀏覽器對cookies個數和大小有限制。可是,若是想要解決的話,以上三個問題仍是能夠找到方案的。只不過會讓整個方案變得複雜而已。
(2) Broker-based(基於經紀人),例如Kerberos等;這種技術的特色就是,有一個集中的認證和用戶賬號管理的服務器。經紀人給被用於進一步請求的電子的身份存取。中央數據庫的使用減小了管理的代價,併爲認證提供一個公共和獨立的"第三方"。例如Kerberos,Sesame,IBM KryptoKnight(憑證庫思想)等。Kerberos是由麻省理工大學發明的安全認證服務,當前版本V5,已經被UNIX和Windows做爲默認的安全認證服務集成進操做系統。
(3) Agent-based(基於代理人)在這種解決方案中,有一個自動地爲不一樣的應用程序認證用戶身份的代理程序。這個代理程序須要設計有不一樣的功能。好比,它可使用口令表或加密密鑰來自動地將認證的負擔從用戶移開。代理人被放在服務器上面,在服務器的認證系統和客戶端認證方法之間充當一個"翻譯"。例如SSH等。
(4) Token-based,例如SecurID,WebID,如今被普遍使用的口令認證,好比FTP,郵件服務器的登陸認證,這是一種簡單易用的方式,實現一個口令在多種應用當中使用。
(5) 基於安全斷言標記語言(SAML)實現,SAML(Security Assertion Markup Language,安全斷言標記語言)的出現大大簡化了SSO,並被OASIS批准爲SSO的執行標準。開源組織OpenSAML 實現了 SAML 規範。可參考http://www.opensaml.org。
目前,不少大型網站都採用一個開源的SSO解決方案——CAS,CAS由耶魯大學開發的單點登陸系統(SSO,single sign-on),應用普遍,具備獨立於平臺的,易於理解,支持代理功能。
在投票網站驗證完用戶的登陸信息以後,緊接着會判斷用戶的權限,而後根據不一樣的權限來給用戶分配不一樣的可投票次數。
關於權限的設計,一直是不少網站都要關心的問題。幾乎全部的網站都會有必定的權限要求。
目前,關於權限設計大部分均採用RBAC理論(Role-Based Access Control),即基於角色的權限訪問控制。
RBAC認爲權限受權其實是Who、What、How的問題。在RBAC模型中,Who、What、How構成了訪問權限三元組,也就是「Who對What進行How的操做」。
一個簡單的權限系統應該包含如下幾個基本元素:
用戶、角色、權限、資源、操做。
【用戶】能夠屬於多個【角色】。【角色】能夠認爲是【權限】的合集。【權限】描述的是對【資源】的可【操做】能力。
好比在「pick王菊」這件事上,雖然你們都是「陶淵明」(王菊的粉絲自稱陶淵明,由於陶淵明愛菊花),可是有些用戶的角色是VIP用戶,有些用戶的角色是普通用戶。VIP角色的用戶在投票權限上就比普通用戶更高。
幾乎全部的投票活動都是有一個起止時間的,只有在有效時間段內用戶才能夠參與投票。也就是說,當活動未開始的時候,用戶來到投票頁面,投票按鈕應該是置灰沒法點擊的,有些網站還會給出倒計時的提示。
這其中就涉及到不少問題了。如何在時間到達時將按鈕點亮呢?用戶經過非法手段在有效時間外點擊按鈕如何處理?到時間後按鈕又如何置灰?
通常狀況下,用戶訪問的投票頁面被設計爲靜態頁面,被緩存在 CDN 與反向代理服務器中,甚至在用戶的瀏覽器上。因此在投票活動未開始時,用戶的刷新頁面請求是不會到達應用服務器。一樣,在後端的投票接口中,在接受到用戶的投票請求時,也要作時間有效性的校驗。
咱們在秒殺商品的靜態頁面中加入一個 JavaScript 文件引用,它包含投票是否已開始的標誌。秒殺開始時,系統會生成一個新的 JavaScript 文件,它會被瀏覽器加載(刷新頁面或定時腳本),這樣就能點亮頁面中的購買按鈕。這個 JavaScript 文件使用隨機版本號,確保它不被瀏覽器、CDN 和反向代理服務器緩存。
同理,到達活動截止時間的按鈕置灰也經過js引用的方式能夠解決。
對於一個投票系統來講,最重要的就是計數了。要保證在高併發的狀況下用戶的投票既不能多也不能少這是一個很大的挑戰。
在「pick王菊」的投票中,計數場景有多處須要。好比對於王菊的票數須要準確的記錄下來。還有,會員的已投票次數也要準確的記錄下來。這裏咱們就拿被投票者的票數來舉例。
咱們能想到的最簡單的方法就是在MySql數據庫表中建立一條記錄,記錄當前被投票者的票數便可。如:
1CREATE TABLE IF NOT EXISTS `table_user_polls`(
2 `id` INT UNSIGNED AUTO_INCREMENT,
3 `user_id` bigint NOT NULL,
4 `polls` bigint NOT NULL
5 PRIMARY KEY ( `runoob_id` )
6)ENGINE=InnoDB DEFAULT CHARSET=utf8;
固然,這只是一張記錄總票數的表。當有用戶投票的時候,咱們經過修改表中的polls的值來進行:
1update table_user_polls set polls = polls +1 where user_id = "1000";
當採用以上的update語句時,基本能夠知足簡單的票數的遞增。可是,一旦有高併發場景的話,就沒法知足了。由於多人同時執行這個語句的時候,就會有的人的更新結果丟失。
這時候,就須要引入鎖機制來處理。好比咱們增長樂觀鎖,改進後的update語句以下:
1update table_user_polls set polls = polls +1 where user_id = "1000" and polls = 1000000;
在更新table_user_polls表的polls字段的時候,咱們先把表中當前的polls值取出,而後做爲樂觀鎖來控制更新,若是發生併發,只會有一個請求能夠更新成功。其餘請求就會更新失敗。
可是,緊接着又有兩個問題來了。
若是數據量太大怎麼辦?好比這場「全民Pick王菊」的行動,參與的用戶可能有不少不少,而咱們要對每一個用戶的投票數據都持久化下來。這就涉及到一旦數據量過大,就會致使數據庫的讀寫性能急劇降低。
傳統的作法是進行分庫分表,把投票表按照必定的維度進行拆分。好比這種投票的場景,咱們能夠按照投票的用戶的userId拆分,對userId取模,根據結果存儲到不一樣的表中。如把全部投票數據拆分到8個庫128張表中,路由規則就是userId%128
。
其實,這種單純的分庫分表還有一個另外的問題,雖然投票場景中可能涉及不到。一、熱點數據問題。如多個熱點用戶的數據都分到了同一個庫甚至同一張表中,就會又給數據庫帶來巨大壓力。二、數據的二次分表問題。一旦分表數量不夠了,就要再次分表。好比把128張表擴展到256張表,這就麻煩了,須要對原來的1287張表中的數據從新進行拆分,數據遷移到新的256張表中。
經過分庫分表以後,咱們暫時解決了數據量大的問題,那麼若是遇到訪問量大的問題怎麼辦。不管是咱們的應用、服務器仍是數據庫等,能承受的QPS(TPS)都是有限的。若是峯值的qps超過了限制,就有可能致使整個系統癱瘓。
那麼如何提升一個服務或者接口的QPS呢,其實一個最簡單的方法就是下降響應時間(RT)。固然RT和QPS的關係還會受最佳線程數等影響,可是下降RT也是一個比較有效的辦法。若是一個請求,在執行過程當中,什麼都不須要須要等待,每一個操做均可以快速執行(如單純的在內存中取數、計算等),那麼RT就會低不少。
在程序中,致使請求阻塞的願意可能有不少,數據庫操做多是其中比較重要的一部分。雖然咱們經過分庫的方式增長了數據庫的鏈接數,可是直接操做數據庫仍是有很大的性能損耗的。
這時候,就要考慮在持久化存儲前面增長緩存了。在訪問數據庫以前先訪問緩存,若是緩存中沒有的話再訪問數據庫。這樣能夠減小請求的響應時間,從而提升QPS,進而承載更大的訪問量。
這樣作有很大的好處,可是也不是完美的方案。好比這種投票系統,計數更新是很是頻繁的,因此要常常失效緩存在從新緩存,緩存和數據庫之間的數據一致性問題就體現出來了。
以上,是經過MySql來進行計數的方案,總結一下:
優勢:便於理解、學習成本低、開發成本也低。 缺點:對大數據量和高併發量支持不友好。
Redis 是目前 NoSQL 領域的當紅炸子雞,它象一把瑞士軍刀,小巧、鋒利、實用,特別適合解決一些使用傳統關係數據庫難以解決的問題。
若是咱們使用Redis來實現計數器的話,就相對來講簡單一些了。由於Redid提供了一個INCR命令,其使用方法以下:
1redis> SET wangju_polls 20
2OK
3
4redis> INCR wangju_polls
5(integer) 21
6
7redis> GET wangju_polls
8"21"
INCR key
語法,能夠將 key 中儲存的數字值增一。若是 key 不存在,那麼 key 的值會先被初始化爲 0 ,而後再執行 INCR 操做。最重要的是INCR是一個原子性的自增操做。很是適合用來實現計數器。
引入了Redis以後,遇到高併發和大數據量的問題解決起來就簡單了——堆機器。
固然,這個方案雖然在很大程度上解決了大數據量和高併發的問題。可是,若是真的是業務量特別巨大,總不能無限制的增長經過增長機器來解決問題吧,機器就是成本啊。
關於這種問題,微博就遇到了,由於微博的點贊功能和咱們的投票功能實際上是相似的。明星的一條微博的點贊數可能有幾十萬,甚至百萬以上。有人(微博計數器的設計)算過一筆賬:
假設 key 爲8字節,value爲 4字節,經過incr存儲的話:
一個 value 經過 createStringObjectFromLongLong 建立一個robj,因爲value在LONGMIN 和LONGMAX 之間,因此能夠將value用 ptr指針來存儲,須要佔用 sizeof(robj) = 16 字節;
一個key(即微博id) 最長64位數字(Eg: 5612814510546515491),但經過 sdsdup 以字符串的形式存儲,至少須要 8(struct sdshdr)+19+1 = 28字節;
爲了存到Redis 的dict裏面,須要一個dictEntry對象,繼續 3*8 = 24字節;
放到db->dict->ht[0]->table中存儲dictEntry的指針,再要 8個字節; 存儲一個64位key,32位value的計數,Redis也至少須要耗費: 16 + 28 + 24 + 8 = 76 字節。
1000億個key全內存的話,就至少須要 100G * 76 = 7.6TB 的內存了(折算76G內存機器也須要100臺!)。
咱們的有效數據實際上是1000億個32位 = 400GB,可是卻須要了7.6T來存儲,內存的有效利用率約爲:400GB/7600GB = 5.3%
總的來講,Redis作爲優秀的內存數據結構,接口方便,使用簡單,對於小型數據量的中高訪問量的計數類服務來講,是一個很不錯的選擇,可是對於微博計數器這種極端的應用場景,成本仍是沒法接受!
因此,微博的點贊功能,實際上是在Redis的基礎上進行了二次開發。如在數據機構優化、轉發和評論數 Value的優化、key的優化、數據的持久化、一致性保證等方面作了不少事情。這裏不詳細介紹了,感興趣的同窗能夠參考微博計數器的設計
避免刷票
在創造101的投票規則中,明確規定了:請公平參與點贊,如採用違法或違反賽事規則的點贊行爲,將會被收回相關點贊數並追究責任。
那麼,若是咱們是這個投票系統的開發,如何有效的避免刷票行爲呢?
首先,咱們要經過設計一個很好的計數器,可以有效的避免高併發請求帶來的計數錯誤。由於投票時可能有不少人使用腳本等構造多條請求,試圖來突破限制來多投票。
其次,還能夠經過一些其餘限制手段來防止惡意刷票,如限制同一IP的投票次數、限制同一賬號的投票頻率等。
聽說,在前段時間的MSI比賽前期,MSI的助威活動中,人氣選手uzi的票數達到了網站開發人員設置的int的最大值。
以上只是個傳說,我並無去辯證他的真僞,可是這至少給咱們一個提醒,在設計投票系統的時候,要充分的考慮到粉絲們的熱情和實力!
不管最終選用那種方式進行計數,數據的持久化問題都相當重要,必定要作好數據存儲的容災工做。避免因爲系統問題致使數據丟失。
PS:做者並無在工做中開發過實際的投票系統,以上總結均是基於平常工做中的積累及一些參考資料(參考連接請到原文中查看)總結得出。歡迎你們指正與討論。
好啦,看完這篇文章以後,若是你有收穫,請給我菊姐投票!!!