WEB安全入門:如何防止 CSRF 攻擊?

現在,我們絕大多數人都會在網上購物買東西。但是很多人都不清楚的是,很多電商網站會存在安全漏洞。烏雲就通報過,國內很多家公司的網站都存在 CSRF 漏洞。如果某個網站存在這種安全漏洞的話,那麼我們在購物的過程中,就很可能會被網絡黑客盜刷信用卡。是不是瞬間有點「不寒而栗」 的感覺?

首先,我們需要弄清楚 CSRF 是什麼。它的全稱是 Cross-site request forgery ,翻譯成中文的意思就是「跨站請求偽造」,這是一種對網站的惡意利用。簡單而言,就是某惡意網站在我們不知情的情況下,以我們的身份在你登錄的某網站上胡作非為——發消息、買東西,甚至轉賬……

這種攻擊模式聽起來有點像跨站腳本(XSS),但 CSRF 與 XSS 非常不同,並且攻擊方式幾乎相左。XSS 利用站點內的信任用戶,而 CSRF 則通過偽裝來自受信任用戶的請求來利用受信任的網站。與 XSS 攻擊相比,CSRF 攻擊往往很少見,因此對其進行防範的資源也相當稀少。不過,這種「受信任」的攻擊模式更加難以防範,所以被認為比 XSS 更具危險性。

這個過程到底是怎樣的呢?讓我們看個簡單而鮮活的案例。

銀行網站 A,它以 GET 請求來完成銀行轉賬的操作,如:

http://www.mybank.com/Transfer.php?toBankId=11&money=1000

危險網站 B,它裏面有一段 HTML 的代碼如下:

<img src=http://www.mybank.com/Transfer.php?toBankId=11&money=1000>

可能會發生什麼?你登錄了銀行網站 A,然後訪問危險網站 B 以後,突然發現你的銀行賬戶少了10000塊……

為什麼會這樣呢?原因是在訪問危險網站 B 之前,你已經登錄了銀行網站 A,而 B 中的以 GET 的方式請求第三方資源(這裏的第三方就是指銀行網站了,原本這是一個合法的請求,但這裏被不法分子利用了),所以你的瀏覽器會帶上你的銀行網站 A 的 Cookie 發出 GET 請求,去獲取資源

「http://www.mybank.com/Transfer.php?toBankId=11&money=1000」,

結果銀行網站服務器收到請求后,認為這是一個合理的轉賬操作,就立刻轉賬了……

其實,真實的銀行網站不會如此不加防範,但即使用 POST 替代 GET,也只是讓危險網站多花些力氣而已。危險網站 B 依然可以通過嵌入 Javascript 來嘗試盜取客戶資金,所以我們時不時會聽到客戶資金被盜的案件,其實這並不是很不稀奇。

相信,很多人了解到這兒,會出現一身冷汗,還讓不讓我們在「618」期間能夠愉快地享受網購的快感了?難道沒有什麼辦法防住它嘛?

當然是有的。

在業界目前防禦 CSRF 攻擊主要有三種策略:驗證 HTTP Referer 字段;在請求地址中添加 token 並驗證;在 HTTP 頭中自定義屬性並驗證。下面就分別對這三種策略進行詳細介紹。

驗證 HTTP Referer 字段

根據 HTTP 協議,在 HTTP 頭中有一個字段叫 Referer,它記錄了該 HTTP 請求的來源地址。在通常情況下,訪問一個安全受限頁面的請求來自於同一個網站,比如需要訪問http://bank.example/withdraw?account=bob&amount=1000000&for=Mallory,用戶必須先登陸 bank.example,然後通過點擊頁面上的按鈕來觸發轉賬事件。這時,該轉帳請求的 Referer 值就會是轉賬按鈕所在的頁面的 URL,通常是以 bank.example 域名開頭的地址。而如果黑客要對銀行網站實施 CSRF 攻擊,他只能在他自己的網站構造請求,當用戶通過黑客的網站發送請求到銀行時,該請求的 Referer 是指向黑客自己的網站。因此,要防禦 CSRF 攻擊,銀行網站只需要對於每一個轉賬請求驗證其 Referer 值,如果是以 bank.example 開頭的域名,則說明該請求是來自銀行網站自己的請求,是合法的。如果 Referer 是其他網站的話,則有可能是黑客的 CSRF 攻擊,拒絕該請求。

這種方法的顯而易見的好處就是簡單易行,網站的普通開發人員不需要操心 CSRF 的漏洞,只需要在最後給所有安全敏感的請求統一增加一個攔截器來檢查 Referer 的值就可以。特別是對於當前現有的系統,不需要改變當前系統的任何已有代碼和邏輯,沒有風險,非常便捷。

然而,這種方法並非萬無一失。Referer 的值是由瀏覽器提供的,雖然 HTTP 協議上有明確的要求,但是每個瀏覽器對於 Referer 的具體實現可能有差別,並不能保證瀏覽器自身沒有安全漏洞。使用驗證 Referer 值的方法,就是把安全性都依賴於第三方(即瀏覽器)來保障,從理論上來講,這樣並不安全。事實上,對於某些瀏覽器,比如 IE6 或 FF2,目前已經有一些方法可以篡改 Referer 值。如果 bank.example 網站支持 IE6 瀏覽器,黑客完全可以把用戶瀏覽器的 Referer 值設為以 bank.example 域名開頭的地址,這樣就可以通過驗證,從而進行 CSRF 攻擊。

即便是使用最新的瀏覽器,黑客無法篡改 Referer 值,這種方法仍然有問題。因為 Referer 值會記錄下用戶的訪問來源,有些用戶認為這樣會侵犯到他們自己的隱私權,特別是有些組織擔心 Referer 值會把組織內網中的某些信息泄露到外網中。因此,用戶自己可以設置瀏覽器使其在發送請求時不再提供 Referer。當他們正常訪問銀行網站時,網站會因為請求沒有 Referer 值而認為是 CSRF 攻擊,拒絕合法用戶的訪問。

在請求地址中添加 token 並驗證

CSRF 攻擊之所以能夠成功,是因為黑客可以完全偽造用戶的請求,該請求中所有的用戶驗證信息都是存在於 cookie 中,因此黑客可以在不知道這些驗證信息的情況下直接利用用戶自己的 cookie 來通過安全驗證。要抵禦 CSRF,關鍵在於在請求中放入黑客所不能偽造的信息,並且該信息不存在於 cookie 之中。可以在 HTTP 請求中以參數的形式加入一個隨機產生的 token,並在服務器端建立一個攔截器來驗證這個 token,如果請求中沒有 token 或者 token 內容不正確,則認為可能是 CSRF 攻擊而拒絕該請求。

這種方法要比檢查 Referer 要安全一些,token 可以在用戶登陸后產生並放於 session 之中,然後在每次請求時把 token 從 session 中拿出,與請求中的 token 進行比對,但這種方法的難點在於如何把 token 以參數的形式加入請求。對於 GET 請求,token 將附在請求地址之後,這樣 URL 就變成 http://url?csrftoken=tokenvalue。 而對於 POST 請求來說,要在 form 的最後加上 <input type=”hidden” name=”csrftoken” value=”tokenvalue”/>,這樣就把 token 以參數的形式加入請求了。但是,在一個網站中,可以接受請求的地方非常多,要對於每一個請求都加上 token 是很麻煩的,並且很容易漏掉,通常使用的方法就是在每次頁面加載時,使用 javascript 遍歷整個 dom 樹,對於 dom 中所有的 a 和 form 標籤后加入 token。這樣可以解決大部分的請求,但是對於在頁面加載之後動態生成的 html 代碼,這種方法就沒有作用,還需要程序員在編碼時手動添加 token。

該方法還有一個缺點是難以保證 token 本身的安全。特別是在一些論壇之類支持用戶自己發表內容的網站,黑客可以在上面發布自己個人網站的地址。由於系統也會在這個地址後面加上 token,黑客可以在自己的網站上得到這個 token,並馬上就可以發動 CSRF 攻擊。為了避免這一點,系統可以在添加 token 的時候增加一個判斷,如果這個鏈接是鏈到自己本站的,就在後面添加 token,如果是通向外網則不加。不過,即使這個 csrftoken 不以參數的形式附加在請求之中,黑客的網站也同樣可以通過 Referer 來得到這個 token 值以發動 CSRF 攻擊。這也是一些用戶喜歡手動關閉瀏覽器 Referer 功能的原因。

在 HTTP 頭中自定義屬性並驗證

這種方法也是使用 token 並進行驗證,和上一種方法不同的是,這裏並不是把 token 以參數的形式置於 HTTP 請求之中,而是把它放到 HTTP 頭中自定義的屬性里。通過 XMLHttpRequest 這個類,可以一次性給所有該類請求加上 csrftoken 這個 HTTP 頭屬性,並把 token 值放入其中。這樣解決了上種方法在請求中加入 token 的不便,同時,通過 XMLHttpRequest 請求的地址不會被記錄到瀏覽器的地址欄,也不用擔心 token 會透過 Referer 泄露到其他網站中去。

然而這種方法的局限性非常大。XMLHttpRequest 請求通常用於 Ajax 方法中對於頁面局部的異步刷新,並非所有的請求都適合用這個類來發起,而且通過該類請求得到的頁面不能被瀏覽器所記錄下,從而進行前進,後退,刷新,收藏等操作,給用戶帶來不便。另外,對於沒有進行 CSRF 防護的遺留系統來說,要採用這種方法來進行防護,要把所有請求都改為 XMLHttpRequest 請求,這樣幾乎是要重寫整個網站,這代價無疑是不能接受的。

Java 代碼示例

下文將以 Java 為例,對上述三種方法分別用代碼進行示例。無論使用何種方法,在服務器端的攔截器必不可少,它將負責檢查到來的請求是否符合要求,然後視結果而決定是否繼續請求或者丟棄。在 Java 中,攔截器是由 Filter 來實現的。我們可以編寫一個 Filter,並在 web.xml 中對其進行配置,使其對於訪問所有需要 CSRF 保護的資源的請求進行攔截。

在 filter 中對請求的 Referer 驗證代碼如下

 

清單 1. 在 Filter 中驗證 Referer

// 從 HTTP 頭中取得 Referer 值
String referer=request.getHeader("Referer"); 
// 判斷 Referer 是否以 bank.example 開頭
if((referer!=null) &&(referer.trim().startsWith(“bank.example”))){ 
   chain.doFilter(request, response); 
}else{ 
   request.getRequestDispatcher(“error.jsp”).forward(request,response); 
}

以上代碼先取得 Referer 值,然後進行判斷,當其非空並以 bank.example 開頭時,則繼續請求,否則的話可能是 CSRF 攻擊,轉到 error.jsp 頁面。

如果要進一步驗證請求中的 token 值,代碼如下

清單 2. 在 filter 中驗證請求中的 token

HttpServletRequest req = (HttpServletRequest)request; 
HttpSession s = req.getSession(); 
 
// 從 session 中得到 csrftoken 屬性
String sToken = (String)s.getAttribute(“csrftoken”); 
if(sToken == null){ 
 
   // 產生新的 token 放入 session 中
   sToken = generateToken(); 
   s.setAttribute(“csrftoken”,sToken); 
   chain.doFilter(request, response); 
} else{ 
 
   // 從 HTTP 頭中取得 csrftoken 
   String xhrToken = req.getHeader(“csrftoken”); 
 
   // 從請求參數中取得 csrftoken 
   String pToken = req.getParameter(“csrftoken”); 
   if(sToken != null && xhrToken != null && sToken.equals(xhrToken)){ 
       chain.doFilter(request, response); 
   }else if(sToken != null && pToken != null && sToken.equals(pToken)){ 
       chain.doFilter(request, response); 
   }else{ 
       request.getRequestDispatcher(“error.jsp”).forward(request,response); 
   } 
}

首先判斷 session 中有沒有 csrftoken,如果沒有,則認為是第一次訪問,session 是新建立的,這時生成一個新的 token,放於 session 之中,並繼續執行請求。如果 session 中已經有 csrftoken,則說明用戶已經與服務器之間建立了一個活躍的 session,這時要看這個請求中有沒有同時附帶這個 token,由於請求可能來自於常規的訪問或是 XMLHttpRequest 異步訪問,我們分別嘗試從請求中獲取 csrftoken 參數以及從 HTTP 頭中獲取 csrftoken 自定義屬性並與 session 中的值進行比較,只要有一個地方帶有有效 token,就判定請求合法,可以繼續執行,否則就轉到錯誤頁面。生成 token 有很多種方法,任何的隨機算法都可以使用,Java 的 UUID 類也是一個不錯的選擇。

除了在服務器端利用 filter 來驗證 token 的值以外,我們還需要在客戶端給每個請求附加上這個 token,這是利用 js 來給 html 中的鏈接和表單請求地址附加 csrftoken 代碼,其中已定義 token 為全局變量,其值可以從 session 中得到。

清單 3. 在客戶端對於請求附加 token

function appendToken(){ 
   updateForms(); 
   updateTags(); 
} 
 
function updateForms() { 
   // 得到頁面中所有的 form 元素
   var forms = document.getElementsByTagName('form'); 
   for(i=0; i<forms.length; i++) { 
       var url = forms[i].action; 
 
       // 如果這個 form 的 action 值為空,則不附加 csrftoken 
       if(url == null || url == "" ) continue; 
 
       // 動態生成 input 元素,加入到 form 之後
       var e = document.createElement("input"); 
       e.name = "csrftoken"; 
       e.value = token; 
       e.type="hidden"; 
       forms[i].appendChild(e); 
   } 
} 
 
function updateTags() { 
   var all = document.getElementsByTagName('a'); 
   var len = all.length; 
 
   // 遍歷所有 a 元素
   for(var i=0; i<len; i++) { 
       var e = all[i]; 
       updateTag(e, 'href', token); 
   } 
} 
 
function updateTag(element, attr, token) { 
   var location = element.getAttribute(attr); 
   if(location != null && location != '' '' ) { 
       var fragmentIndex = location.indexOf('#'); 
       var fragment = null; 
       if(fragmentIndex != -1){ 
 
           //url 中含有隻相當頁的錨標記
           fragment = location.substring(fragmentIndex); 
           location = location.substring(0,fragmentIndex); 
       } 
 
       var index = location.indexOf('?'); 
 
       if(index != -1) { 
           //url 中已含有其他參數
           location = location + '&csrftoken=' + token; 
       } else { 
           //url 中沒有其他參數
           location = location + '?csrftoken=' + token; 
       } 
       if(fragment != null){ 
           location += fragment; 
       } 
 
       element.setAttribute(attr, location); 
   } 
}

在客戶端 html 中,主要是有兩個地方需要加上 token,一個是表單 form,另一個就是鏈接 a。這段代碼首先遍歷所有的 form,在 form 最後添加一隱藏字段,把 csrftoken 放入其中。然後,代碼遍歷所有的鏈接標記 a,在其 href 屬性中加入 csrftoken 參數。注意對於 a.href 來說,可能該屬性已經有參數,或者有錨標記。因此需要分情況討論,以不同的格式把 csrftoken 加入其中。

如果你的網站使用 XMLHttpRequest,那麼還需要在 HTTP 頭中自定義 csrftoken 屬性,利用 dojo.xhr 給 XMLHttpRequest 加上自定義屬性代碼如下:

清單 4. 在 HTTP 頭中自定義屬性

var plainXhr = dojo.xhr; 
 
// 重寫 dojo.xhr 方法
dojo.xhr = function(method,args,hasBody) { 
   // 確保 header 對象存在
   args.headers = args.header || {}; 
 
   tokenValue = '<%=request.getSession(false).getAttribute("csrftoken")%>'; 
   var token = dojo.getObject("tokenValue"); 
 
   // 把 csrftoken 屬性放到頭中
   args.headers["csrftoken"] = (token) ? token : "  "; 
   return plainXhr(method,args,hasBody); 
};

這裏改寫了 dojo.xhr 的方法,首先確保 dojo.xhr 中存在 HTTP 頭,然後在 args.headers 中添加 csrftoken 字段,並把 token 值從 session 里拿出放入字段中。

CSRF 防禦方法選擇之道

通過上文討論可知,目前業界應對 CSRF 攻擊有一些克制方法,但是每種方法都有利弊,沒有一種方法是完美的。如何選擇合適的方法非常重要。如果網站是一個現有系統,想要在最短時間內獲得一定程度的 CSRF 的保護,那麼驗證 Referer 的方法是最方便的,要想增加安全性的話,可以選擇不支持低版本瀏覽器,畢竟就目前來說,IE7+, FF3+ 這類高版本瀏覽器的 Referer 值還無法被篡改。

如果系統必須支持 IE6,並且仍然需要高安全性。那麼就要使用 token 來進行驗證,在大部分情況下,使用 XmlHttpRequest 並不合適,token 只能以參數的形式放於請求之中,若你的系統不支持用戶自己發布信息,那這種程度的防護已經足夠,否則的話,你仍然難以防範 token 被黑客竊取並發動攻擊。在這種情況下,你需要小心規劃你網站提供的各種服務,從中間找出那些允許用戶自己發布信息的部分,把它們與其他服務分開,使用不同的 token 進行保護,這樣可以有效抵禦黑客對於你關鍵服務的攻擊,把危害降到最低。畢竟,刪除別人一個帖子比直接從別人賬號中轉走大筆存款嚴重程度要輕的多。

如果是開發一個全新的系統,則抵禦 CSRF 的選擇要大得多。筆者建議對於重要的服務,可以盡量使用 XMLHttpRequest 來訪問,這樣增加 token 要容易很多。另外盡量避免在 js 代碼中使用複雜邏輯來構造常規的同步請求來訪問需要 CSRF 保護的資源,比如 window.location 和 document.createElement(“a”) 之類,這樣也可以減少在附加 token 時產生的不必要的麻煩。

最後,要記住 CSRF 不是黑客唯一的攻擊手段,無論你 CSRF 防範有多麼嚴密,如果你系統有其他安全漏洞,比如跨站域腳本攻擊 XSS,那麼黑客就可以繞過你的安全防護,展開包括 CSRF 在內的各種攻擊,你的防線將如同虛設。我們只有充分重視 CSRF,根據系統的實際情況選擇最合適的策略,這樣才能把 CSRF 的危害降到最低。

 

點擊關注,第一時間了解華為雲新鮮技術~

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

Day8-微信小程序實戰-交友小程序-首頁用戶列表渲染及多賬號調試及其點贊功能的實現

在這之前已經把編輯個人的所有信息的功能已經完成了

之後先對首頁的列表搞動態的,之前都是寫死的靜態

1、之前都是把好友寫死的,現在就在js裏面定義一個數組,用循環來動態的綁定

在onReady中定義,取真實的數據給定義的列表數組list  

通過調用  db.collection(‘users’).get()  這裏沒有加其他的限制,得到的就是所有的數據了,拿到全部的數據之後就會觸發then方法了

用then返回的res中有一個data的列表集合,有一個注意的點就是,這樣子讀取是吧數據庫中的數據的全部字段,但是我們需要用的只是

用戶的頭像和點贊數、用戶昵稱,其他的數據其實是不需要的

可以加一個field方法,可以要求返回的字段是哪些的

 

可以看到是只有一個用戶,我們為了模擬的話,就可以多賬號進行調試,

 

 1、創立多賬號

    ①首先這個多賬號的一定要是測試號,所以先進入微信的管理後台 https://mp.weixin.qq.com/

    ②進入 【成員管理】添加項目成員

    ③回到微信開發者工具中-》工具-》多賬號調試-》可以通過添加虛擬測試號來進行測試的,不用真實的微信號都可以

 

之後就是對點贊的功能進行設計了(就可以在index.wxml中給點贊的小心上加一個點擊事件即可了

(小細節在小程序中規定,在客戶端中讀取用戶列表的時候,一般不會把整個數據庫的用戶都讀取出來的,是有一個限制的

一般都是把前二十條數據給讀取出來的,如果數據一多的話,一般都是進行分頁處理了–一般都是用數據庫中的collection.skip和collection.limit,這兩個東西一起配合的話就可以做下拉加載的功能了)

由於這個點贊是要對找到用戶的id地址的,所以在wxml中給點贊這個圖標加上一個id自定義屬性的掛載

 

data-id="{{ item._id}}"

這個東西的用處就是,在點擊這個心心的時候就可以拿到這個自定義的屬性id了  

通過把handleLinks(ev)函數中把ev打印出來發現,這個自定義屬性id 的位置在

 ev.target.dataset.id

console.log就是用來測試的,如果沒效果的話,一般都是直接在點擊事件後面通過promise 的then把res打印出來看看情況是怎麼樣的

***有時候這些點擊事件無法觸發的話,就可以在檢查一下樣式,可能這個區域在前端显示是在這裏,但是實際上是在其他的地方,

 

 

 也就是布局引起的問題了

 

 

 

 可以看到這裏就是碰到的是點贊的這個圖標,但是這個樣式是在左上角進行了渲染的

這個時候為了演示把,就把自定義屬性,和點擊事件放在點贊圖標還有點贊數包含的這個text裏面了

<text bindtap="handleLinks" data-id="{{ item._id}}" >
          <!-- 點贊圖標 -->
              <text class="iconfont icondianzan"
              ></text>
          <!-- 點贊數 -->
              <text>{{ item.links }}</text>
          </text>

改完之後,再點擊心心或者是數量,就可以正常的進行點贊了

  但是發現只有給自己點贊的時候才可以改變點贊的數量,而改其他人點贊的時候就改不了

比如:

 

 

 這就是一個權限的問題了也就是只能改自己的數據(點贊數)改不了別人的數據l

 

 

因為在小程序端中,由於用戶可以直接對數據庫進行操作,所以會有一定的風險,所以就通過這個訪問權限來進行了限制

那?

怎麼修改別人的數據呢?—這個操作就要在服務端來進行操作了!  也就是在雲函數中去完成一個雲函數的操作了

 

下面就是講解 如何在 服務端來對数字字段來進行更改!

 

二、點贊功能實現與update雲函數

 由於要在服務端來做的話,就可以把這一塊的部分代碼刪掉了

  handleLinks(ev){
    let id = ev.target.dataset.id;
    db.collection('users').doc(id).update({
      data : {
        links : 5
      }
    }).then((res)=>{
      console.log(res)
    }); 
  }

需要新創一個雲函數(新建一個node.js雲函數

 

這些默認的結構都是可以刪掉的了

 

 之後就是參考 微信開放文檔 雲開發-》SDK文檔->數據庫-》collection->update-》示例代碼demo

其中:這個就是指定了數據庫的環境

cloud.init({
  env: cloud.DYNAMIC_CURRENT_ENV
})

然後就是在服務端拿到數據庫db

const db = cloud.database()

然後可以在示例代碼中看到async這個異步的操作

直接複製 try catch

try {
    return await db.collection('todos').where({
      done: false
    })
    .update({
      data: {
        progress: _.inc(10)
      },
    })
  } catch(e) {
    console.error(e)
  }

return就是返回一個異步的數據,而catch就是返回錯誤信息

主要修改的就是 db.collecion()中要反問那個數據庫表單,這裏不能寫死為users,因為可能其他的地方也是要用到更新的這個功能的

所以最好就是把update這個雲函數寫成一個通用的方式

其中雲函數入口函數 

exports.main = async (event, context) => {  這個裡面的event就是前端傳參過來的對象了 就訪問 event.colletion,然後不用where而是用doc 再把event的doc也傳進來,這 ***小知識點:ES6的擴展運算符 。。。 (三個點)

更新成功了之後,就可以返回結果到前台了

這個就是update雲函數中js文件的代碼(傳進去的doc實際上是用戶的_id

 // 雲函數入口文件
const cloud = require('wx-server-sdk')

cloud.init({
  env: cloud.DYNAMIC_CURRENT_ENV
})
const db = cloud.database()
// 雲函數入口函數
exports.main = async (event, context) => {
  try {
    return await db.collection(event.collection).doc(event.doc)
      .update({
        data: {
         ...event.data 
        },
      })
  } catch (e) {
    console.error(e)
  }
}

View Code

把雲函數寫好之後,這個時候雲函數還是在本地,要把這個雲函數傳到雲開發平台上

 

上傳了之後一定要去雲平台-》雲函數中去檢查一下

 

之後就可以調用這個雲函數了

再回到index.js 點贊的方法 handleLinkes方法中進行設置即可;

 

 如果雲函數沒問題,可能是雲函數裏面定義的env出了問題,就可以寫死了,傳入自己的那個環境,就不用默認的那個環境了

【注意】修改了雲函數記得要重新上傳到雲平台才行

 

 

 

 (因為在服務端是不會受到數據庫權限的限制的)

後面要優化的就是(上面的點贊是寫死給多少links的,並且不能點完之後立馬更新

可以把數據庫的links讀出來,+1之後再寫入,但是這樣的話就多了一個數據庫的操縱了  ,但是數據庫本身就提供了累加或者累減等運算的操作的

(這樣的話就只需要一次的數據庫讀取即可了)

在開放文檔 db.command裏面就有很多的方法

https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/database/command/Command.inc.html

示例代碼
將一個 todo 的進度自增 10:

const _ = db.command
db.collection('todos').doc('todo-id').update({
  data: {
    progress: _.inc(10)
  }
})

為了在服務端不把運算給寫死了,一般都是把運算直接通過前端來傳入的

由於前端不認識下劃線這種操作,因為前端要先解析,之後再把東西傳到服務端的,所以就可以在前端給服務端傳一個字符串的話

然後再在服務端解析即可

 data : "{links : _.inc(1)}"

在前端把這個字符串傳入到服務端中,之後就可以在服務端那邊對這個字符串進行解析了

所以在update雲函數中 就要對event.data這個傳過來的數據進行判斷,判斷它的類型,是普通的還是字符串類型的,如果是字符串類型的話就要進行解析

用js裏面的eval方法,它是把字符串轉成 js 語句的

 if(typeof event.data == 'string'){
      event.data =  eval('(' + event.data + ')')
    }

即可了(點一下心心就可以讓點贊數+1)

後面有空的話,可以繼續進行優化,也就是對一次的點贊數進行限制,或者是點一下加+1,再點一下就是取消了就-1了

後面就是要把點贊了之後,實時的把點贊數進行更新

 

可以看到給服務端那邊上傳之後,對數據庫進行了更新之後,then返回的結果res,中有一個是updated==1,就可以進行if判斷了

再用for循環對列表中的每一個元素判斷,是不是現在被點擊點贊的這個id

let updated = res.result.stats.updated;
    if(updated){
      // 先用擴展運算符 克隆一份數組
      let cloneListData = [...this.data.listData];
      for(let i = 0;i < cloneListData.length ; i++){
        if( cloneListData[i]._id == id){ 
          cloneListData[i].links++;
        }
      }
      this.setData({
        listData : cloneListData
      });
    }

點贊數增加 就是通過_inc 但是在服務端中的update函數中是用全局的,不能寫死,所以運算的規則就通過前端傳過去

為了以後其他的頁面也有更新功能的話做準備了

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

一起玩轉微服務(7)——單一職責

 

單一職責

單一職責原則(Single Responsibility Principle, SRP):一個類只負責一個功能領域中的相應職責,或者可以定義為:就一個類而言,應該只有一個引起它變化的原因。

單一職責原則是實現高內聚、低耦合的指導方針,它是最簡單但又最難運用的原則

單一職責原則是最簡單的面向對象設計原則,它用於控制類的粒度大小

設計原則很重要的一點就是簡單,單一職責,也就是我們經常所說的專人干專事。

一個單元(一個類、函數或者微服務)應該有且只有一個職責。無論如何,一個微服務不應該包含多於一個的職責。職責單一的後果之一就是職責單位(微服務,類,接口,函數)的數量劇增。據說Amazon,Netflix這些採用微服務架構的網站一個小功能就會調用幾十上百個微服務。但是相較於每個函數都是多個業務邏輯或職責功能的混合體的情況,維護成本還是低很多的。 SRP中的“單一職責”是個比較模糊的概念。對於函數,它可能指單一的功能,不涉及複雜邏輯;但對於類或者接口,它可能是指對單一對象的操作,也可能是指對該對象單一屬性的操作。總而言之,單一職責原則就是為了讓代碼邏輯更加清晰,可維護性更好,定位問題更快的一種設計原則。

什麼是高內聚低耦合?

這犀利的措辭一看就是來自開發界的術語。高內聚是說一個功能模塊最好僅完成一個獨立的子功能並且完成的很好。低耦合是指模塊與模塊之間盡量獨立/聯繫少/接口簡單。

這個原則出現的背景是為了讓程序“可復用/可擴展/夠靈活/可維護”。干過一陣子產品的人對這幾個詞應該都不陌生。對於程序設計者來說,這幾個詞是十分重要的,不亞於產品經理口中的“用戶體驗”(原則or擋箭牌)。

優點

單一職責的優點如下:

•類的複雜性降低,實現什麼職責都有清晰明確的定義。•可讀性提高,複雜性降低。•可維護性提高,可讀性提高。•變更引起的風險降低,變更是必不可少的,如果接口的單一職責做得好,一個接口修改只對相應的實現類有影響,對其他的接口無影響,這對系統的擴展性、維護性都有非常大的幫助。

記得在三字經裡邊有這樣一段 教之道,貴以專(出自三字經) 說的就是無論學習還是構建團隊,最重要的是專才,而不是全才。就好比一個足球隊,如果都是前鋒或者都是後衛,那麼這樣的球隊一定不能出成績,反而是將各個位置上的人進行統一協調,根據分工不同,共同協作,形成1+1>2的效果,那麼這樣的團隊就非常容易出成績。

有很多公司為了趕進度,經常會招聘一些所謂的全能型人才,但是這種人往往專業的程度不夠,當遇到某些棘手的問題的時候,往往不能夠非常快速的解決問題。從而導致最終交付的質量較差。

單一職責的目的

實施單一職責的目的如下:

•以類來隔離需求功能點,這樣當一個點的需求發生變化的時候,不會影響別的類的邏輯,這個和設計模式中的開閉原則類似,對於擴展持開放態度,對於修改持關閉態度。•是一個原子模塊級的粒度,至於原子的粒度到底是什麼樣的,應該因業務而異,設計的過程中同時考慮業務的擴展,所以這就要求在設計的過程中,必須有業務專家共同參与,共同規避風險。•粒度小,靈活,復用性強,方便更高一級別的抽象。

每個微服務單獨運行在獨立的進程中,能夠實現松耦合,並且獨立部署。

如何做

分3步:

1.把一個具體的問題抽象成一類問題;

2.根據用戶體驗流程劃分功能模塊;

3.針對每個功能設計封閉的解決方案。

最佳實踐

在實際工作中,有一個經常會用到的設計模式,DAO模式,又叫數據訪問對象,裏面定義了數據庫中表的增、刪、改、查操作,按照單一職責原則,為什麼不把增、刪、改、查分別定義成四種接口?這是因為數據庫的表操作,基本上就是這四種類型,不可能變化,所以沒有必要分開定義,反而經常變化的是數據庫的表結構,表結構一變,這四種操作都要跟着變。所以通常我們會針對一張表實現一個DAO,一張表就代表一種類型的職責。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

鍵盤俠Linux教程(五)| 基本權限管理

基本權限管理

權限的介紹

權限位的含義

前面講解ls命令時,我們已經知道長格式显示的第一列就是文件的權限,例如:

[root@es ~]# ls -l anaconda-ks.cfg
-rw-------. 1 root root 1573 May 18 23:28 anaconda-ks.cfg

第一位為文件類型

文件類型標識 文件類型
普通文件
d 目錄文件
l 軟鏈接文件
s(偽文件) 套接字文件
b(偽文件) 塊設備文件
c(偽文件) 字符設備文件
p(偽文件) 管道符文件

第一列的權限位如果不計算最後的 “.” (這個點的含義我們在後面解釋),則共有10位,這10位權限位的含義如圖所示。

基本權限介紹

修改權限的命令 chmod ,基本信息如下

命令格式

[root@es ~]#chmod [選項] 權限模式 文件名

選項:

-R : 遞歸設置權限,也就是給子目錄中的所有文件設定權限

權限模式

chmod 命令的權限模式的格式時”[ugoa][[+-=][perms]]”,也就是“[用戶身份][[賦予方式][權限]]”的格式

  • 用戶身份:

    • u:代表所有者(user)

    • g:代表所屬組(group)

    • o:代表其他人(other)

    • a:代表全部身份(all)

  • 賦予方式:

    • +:加入權限

    • -:減去權限

    • =:設置權限

  • 權限:

    • -r:讀取權限(read)

    • -w:寫權限(write)

    • -x:執行權限(execute)

数字權限

数字權限的賦予方式是最簡單的,但是不如之前的字母權限好記,直觀。我們來看看這些数字權限的含義。

  • 4:代表”r”權限

  • 2:代表”w”權限

  • 1:代表”x”權限

常用權限

数字權限的賦予方式更加簡單,但是需要用戶對這幾個数字更加熟悉。其實常用權限也並不多,只有如下幾個。

  • 644:這是文件的基本權限,代表所有者擁有讀,寫權限,而所屬組和其他人擁有隻讀權限。

  • 755:這是文件的執行權限和目錄的基本權限,代表所有者擁有讀,寫和執行權限,而所屬組和其他人擁有讀和執行權限。

  • 777:這時最大權限。在實際的生產服務器中,要儘力避免給文件或目錄賦予這樣的權限,這會造成一定的安全隱患。

基本權限的作用

權限含義的解釋

首先,讀,寫,執行權限對文件和目錄的作用是不同的。

  • 權限對文件的作用

    • 讀(r):對文件有讀(r)權限,代表可以讀取文件中的數據。如果把權限對應到命令上,那麼一旦對文件有讀(r)權限,就可以對文件執行cat,more,less,head,tail等文件查看命令。

    • 寫(w):對文件有寫(w)權限,代表可以修改文件中的數據。如果把權限對應到命令上,那麼一旦對文件有寫(w)權限,就可以對文件執行vim,echo等修改文件數據的命令。注意:對文件有寫權限,是不能刪除文件本身的,只能修改文件中的數據。如果要想刪除文件,則需要對文件的上級目錄擁有寫權限。

    • 執行(x):對文件有執行(x)權限,代表文件擁有了執行權限,可以運行。在Linux中,只要文件有執行(x)權限,這個文件就是執行文件了。只是這個文件到底能不能正確執行,不僅需要執行(x)權限,還要看文件中代碼是不是正確的語言代碼。文件夾來說,執行(x)權限是最高權限。

  • 權限對目錄的作用

    • 讀(r):對目錄有讀(r)權限,代表可以查看目錄下的內容,也就是可以查看目錄下有哪些子文件和子目錄。如果把權限對應到命令上,那麼一旦對目錄擁有了讀(r)權限,就可以在目錄下執行ls命令,查看目錄下的內容

    • 寫(w):對目錄有寫(r)權限,代表可以修改目錄下的數據,也就是可以在目錄中新建,刪除,複製,剪切子文件或子目錄。如果把權限對應到命令上,那麼一旦目錄擁有了寫(w)權限,就可以在目錄下執行touch,rm,cp,mv命令。對目錄來說寫(w)權限是最高權限。

    • 執行(x):目錄是不能運行的,那麼對目錄擁有執行(x)權限,代表可以進入到目錄。如果把權限對應到命令上,那麼一旦對目錄擁有了執行(x)權限,就可以對目錄執行cd命令,進入目錄。

目錄的可用權限

目錄的可用權限其實有以下幾個

  • 0:任何權限都不賦予

  • 5:基本的目錄瀏覽和進入權限

  • 7:完全權限

所有者和所屬組命令

chown命令

修改權限的命令 chown ,基本信息如下

[root@es ~]#chown [選項] 所有者:所屬組 文件或目錄

選項:

-R : 遞歸設置權限,也就是給子目錄中的所有文件設定權限

普通用戶不能修改文件的所有者,哪怕自己是這個文件的所有者也不行,只有超級用戶才能修改
普通用戶可以修改所有者是自己的文件的權限=

umask 默認權限

查看系統的umask權限

#用八進制數值显示umask權限
[root@centos7 ~]# umask
0022

#用字母表示文件和目錄的初始權限
[root@centos7 ~]# umask -S
u=rwx,g=rx,o=rx

umask權限的計算方法

我們需要先了解一下新建文件和目錄的默認最大權限

  • 對文件來講,新建文件的默認最大權限是666,沒有執行(x)權限。這時因為執行權限對文件來講比較危險,不能在新建文件的時候默認賦予,而必須通過用戶手工賦予。

  • 文件的默認權限最大隻能是666,而umask的值是022
    “-rw-rw-rw-“減去”—–w–w-” 等於”-rw-r–r–“

  • 對目錄來講,新建文件的默認最大權限是777。這時因為對目錄而言,執行(x)權限僅僅代表進入目錄,所以即使建立新文件時直接默認賦予,也沒有什麼危險。

  • 目錄的權人權限最大隻能是777,而umask的值是022

“drwxrwxrwx” 減去 “d—-w–w-” 等於 “drwx-r-xr-x”

寫在最後

如果文檔對你有幫助的話,留個贊再走吧 ,你的點擊是我的最大動力。

我是鍵盤俠,現實中我唯唯諾諾,網絡上我重拳出擊,關注我,持續更新Linux乾貨教程。

更多鍵盤俠Linux系列教程:鏈接地址

更多Linux乾貨教程請掃:

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※教你寫出一流的銷售文案?

教你一招另闢蹊徑抓取美團火鍋數據

最近有個小夥伴在群里問美團數據怎麼獲取,而且她只要火鍋數據,她在上海,只要求抓上海美團火鍋的數據,而且要求也不高,只要100條,想做個簡單的分析,相關的字段如下圖所示。

乍一看,這個問題還真的是蠻難的,畢竟美團也不是那麼好抓,什麼驗證碼,模擬登陸等一大堆拂面而來,嚇得小夥伴都倒地了。

通過F12查看,抓包,分析URL,找規律,等等操作。

不過白慌,今天小編給大家介紹一個小技巧,另闢蹊徑去搞定美團的數據,這裏需要用到抓包工具Fiddler。講道理,之前我開始接觸網絡爬蟲的時候也沒有聽過這個東東,後來就慢慢知道了,而且它真的蠻實用的,建議大家都能學會用它。這個工具專門用於抓包,而且其安裝包也非常小,如下圖所示。

接下來,我們開始進行抓取信息。

1、在Fiddler的左側找到meituan網站的鏈接,如下圖所示。鏈接的左邊返回的response(響應)的文件類型,可以看到是JSON文件,爾後雙擊這一行鏈接。

2、此時在右側會显示下圖的界面,點擊黃色區域內的那串英文“Responsebody is encoded. Click to decode.”意思是response是加密的,點擊此處進行解碼,對返回的網頁進行解碼。

3、此時會彈出下圖所示的界面,在WebView中可以看到返回的數據,與網頁中的內容對應一致。

4、不過美團網限制一頁最多显示32條火鍋信息,如下圖所示。

5、如果我想獲取100條信息的話,那得前後找4頁,才能夠滿足要求。有沒有辦法讓其一次性多显示一些數據呢?答案是可以的,操作方法如下。

在左側找到對應的美團網鏈接,然後點擊右鍵一次選擇CopyàJustUrl,如下圖所示。

7、將得到的URL放到瀏覽器中去進行訪問,如下圖所示。可以看到limit=32,即代表可以獲取到32條相關的火鍋信息,並且返回的內容和Fiddler抓包工具返回的信息是一致的。

8、此時,我們直接在瀏覽器中將limit=32這個參數改為limit=100,也就是說將32更改為100,讓其一次性返回100條火鍋數據,天助我也,竟然可以一次性訪問到,如下圖所示。就這樣,輕輕鬆松的拿到了一百條數據。

9、接下來,可以將瀏覽器返回的數據進行Ctrl+A全部選中,放到一個本地文件中去,存為txt格式,在sublime中打開,如下圖所示。

10、其實乍一看覺得很亂,其實它就是一個JSON文件,剩下的工作就是對這個JSON文件做字符串的提取,寫個代碼,提取我們的目標信息,包括店門、星級、評論數、關鍵詞、地址、人均消費等,如下圖所示。

11、運行程序之後,我們會得到一個txt文件,列與列之間以製表符分開,如下圖所示。

12、在txt文件中看上去很是費勁,將其導入到Excel文件中去,就清晰多了,如下圖所示。接下來就可以很方便的對數據做分析什麼的了。

13、至此,抓取美團火鍋數據的簡易方法就介紹到這裏了,希望小夥伴們都可以學會,以後抓取類似的數據就不用找他人幫你寫程序啦~~

14、關於本文涉及的部分代碼,小編已經上傳到github了,後台回復【美團火鍋】四個字即可獲取。

看完本文有收穫?請轉發分享給更多的人

IT共享之家

入群請在微信後台回復【入群】

想學習更多Python網絡爬蟲與數據挖掘知識,可前往專業網站:http://pdcfighting.com/

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※別再煩惱如何寫文案,掌握八大原則!

※產品缺大量曝光嗎?你需要的是一流包裝設計!

TestLoader源碼解析

 1 def loadTestsFromTestCase(self, testCaseClass) #看名稱分析:從TestCase找測試集--那麼就是把我們的def用例加載到testSuit裏面
 2 def loadTestsFromModule(self, module, *args, pattern=None, **kws) #看名稱分析:從模塊裏面找測試集,那麼 模塊>類>test_方法>添加到testSuit裏面
 3 def loadTestsFromName(self, name, module=None)  #看名稱分析: 接收到name直接添加到testSuit裏面
 4 def loadTestsFromNames(self, names, module=None) #看名稱分析:接受到的是一個包含測試test_方法的列表
 5 def getTestCaseNames(self, testCaseClass) # 看名稱分析:  取出一個包含test_方法的列表
 6 def discover(self, start_dir, pattern='test*.py', top_level_dir=None) ## 看名稱分析:發現--找test_方法
 7 def _get_directory_containing_module(self, module_name) #獲取目錄包含的模塊
 8 def _get_name_from_path(self, path)  #從路徑從找名稱
 9 def _get_module_from_name(self, name)  #從名稱找模塊
10 def _match_path(self, path, full_path, pattern)   #正則匹配路徑--參數包含pattern 那估計是匹配我們測試腳本格式的
11 def _find_tests(self, start_dir, pattern, namespace=False)  #找測試集合
12 def _find_test_path(self, full_path, pattern, namespace=False)  #找測試集合的路徑

View Code

 1 那就是1234
 2 一個discover,getTest,_match_path
 3 二個find
 4 三個_get
 5 四個loadTests
 6 
 7 discover  邏輯
 8 >
 9 _find_tests【兩個處理邏輯  一個是本次傳的目錄和上次傳的一樣或不一樣,】
10 【一樣:直接從我們傳的目錄下面繼續去找testcaose---11 【不一樣:會從我們傳的目錄下面去執行os.path.listdir找到所有的子文件列表paths(文件)),然後遍歷得到單獨的path做 start_dir+path拼接】
12 >
13 ①—get_name_from_path【傳入start_dir,判斷當前傳入的目錄是否為上次傳入的頂級目錄返回".",不一樣可能有點繞-並返回一個值這個值有四種情況 . test  ...test dir.tests--正常應該是返回test文件名
14 ②_find_test_path(self, full_path, pattern, namespace=False)
15 【執行這個從路徑中找test,那麼很明顯 一樣:傳目錄路徑   不一樣傳文件路徑  】
16 _find_test_path

View Code

 1 class TestLoader(object):
 2     """
 3     This class is responsible for loading tests according to various criteria
 4     and returning them wrapped in a TestSuite
 5     """
 6     testMethodPrefix = 'test'
 7     sortTestMethodsUsing = staticmethod(util.three_way_cmp)
 8     suiteClass = suite.TestSuite
 9     _top_level_dir = None
10 
11     def __init__(self):
12         super(TestLoader, self).__init__()
13         self.errors = []
14         # Tracks packages which we have called into via load_tests, to
15         # avoid infinite re-entrancy.
16         self._loading_packages = set()  #這裏創建了一個空的self._loading_packages={}無序且不重複的元素集合

View Code 1.discover方法:unittest.defaultTestLoader a.定義了三個布爾值屬性 is_not_importable==True則是不能導入,is_namespace ,set_implicit_top b.對頂層目錄做了處理–當服務首次啟動 執行unittest.defaultTestLoader.discover(“文件目錄A”,pattern,top_level_dir=None):self._top_level_dir = top_level_dir = start_dir 這三個相等。 再次執行unittest.defaultTestLoader.discover(“文件目錄B”,pattern,top_level_dir=None): top_level_dir=self._top_level_dir【也就是他會默認上次start_dir 為頂層目錄】– 無論是首次還是複次–上面的操作完成之後 self._top_level_dir = top_level_dir 仍然繼續執行了一句這個—也就是說 self._top_level_dir == top_level_dir 始終一樣 c.針對頂層目錄不是一個目錄文件做了一系列的處理如果你傳的目錄是一個可導入的模塊-他在這個異常處理中.會重新自導入這個模塊。並開始追尋他的絕對路徑,判斷其模塊的可用性,然後執行_find_tests()尋找用例 d.如果是一個目錄就直接開始執行_find_tests()尋找用例 e.所以他這裏分兩種情況可以找到用例 第一種:傳的目錄 第二種:傳入的可導入模塊-這種情況self._top_level_dir 最終也是一個絕對路徑

  1 def discover(self, start_dir, pattern='test*.py', top_level_dir=None):  #一般我們top_level_dir傳的都None
  2     set_implicit_top = False   #是否存在頂層目錄
  3     if top_level_dir is None and self._top_level_dir is not None:
  4         # make top_level_dir optional if called from load_tests in a package
  5         top_level_dir = self._top_level_dir  #複次走這裏
  6     elif top_level_dir is None:  #初次走這裏
  7         set_implicit_top = True
  8         top_level_dir = start_dir
  9     #上面這一串花里胡哨的東西就是處理頂層目錄-如果是第一次啟動服務-
 10     #就走elif-top_level_dir==我們下面傳的值--之後--self._top_level_dir就不為空了,
 11     #但是top_level_dir  頂部是處理==None所以會走if=True
 12     top_level_dir = os.path.abspath(top_level_dir)#轉絕對路徑
 13     if not top_level_dir in sys.path:
 14         #這裡是防止重複將top_level_dir加入執行目錄--BUT如果我第一次傳的start_dir=a,第二次傳的start_dir=b
 15         #分析一下--第一次就是把a加入到了執行目錄---下面self._top_level_dir=a 二次(複次)傳b的時候,會出現top_level_dir=a---並沒有判斷b是否在執行目錄
 16         #這裏加一波問號?????????????????????
 17         #但是一般情況 我們目錄就只有一個--所以這裏--先放着。。。先看後面再來看這裏
 18         # all test modules must be importable from the top level directory
 19         # should we *unconditionally* put the start directory in first
 20         # in sys.path to minimise likelihood of conflicts between installed
 21         # modules and development versions?
 22         sys.path.insert(0, top_level_dir)
 23     self._top_level_dir = top_level_dir
 24     #如果top_level_dir我們傳的目錄不在可執行目錄--則臨時添加進去
 25     is_not_importable = False  #是否  不能導入
 26     is_namespace = False    #is_namespace那麼這個字段的意思就是是否可以找到傳入的路徑
 27     tests = []
 28     if os.path.isdir(os.path.abspath(start_dir)):  #判斷我們傳的是否為一個目錄--實際上這裏直接用top_level_dir不香嗎--
 29         start_dir = os.path.abspath(start_dir)
 30         # 之前把top_level_dir = start_dir
 31         # 然後top_level_dir = os.path.abspath(top_level_dir)
 32         #現在start_dir = os.path.abspath(start_dir)
 33         #為什麼不   直接用top_level_dir?  小朋友你是否有許多問號
 34         # 問題出在上面--複次的時候--並沒有走 top_level_dir = start_dir 而是走的 top_level_dir = self._top_level_dir ,
 35         #所以如果我們上次傳的路徑如果和這次不一樣--那麼top_level_dir是不等於start_dir--而start_dir才是我們傳的--
 36         if start_dir != top_level_dir:  #所以這裏相當於判斷前後傳的路徑是否一樣--一般來說我們的start_dir都是等於top_level_dir的
 37             is_not_importable = not os.path.isfile(os.path.join(start_dir, '__init__.py'))#如果是一個文件返回false
 38             #如果不一樣--則判斷我們當前傳入的start_dir/__init__.py是不是一個正確的文件路徑..os.path.isfile() 返回布爾值
 39     else: #如果我們傳入的不是一個目錄,就開始一堆花里胡哨的報錯東西了。。暫時不用管
 40         # support for discovery from dotted module names
 41         try:
 42             __import__(start_dir)
 43             #這裏就很有意思了---__impor__("PyFiles.Besettest")那就是導入PyFiles
 44             #那麼也就是說這個97.33的概率會報錯--也就是說你如果目錄錯了--下面的else基本不會走。。。除非你很神奇的填的路徑右側是一個可導入的模塊
 45         except ImportError:
 46             is_not_importable = True   #如果導入不鳥--就is_not_importable設置為true  在這裏我清楚了這個字段的含義--不能導入=true
 47         else:#那麼這裏假設導入成功之後
 48             the_module = sys.modules[start_dir]  #這裡是如果我們導入成功--就走這裏-取出start_dir導入的賦值給the_module
 49             top_part = start_dir.split('.')[0]    #這裡是將我們導入的模塊名稱取出來
 50             try:
 51                 start_dir = os.path.abspath( #打印導入模塊所在目錄的絕對路徑
 52                    os.path.dirname((the_module.__file__)))
 53             except AttributeError:     #這裡是如果導入模塊成功了---但是尼瑪打印導入模塊的絕對路徑又報錯--不想看了+2
 54                 # look for namespace packages
 55                 try:  #然後有開始進行模塊導入檢查---日了狗了。。。。。
 56                     #  fuck----想直接關機了+1,這裏估計是想找到為什麼不能導入的原因。。大神的思路就是完美,如果是我就拋出一個目錄不對就完事
 57                     #這一塊的學習 文檔 Python標準模塊--import
 58                     spec = the_module.__spec__
 59                     #將導入成功的模塊的規格說明賦值給spec--
 60                     #打印出來就是ModuleSpec(name='besettest.interface', loader=<_frozen_importlib_external.SourceFileLoader object at 0x0000000003D6E780>, origin='E:\\PyFiles\\Besettest\\besettest\\interface\\__init__.py', submodule_search_locations=['E:\\PyFiles\\Besettest\\besettest\\interface'])
 61                     #這麼一串東西--也沒用過。。。。大概就是模塊名稱、路徑、導入的模塊對象吧
 62                     #origin 加載模塊的位置--
 63                     #loader_state模塊特定數據的容器
 64                 except AttributeError:  #如果模塊的規格說明取不出來。。。。。。。
 65                     spec = None   #我查閱了一下。。。的確存在部分模塊規格說明為None的--所以還得繼續往下看
 66 
 67                 if spec and spec.loader is None:   #如果存在規格說明 且 數據容器為None。
 68                     if spec.submodule_search_locations is not None: 
 69                         #這是個什麼玩意呢--模塊 搜索 位置s(列表)。。。
 70                         is_namespace = True    #如果spec.submodule_search_locations不為none -----
 71                         # 2.5級英文翻譯 就是模塊的路徑 如果模塊路徑不為空is_namespace可以找到---is_namespace那麼這個字段的意思就是存在命名空間。。就是可以找到這個模塊
 72                         for path in the_module.__path__:   #這裏我研究懷疑是故意提升逼格。。the_module.__path__==spec.submodule_search_locations
 73                             if (not set_implicit_top and     #首次set_implicit_top==True  複次set_implicit_top==Fase
 74                                 not path.startswith(top_level_dir)):
 75                                 continue
 76                                 #這裏讓我稍微有點疑惑。。為什麼要判斷是首次還是複次--我猜是判斷the_module.__path__列表裡面有幾個某塊的路徑
 77                                 #如果是首次直接下一步--如果是複次會有多個路徑。但是如果是複次top_level_dir這個路徑又是上次的。。。日
 78                                 #他的作用是找到導入模塊的路徑-知道這個就行。。。
 79                             self._top_level_dir = \
 80                                 (path.split(the_module.__name__
 81                                      .replace(".", os.path.sep))[0])   #取出導入模塊的上級目錄絕對路徑。。。
 82                             #the_module.__name__.replace(".", os.path.sep) 這一串我看來是沒有必要的。。因為  the_module.__name__既然取到了模塊名稱那他肯定是一個字符串
 83                             tests.extend(self._find_tests(path,     #然後調用_find_tests 尋找測試。加入tests列表--這個有點熟悉的味道---
 84                                                           pattern,  #我覺得基本不會走這裏去找---腳本路徑一般都會填對  填錯了,都不知道執行到哪裡去了。。。
 85                                                           namespace=True))
 86                 elif the_module.__name__ in sys.builtin_module_names:
 87                     #判斷 sys.builtin_module_names返回一個列表,包含所有已經編譯到Python解釋器里的模塊的名字 和sys.models是一個字典
 88                     #就是沒法導入報錯
 89                     # builtin module
 90                     raise TypeError('Can not use builtin modules '
 91                                     'as dotted module names') from None
 92                 else:  #沒發現這個模塊
 93                     raise TypeError(
 94                         'don\'t know how to discover from {!r}'
 95                         .format(the_module)) from None
 96 
 97             if set_implicit_top:     #如果是首次。。。。
 98                 if not is_namespace:  #is_namespace默認的是false-且可以找到模塊相關規格
 99                     self._top_level_dir = \
100                        self._get_directory_containing_module(top_part)  #interface.testFiles   interface假設這個是導入的 -self._top_level_dir 是一個目錄的絕對路徑
101                     #top_part導入的模塊名稱-----
102                     sys.path.remove(top_level_dir)    #只知道是從系統路徑移除--但是不知道為什麼移除。。。。
103                 else:
104                     sys.path.remove(top_level_dir)   #
105 
106     if is_not_importable:   #如果我們傳的文件不能導入---就直接拋出異常
107         raise ImportError('Start directory is not importable: %r' % start_dir)
108 
109     if not is_namespace:  #is_namespace默認的是false--這裏就是可以找到模塊。。。。
110         tests = list(self._find_tests(start_dir, pattern))
111     return self.suiteClass(tests)

View Code 2._find_tests()–尋找testCase並生成測試套件tests=[]

 1 def _find_tests(self, start_dir, pattern, namespace=False):   #注意這裏如果我們傳的不是腳本目錄而是一個可導入的模塊namespace是等於True的
 2     """Used by discovery. Yields test suites it loads."""
 3     # Handle the __init__ in this package
 4     name = self._get_name_from_path(start_dir)   #返回一個name   name存在三種返回情況  "."-當本次和上次傳入的start_dir一致     不一致  "文件名"    "...文件名"
 5      #get_name_from_path的邏輯在這裏就很清晰了 
 6           
 7     # name is '.' when start_dir == top_level_dir (and top_level_dir is by
 8     # definition not a package).
 9     if name != '.' and name not in self._loading_packages:  
10     #當name最少有一個且也不再self._loading_packages.【self._loading_packages初始化的時候建的空集合】 走下面這個
11         # name is in self._loading_packages while we have called into
12         # loadTestsFromModule with name.
13         tests, should_recurse = self._find_test_path(      #然後這裏start_dir是我們傳的模塊--他就去找。。這裏就恢復到了傳測試目錄的邏輯了
14             start_dir, pattern, namespace)
15         if tests is not None:
16             yield tests
17         if not should_recurse:
18             # Either an error occurred, or load_tests was used by the
19             # package.
20             return
21     # Handle the contents.
22     paths = sorted(os.listdir(start_dir))  #那就從這裏開始--當我們穿的目錄和上次一樣-他會找到目錄下所有的文件然後排序--我們的用例執行順序就是從這裏開始搞了。。
23     for path in paths:       #遍歷我們傳的目錄下的所有文件
24         full_path = os.path.join(start_dir, path)     將我們傳入的目錄和目錄下的py文件拼接的完整路徑
25         tests, should_recurse = self._find_test_path(    #把我們文件路徑和我們的文件格式傳入_find_test_path這個方法--
26             full_path, pattern, namespace)
27         如果當前傳的是一個目錄-會返回should_recurse=True--這個英文直譯是應該_遞歸--下面yield from 就是執行遞歸的操作
28         if tests is not None:
29             yield tests
30         if should_recurse:  #這句是判斷他是不是一個目錄
31             # we found a package that didn't use load_tests.
32             name = self._get_name_from_path(full_path)
33             self._loading_packages.add(name)
34             try:
35                 yield from self._find_tests(full_path, pattern, namespace)
36             finally:
37                 self._loading_packages.discard(name)

View Code yield與yield from

 1 def  a(n):
 2     testList=b(n)
 3     return testList
 4 
 5 def  b(n,m=1):
 6     print("執行第%s次"%m)
 7     for a in range(n):
 8         if not divmod(a,2)[1] and a!=0:
 9             print(a)
10             yield a   #是用yield之後返回的是一個生成器
11             if divmod(a,3)[1]:
12                 m =m+1
13                 yield from b(a,m)  #重新執行b方法
14 
15 print(list(a(7)))
16 
17 執行第1次
18 2
19 執行第2次
20 4
21 執行第3次
22 2
23 執行第4次
24 6
25 [2, 4, 2, 6]

View Code _get_name_from_path 他主要做了:從我們傳的路徑裏面找腳本文件 名。。。如果找到了腳本文件則返迴文件名稱—如果沒找到也就是我們返回一個點 或者 至少一個點(三種情況 . test_case …..test_case 返回name可能存在的三種值) 在_test_find調用這個方法path=start_dir(我們傳的目錄)–這個返回一個點 或者 至少一個點 在 _fin_test_path調用這個方法是傳的我們傳的目錄下的文件路口–返回的name就是文件名

 1 name = self._get_name_from_path(start_dir)    #因為discover我們是支持我們傳目錄或者模塊尋找testcase的,所以這個方法
 2 
 3 def _get_name_from_path(self, path):
 4      #主要正確邏輯三個 比如我們的腳本目錄結構是  E://a/b/     b目錄下面有script.py    和  /c/script.py
 5      #第一次是我們自己傳的目錄--之前在discover他做了一個處理 就是第一次運行時會把我們傳的目錄賦值給頂層目錄---
 6      #第一個邏輯判斷我們傳的是不是  -和頂層一樣---一樣的話===_find_tests方法就直接從目錄下面找腳本-當如如果有目錄也會繼續走--他是在_find_tests_path判斷的--最終也是回到找腳本模塊上
 7      #如果不一樣--那就是找了 找到這個目錄了---那麼就從頂層開始找這個目錄的相對路徑--其實就是找最後那個目錄(必須是一個packge。上面說的目錄都是包)、。。然後返回一個name
 8      #如果還有子目錄  d---那就會返回  c.d
 9     if path == self._top_level_dir:  #首次運行pattern,top_level_dir=None):self._top_level_dir = top_level_dir = start_dir -第二次運行如果目錄沒有變,這裏也是直接返回的
10         return '.'
11         
12         
13     path = _jython_aware_splitext(os.path.normpath(path))  #如果我們當前傳的和上次傳的目錄不一致。。這裏得path我們當前傳的路徑
14     
15     _relpath = os.path.relpath(path, self._top_level_dir)    #從self._top_level_dir開始找path的相對路徑
16     #這裡是從我們傳的path開始找到self._top_level_dir上次傳的相對路徑 
17     #例如: path=path1="E:\\PyFiles\\Besettest\\besettest\\interface\\testFiles"   self._top_level_dir="E:\\PyFiles\\Besettest\\besettest\\interface\\result"
18     #那麼_relpath="..\testFiles"    -暫時還不清楚為什麼要找這個??????????????????????????
19     assert not os.path.isabs(_relpath), "Path must be within the project"
20     #↑↑斷言 不是絕對路徑-也就是說_relpath是否為相對路徑↑↑↑特么的 這裏肯定是一個相對路徑啊。。。上面都有relpath了。。。丟
21     #↓↓↓↓斷言以..開頭就失敗。。。↓↓↓--這兩處超出理解範圍了。。。。。。
22     assert not _relpath.startswith('..'), "Path must be within the project"
23     
24     name = _relpath.replace(os.path.sep, '.')  #然後這裏又把分隔符替換成.  返回 a.b  當然或許會有異常情況返回.....這是我意淫的
25     return name

View Code self._find_test_path #找測試的路徑 兩個主要邏輯: 傳到full_path 是一個文件 還是一個目錄

 1 def _find_test_path(self, full_path, pattern, namespace=False):  
 2  #_find_tests()調用這個方法 傳了一個我們傳的目錄下的a文件路徑、和需要找的文件pattern-namespace【傳的可能是true 也可能是false】,如果我的目錄是對的-namespace傳的就是false
 3     """Used by discovery.
 4 
 5     Loads tests from a single file, or a directories' __init__.py when
 6     passed the directory.
 7 
 8     Returns a tuple (None_or_tests_from_file, should_recurse).
 9     """
10     basename = os.path.basename(full_path)   #basename==文件名.py後續帶py的統稱文件--不帶後綴的統稱文件名。。。
11     if os.path.isfile(full_path):   #如果我們傳的full_path是一個文件---我們在discover傳的是一個腳本目錄-之前在_test_find是做了一個拼接得到的完整路徑full_path
12         if not VALID_MODULE_NAME.match(basename): #判斷他是不是一個py文件----
13             
14             # valid Python identifiers only
15             return None, False   #如果不是直接返回
16         if not self._match_path(basename, full_path, pattern):  #這裏雖然傳了三個值--但是實際上只有basename,pattern有用--
17          #_match_path調用fnmatch(文件, 我們傳的文件格式或文件)這個需要————from fnmatch import fnmatch他的主要作用是做此模塊的主要作用是文件名稱的匹配
18          #當此次傳入的文件名與我們的文件格式匹配一致self._match_path返回true
19             return None, False    #如果不一樣 就直接回到 _find_test繼續找
20         # if the test file matches, load it
21         name = self._get_name_from_path(full_path)   #然後這裏把文件路徑又傳到 self._get_name_from_path去返迴文件名-這個時候因為我們傳的是腳本目錄-full_path目錄下的文件路徑,所以返回的name 就是文件名
22        
23         #self._top_level_dir是當前文件目錄路徑,path是當前文件路徑--從目錄找文件--直接就是文件名--他返回的name就是文件名   
24         try:
25             module = self._get_module_from_name(name)    #_get_module_from_name  這個方法就是動態導入模塊名--然後返回一個所有導入的模塊的對象 moudel.__file__路徑、moudel.__name__名稱
26         except case.SkipTest as e:   #如果導入不成功 case.SkipTest 實際上case是繼承--Exception--所以把這個理解為Exception就可以了--
27             return _make_skipped_test(name, e, self.suiteClass), False
28         except:
29             error_case, error_message = \
30                 _make_failed_import_test(name, self.suiteClass)   
31             self.errors.append(error_message)
32             return error_case, False
33         else: #module 獲取到值之後走這裏。。
34             mod_file = os.path.abspath(
35                 getattr(module, '__file__', full_path))    #然後這裏取出我們導入模塊的 絕對路徑---如果反射找不到就返回該文件的路徑-其實差別不大-處理一下更嚴謹
36             realpath = _jython_aware_splitext(
37                 os.path.realpath(mod_file))   #os.path.realpath(mod_file)然後又返回真實路徑---然後又去掉路徑的.py,。,,,,,,,,丟
38             fullpath_noext = _jython_aware_splitext(
39                 os.path.realpath(full_path))   #然後full_path 找真實路徑去掉.py
40             if realpath.lower() != fullpath_noext.lower():  #如果動態導入的模塊的目錄路徑 不等於 傳進來(也就是pattern)的目錄路徑--實際上傳進來的路徑肯定是個絕對路徑--因為前面已經轉了好幾次絕對路徑了
41                 module_dir = os.path.dirname(realpath)   #不等於就找動態導入模塊所在的目錄----實際上上面處理的realpath已經是一個目錄了。。但是他防止realpath還是一個.py文件。所以又操作了一次
42                 mod_name = _jython_aware_splitext(    #full_path是文件的路徑.py的,然後這裏又先是basename取出文件(就是把路徑去掉,只留下xxx.py) 然後外面那個方法 把.py去掉--留下文件名
43                     os.path.basename(full_path))
44                 expected_dir = os.path.dirname(full_path)  
45                 #然後找到需要執行腳本所在的目錄。。。。。也就是說正常情況  假設expected_dir="e://a/b"  那麼 mod_file =realpathfullpath_noext="e://a/b/scripy" 
46                 #scripy是一個py文件---上面這個if是說的 正常情況。。。我想不到導入模塊和導入模塊的路徑不相等的情況--不過這個不重要-源碼這樣肯定是有道理的
47                   msg = ("%r module incorrectly imported from %r. Expected "
48                        "%r. Is this module globally installed?")
49                 raise ImportError(
50                     msg % (mod_name, module_dir, expected_dir))
51             return self.loadTestsFromModule(module, pattern=pattern), False   
52             #然後走 從模塊從加載測試s 這個方法---也就是說discover實際上是調用loadTestsFromMould這個方法的。。測試套件也是在這一步處理的
53     elif os.path.isdir(full_path):  #dicover裏面傳腳本目錄是走這裏。。
54         if (not namespace and    #namespace-默認是false   not namespace就是true  
55             not os.path.isfile(os.path.join(full_path, '__init__.py'))): #不是一個包。。-也就是說我們傳的目錄應該是一個包,下面包含__init__.py
56             return None, False
57         
58         load_tests = None  #這個load_tests是啥意思呢??????????後面繼續看----看了一遍--並且用unittest.main()試了一下-模塊下面是沒有這個屬性的。。只是unittest的初始化文件有這個方法--他也是通過discover找的。。
59         tests = None
60         name = self._get_name_from_path(full_path)   #這裏就是走子目錄的邏輯了
61         #get_name_from_path的邏輯在這裏就很清晰了 
62             #A.如果通過_find_test 調用self._get_name_from_path  是為了判斷兩次start_dir是否一致一致返回.  不一致返回從上次的start_dir1找到本次start_dir12的相對路徑--
63             #這裏又分兩種情況-A1正常情況-start_dir1是start_dir12的上級目錄。。。那麼返回的那麼就是-A.B這樣的了。。因為第一次的A是已經os.path.insert到環境變量了..所以A.B是可以直接用
64                                 #A2不正常情況 就是之前說的 最少返回一個點的...A這種返回---然後問題來了--他為什麼要這麼處理呢--原因是?????????
65                                 #我猜是與腳本同級存在另一個腳本目錄。。。後面驗證這一點。-----這裡在上面補充了--是因為子目錄中還存在腳本所有這麼走邏輯-完美的
66         try:
67             package = self._get_module_from_name(name)   #上面是導入的一個module--這裡是導入一個包-- 返回--
68         except case.SkipTest as e:
69             return _make_skipped_test(name, e, self.suiteClass), False
70         except:
71             error_case, error_message = \
72                 _make_failed_import_test(name, self.suiteClass)
73             self.errors.append(error_message)
74             return error_case, False
75         else:
76             load_tests = getattr(package, 'load_tests', None)  #然後判斷這個包裏面有沒有'load_tests'這個屬性---這裏我代碼一直看下來,我們是不知道這個lood_tests是什麼的,字面意思 加載測試集合
77             # Mark this package as being in load_tests (possibly ;))
78             self._loading_packages.add(name)  #然後把模塊名稱添加到set集合
79             try:
80                 tests = self.loadTestsFromModule(package, pattern=pattern)   
81                 #這裏傳了一個package模塊對象,和文件匹配規則合作或者文件。。--但是這裏導入一個包之後實際上是找不到testCase的-因為包下面的屬性肯定不是一個類-不會走loadTestsFromTestCase
82                 #所以這裏返回的tests是一個空列表
83                 if load_tests is not None: #貌似這個是棄用的,向後兼容-暫時沒看明白這個load_tests代表的意思
84                     # loadTestsFromModule(package) has loaded tests for us.
85                     return tests, False
86                 return tests, True  #  如果能走到這裏------就返回True_就是給_find_test判斷走遞歸的--_find_tests裏面就得到 should_recurse=True
87             finally:
88                 self._loading_packages.discard(name)  #然後這個刪掉set集合裏面之前導入的那個包
89         else:
90         return None, False

View Code

 1 def loadTestsFromModule(self, module, *args, pattern=None, **kws):
 2     """Return a suite of all test cases contained in the given module"""
 3     # This method used to take an undocumented and unofficial
 4     # use_load_tests argument.  For backward compatibility, we still
 5     # accept the argument (which can also be the first position) but we
 6     # ignore it and issue a deprecation warning if it's present.
 7     if len(args) > 0 or 'use_load_tests' in kws:    #args這個默認是一個空元組  長度默認為0  kws是一個空字典
 8         warnings.warn('use_load_tests is deprecated and ignored',
 9                       DeprecationWarning)
10         kws.pop('use_load_tests', None)
11     if len(args) > 1:
12         # Complain about the number of arguments, but don't forget the
13         # required `module` argument.
14         complaint = len(args) + 1
15         raise TypeError('loadTestsFromModule() takes 1 positional argument but {} were given'.format(complaint))
16     if len(kws) != 0:
17         # Since the keyword arguments are unsorted (see PEP 468), just
18         # pick the alphabetically sorted first argument to complain about,
19         # if multiple were given.  At least the error message will be
20         # predictable.
21         complaint = sorted(kws)[0]  #取出第一個Key-
22         raise TypeError("loadTestsFromModule() got an unexpected keyword argument '{}'".format(complaint))
23     tests = []
24     for name in dir(module):    #這裏得modul實際上使我們傳入的模塊對象---dir(object)  返回模塊下的所有屬性列表-
25         obj = getattr(module, name)   #然後反射返回name對象。。返回的是一個class對象
26         if isinstance(obj, type) and issubclass(obj, case.TestCase):    #這裏判斷obj是否是一個類--並且這個類是case.TestCase的子類,也就是說 是否寫在我們繼承unitest.testCase那個類的下面
27             tests.append(self.loadTestsFromTestCase(obj))  #可以看到最後走loadTestFromTestCase obj這裡是傳入的一個類名
28 
29     load_tests = getattr(module, 'load_tests', None)
30     tests = self.suiteClass(tests)
31     if load_tests is not None:
32         try:
33             return load_tests(self, tests, pattern)
34         except Exception as e:
35             error_case, error_message = _make_failed_load_tests(
36                 module.__name__, e, self.suiteClass)
37             self.errors.append(error_message)
38             return error_case
39     return tests  #返回集合

View Code loadTestsFromTestCase:這裏就是添加testcase到suit集合裏面的主要邏輯

 1 def loadTestsFromTestCase(self, testCaseClass):  #testCaseClass是我們傳的一個用例類
 2     """Return a suite of all test cases contained in testCaseClass"""
 3     if issubclass(testCaseClass, suite.TestSuite):  #這個類是不是suite.TestSuite的子類--如果是的就拋出異常==
 4         raise TypeError("Test cases should not be derived from "
 5                         "TestSuite. Maybe you meant to derive from "
 6                         "TestCase?")
 7     testCaseNames = self.getTestCaseNames(testCaseClass)  #getTestCaseNames  從類下面找到用例名稱--找到名稱返回的是一個列表
 8     if not testCaseNames and hasattr(testCaseClass, 'runTest'):  #這裏判斷testCaseNames是否為空-並且 是否存在"runTest"這個元素
 9         testCaseNames = ['runTest']
10     loaded_suite = self.suiteClass(map(testCaseClass, testCaseNames)) #是我的一個用例類--然後將testCaseNames類下面的測試方法--帶進去遍歷。。。高級用法---第一次見--這個方法很關鍵
11     return loaded_suite

View Code getTestCaseNames

 1 def getTestCaseNames(self, testCaseClass):
 2     """Return a sorted sequence of method names found within testCaseClass
 3     """
 4     def isTestMethod(attrname, testCaseClass=testCaseClass,   #定義一個內部方法
 5                      prefix=self.testMethodPrefix):     #self.testMethodPrefix="test"   這個在TestLoader下第一行就已經默認了,他是我們用例開頭的固定格式
 6         return attrname.startswith(prefix) and \  #這裏判斷了是否已test開頭以及 方法對象是否可用----getattr返回方法對象--callable()是檢測對象是否可用    返回一個布爾值
 7             callable(getattr(testCaseClass, attrname))
 8     testFnNames = list(filter(isTestMethod, dir(testCaseClass)))
 9      #dir(testCaseClass)返回該對象下面所有的屬性--包括變量test_1--所以上面需要檢測屬性時test開頭且是一個可以調用的對象--
10      #filter函數---前面是一個function -後面是一個可迭代對象-會遍歷可迭代對象-並傳入function-functions返回true則添加到列表--這樣就找到了所有的
11     if self.sortTestMethodsUsing:
12         testFnNames.sort(key=functools.cmp_to_key(self.sortTestMethodsUsing))     
13         #sortTestMethodsUsing = staticmethod(util.three_way_cmp) 轉為為靜態方法-內存地址指向self.sortTestMethodsUsing
14         #然後通過functools這個模塊排序==
15     return testFnNames  #然後返回用例方法名稱列表

View Code 所有的邏輯 就是在TestLoader找測試用例的時候–通過_find_tests這個方法從目錄開始找文件(子目錄)-模塊-類-方法名 然後將某個模塊下的類通過他下面的方法map返回多個對象,也就是說一個testClass下面存在五個test_method,他就會返回五個實例對象-並生成一個suite集合–然後加入到一個列表 如果一個模塊下有多個testClasee 同樣-實際上是一樣的–實際上他是先通過loadTestsFromModule這個方法找到所有的類對象之後在遍歷–然後才走上面那一步的,,,多個testClasss就存在多個suite集合– 也就是說 一個modul下面的 suite集合會添加到一個列表–[suite=[A-TestCase實例化對象1,A-TestCase實例化對象2],suite=[B-TestCase實例化對象1,B-TestCase實例化對象2]]—然後在將這個列表當做參數傳入TestSuite實例化一個新對象[suite=-[suite=[A-TestCase實例化對象1,A-TestCase實例化對象2],suite=[B-TestCase實例化對象1,B-TestCase實例化對象2]]]—-這樣就是一個模塊下用的結構 但是還沒有完–這裏只是一個modul下的—還有多個modul–到了大家估計也知道剩下的會幹什麼了— 沒錯–當我得到modul的全部suite集合之後—這個集合最終會返回給_find_tests方法–通過生成器返回給discover–也就是將這個suite集合又加入到了一個新的列表–然後discover又將這個list 帶入形成了一個—–最終的實例對象,最終返回的格式如下——- [suite= [suite1=-[suite=[A-TestCase實例化對象1,A-TestCase實例化對象2],suite=[B-TestCase實例化對象1,B-TestCase實例化對象2]]], [suite2=-[suite=[A-TestCase實例化對象1,A-TestCase實例化對象2],suite=[B-TestCase實例化對象1,B-TestCase實例化對象2]]] ]   –看過源碼的都知道—我們run的時候—就這個實例對象是可以接受參數的–而這個參數就是result—因為TestSuite繼承的BaseTestsSuite 有一個__call__這個
魔術方法:如果在類中實現了 __call__ 方法,那麼實例對象也將成為一個可調用對象,具體百度。這裏不做過多解釋—–所以最終的suite是可以接受參數的test(result)–接受參數之後直接走——call下面的邏輯了 本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

FB行銷專家,教你從零開始的技巧

Optional 容器類

什麼是Optional容器類

Optional 類(java.util.Optional) 是一個容器類,代表一個值存在或不存在,原來用 null 表示一個值不存在,現在 Optional 可以更好的表達這個概念。並且可以避免空指針異常

Optional類常用方法:

Optional.of(T t) : 創建一個 Optional 實例。

Optional.empty() : 創建一個空的 Optional 實例。

Optional.ofNullable(T t):若 t 不為 null,創建 Optional 實例,否則創建空實例。

isPresent() : 判斷是否包含值。

orElse(T t) : 如果調用對象包含值,返回該值,否則返回t。

orElseGet(Supplier s) :如果調用對象包含值,返回該值,否則返回 s 獲取的值。

orElseThrow(Supplier es) : 當遇到一個不存在的值的時候,並不返回一個默認值,而是拋出異常。

map(Function f): 如果有值對其處理,並返回處理后的Optional,否則返回 Optional.empty()。

flatMap(Function mapper):與 map 類似,要求返回值必須是Optional。

filter(Predicate p):接收一個函數式接口,當符合接口時,則返回一個Optional對象,否則返回一個空的Optional對象。

示例:

 1 import org.junit.Test;
 2 import java.util.Optional;
 3 /*
 4  * Optional 容器類:用於盡量避免空指針異常
 5  *     Optional.of(T t) : 創建一個 Optional 實例
 6  *     Optional.empty() : 創建一個空的 Optional 實例
 7  *     Optional.ofNullable(T t):若 t 不為 null,創建 Optional 實例,否則創建空實例
 8  *     isPresent() : 判斷是否包含值
 9  *     ifPresent(Consumer<? super T> consumer) 判斷是否包含值,再執行 consumer
10  *     orElse(T t) :  如果調用對象包含值,返回該值,否則返回t
11  *     orElseGet(Supplier s) :如果調用對象包含值,返回該值,否則返回 s 獲取的值
12  *     orElseThrow(Supplier<? extends X> exceptionSupplier) : 當遇到一個不存在的值的時候,並不返回一個默認值,而是拋出異常
13  *     map(Function f): 如果有值對其處理,並返回處理后的Optional,否則返回 Optional.empty()
14  *     flatMap(Function mapper):與 map 類似,要求返回值必須是Optional
15  *     filter(Predicate<? super T> predicate):接收一個函數式接口,當符合接口時,則返回一個Optional對象,否則返回一個空的Optional對象
16  *
17  *     map、flatMap 和 filter 的使用方法和 StreamAPI 中的一樣
18  */
19 public class TestOptional {
20 
21 
22     /**
23      * 創建 Optional 實例
24      */
25     @Test
26     public void test1(){
27 
28         // Optional.empty() : 創建一個空的 Optional 實例
29         Optional<String> empty = Optional.empty();
30         System.out.println(empty);// 輸出結果:Optional.empty
31         //System.out.println(empty.get());// 報錯:java.util.NoSuchElementException: No value present
32 
33         //    Optional.of(T t) : 創建一個 Optional 實例
34         Optional<Employee> eop = Optional.of(new Employee());
35         System.out.println(eop);// 輸出結果:Optional[Employee{name='null', age=null, gender=null, salary=null, status=null}]
36         System.out.println(eop.get());//輸出結果:Employee{name='null', age=null, gender=null, salary=null, status=null}
37 
38         //注意:Optional.of(T t) 中,傳遞給of()的值不可以為空,否則會拋出空指針異常
39         //Optional<Employee> eop1 = Optional.of(null);//這一行直接報錯:java.lang.NullPointerException
40 
41         //Optional.ofNullable(T t):若 t 不為 null,創建 Optional 實例,否則創建空實例
42         //所以,在創建Optional對象時,如果傳入的參數不確定是否會為Null時,就可以使用 Optional.ofNullable(T t) 方式創建實例。
43         Optional<Object> op = Optional.ofNullable(null);//這樣的效果和 Optional.empty() 一樣
44         System.out.println(op);//Optional.empty
45         op = Optional.ofNullable(new Employee());//
46         System.out.println(op);//Optional[Employee{name='null', age=null, gender=null, salary=null, status=null}]
47     }
48 
49 
50     /**
51      * isPresent() : 判斷是否包含值
52      * ifPresent(Consumer<? super T> consumer) 判斷是否包含值,再執行 consumer
53      */
54     @Test
55     public void test2(){
56         Optional<Employee> opt = Optional.of(new Employee());
57         System.out.println(opt.isPresent());//輸出結果:true
58         opt = Optional.ofNullable(null);
59         System.out.println(opt.isPresent());//輸出結果:false
60         opt.ifPresent(employee -> System.out.println(employee));// 如果 opt.isPresent() 為false ,這裏就不輸出,否則就輸出 employee
61     }
62 
63     /**
64      *     orElse(T t) :  如果調用對象包含值,返回該值,否則返回t
65      *     orElseGet(Supplier s) :如果調用對象包含值,返回該值,否則返回 s 獲取的值
66      */
67     @Test
68     public void test3(){
69         //Optional<Employee> opt = Optional.of(new Employee());
70         Optional<Employee> opt = Optional.ofNullable(null);
71         /*Employee emp = opt.orElse(new Employee("張三",20));
72         System.out.println(emp);*/
73 
74         int condition = 2;//模擬條件
75         Employee emp = opt.orElseGet(()-> {
76             if (condition == 1){
77                 return new Employee("李四");
78             }else if (condition == 2){
79                 return new Employee("王二麻子");
80             }else {
81                 return new Employee("趙六");
82             }
83         });
84         System.out.println(emp);
85     }
86     /**
87      *     orElseThrow(Supplier<? extends X> exceptionSupplier) : 當遇到一個不存在的值的時候,並不返回一個默認值,而是拋出異常
88      */
89     @Test
90     public void test4(){
91         Object obj = Optional.ofNullable(null).orElseThrow(IllegalArgumentException::new);//當參數為null,則拋出一個不合法的參數異常
92         System.out.println(obj);
93     }
94 
95 
96 }

備註:map、flatMap 和 filter 的使用方法和 StreamAPI 中的一樣,如需了解詳細使用方法,請參考:Stream API 詳解

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

網頁設計最專業,超強功能平台可客製化

IDEA自定義類註釋和方法註釋(自定義groovyScript方法實現多行參數註釋)

一、類註釋

1、打開設置面板:file -> setting -> Editor -> file and code Templates

選擇其中的inclues選項卡,並選擇File header,如圖。不要選擇Files選項卡再設置Class,這樣比較麻煩,而且這樣設置以後沒新建一個類都要自己寫一次Date。

2、在右邊編輯面板插入自己想要的註釋即可。其中${}是變量,需要在變量基本都在編輯款下面的Description,往下拉即可看到。
/*
* @Classname ${NAME}
*
* @Date ${DATE}
*
* @userName
*/
3、新建一個類,看是否自動加了註釋

 

二、方法註釋

1、打開設置面板:file -> setting -> Editor -> Live Templates

 

 2、新建一個Template Group…,命名隨意,假設為bokeyuan,然後選擇該組,點擊新建一個模板Live Template

 

3、名稱建議設為*,文本框輸入自己想要設置的註釋格式,右下角要選擇enter(原本是tab)。

 

 4、留意註釋格式,其中參數要直接寫變量$param$,開頭只有一個*號。寫好之後點擊上圖框中的edit variables

 

其中返回值return使用系統自帶的,下拉可以找到methodReturnType()

 

 

 5、自定義多行參數註釋

IDEA自帶的參數函數methodParameters()產出的註釋格式是這樣的:

/**
      * 
      * @param [a,b,c]
      * @return void
      * @throws 
      */

我們可能需要的是多行參數註釋:

/**
      * 
      * @param a
      * @param b
      * @param c
      * @return void
      * @throws 
      */

這個時候就要使用裏面的groovyScript()函數來自定義格式:

groovyScript("def result=''; def params=\"${_1}\".replaceAll('[\\\\[|\\\\]|\\\\s]', '').split(',').toList(); for(i = 0; i < params.size(); i++) {if(i == 0) result += '* @param ' + params[i] + ' ' + ((i < params.size() - 1) ? '\\n' : '');else result+='  * @param ' + params[i] + ' ' + ((i < params.size() - 1) ? '\\n' : '')}; return result", methodParameters())

直接複製在Expression裏面即可。

 

6、選擇語言,點擊Define勾選Java

 

 

有其他問題可以評論問我哦

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

萬級TPS億級流水-中台賬戶系統架構設計

萬級TPS億級流水-中台賬戶系統架構設計

標籤:高併發 萬級TPS 億級流水 賬戶系統

  • 背景
  • 業務模型
  • 應用層設計
  • 數據層設計
  • 日切對賬

背景

我們需要給所有前台業務提供統一的賬戶系統,用來支撐所有前台產品線的用戶資產管理,統一提供支持大併發萬級TPS、億級流水、數據強一致、風控安全、日切對賬、財務核算、審計等能力,在萬級TPS下保證絕對的數據準確性和數據溯源能力。

注:資金類系統只有合格和不合格,哪怕數據出現只有0.01分的差錯也是不合格的,局部數據不準也就意味着全局數據都不可信。

本文只分享系統的核心模型部分的設計,其他常規類的(如壓測驗收、系統保護策略-限流、降級、熔斷等)設計就不做多介紹,如果對其他方面有興趣歡迎進一步交流。

業務模型

基本賬戶管理: 根據交易的不同主體,可以分為個人賬戶機構賬戶
賬戶餘額在使用上沒有任何限制,很純粹的賬戶存儲、轉賬管理,可以滿足90%業務場景。

子賬戶功能: 一個用戶可以開通多個子賬戶,根據餘額屬性不同可以分為基本賬戶、過期賬戶,根據幣種不同可以分為人民幣賬戶、虛擬幣賬戶,根據業務形態不同可以自定義。
(不同賬戶的特定功能是通過賬戶上的賬戶屬性來區分實現。)

過期賬戶管理: 該賬戶中的餘額是會隨着進賬流水到期自動過期。
如:在某平台充值1000元送300元,其中300元是有過期時間的,但是1000元是沒有時間限制的。這裏的1000元存在你的基本賬戶中,300元存在你的過期賬戶中。

注:過期賬戶的每一筆入賬流水都會有一個到期時間。系統根據交易流水的到期時間,自動核銷用戶過期賬戶中的餘額,記為平台的確認收入。

賬戶組合使用:支持多賬戶組合使用,根據配置的優先扣減順序進行扣減餘額。比如:在 基本賬戶過期賬戶 (充值賬戶)中扣錢一般的順序是優先扣減過期賬戶的餘額。

應用層設計

根據上述業務模型,賬戶系統是一個典型的 數據密集型系統 ,業務層的邏輯不複雜。整個系統的設計關鍵點在於如何平衡大併發TPS和數據一致性。

熱點賬戶:前台直播類業務存在熱點賬戶問題,每到各種活動賽事的時候會存在 90%DAU 給少數幾個頭部主播打賞的場景。
DB就會有熱點行問題,由於 行鎖 關係併發一大肯定大量超時、RT突增DB活躍線程 增加等一系列問題,最終DB會被拖掛。

賬戶類系統有一個特點,原賬戶的扣減可以實時處理,目標賬戶可以異步處理,我們可以將轉賬動作拆解為兩個階段進行異步化。(可以參考銀行轉賬業務。)

比如:A給B轉賬100元,原賬戶A的100元餘額扣減可以同步處理,B賬戶的100增加可以異步處理。這樣哪怕10w人給主播打賞,這10w人的賬戶都是分散的,而主播的餘額增加則是異步處理的。

賬戶轉賬扣減A賬戶餘額,記錄A賬戶出賬流水,記錄B賬戶入賬流水,這三個動作可以在一個DBTransaction中處理,可以保證源賬戶進出帳一致性。目標賬戶B的入賬可以異步處理,為了保證萬無一失且滿足一定的實時性,需要兩步結合,程序里通過MQ走異步入賬,同時增加DB的兜底JOB定時掃描 入賬流水記錄未到賬的流水進行入賬。

我們通過異步化緩解熱點行處理,但是如果 收款方 強烈要求收款必須在一定的時間內完成,我們還是需要進一步處理,後面會講到。

過期賬戶: 通常過期賬戶用來管理贈送類賬戶,這類賬戶有一定的時效性,用戶在使用上也是優先扣減此類賬戶餘額。
這類使用需求其實覆蓋面不大,真正用戶賬戶餘額不使用等着被系統過期的很少,畢竟這是一個很傻的行為。

過期賬戶的兩種核銷情況:第一種是用戶使用過期賬戶時的核銷。第二種是某個過期流水到了過期時間,系統自動核銷記為平台的確認收入。

過期賬戶核銷邏輯:用戶充值1000元到基本賬戶,平台贈送300元到贈送賬戶。此時,基本賬戶記錄進賬流水+1000元,贈送賬戶記錄進賬流水+300元並且該筆流水的過期時間2020-12-29 23:59:59 (過期時間由前台業務方設置) 。

系統自動核銷:如果用戶不在此時間之前用完就會被系統自動划進平台的收入,贈送賬戶餘額扣減-300元。

用戶使用核銷:如果用戶在過期時間前陸續在使用贈送賬戶,比如使用100元,那麼我們需要核銷原本進賬的300元的那筆流水,減少-150元。
也就是說,該筆過期流水已經核銷掉150元,帶過期核銷150元,到期后只要核銷150元即可,而不是300元。

過期賬戶每次使用均產生待核銷負向流水,系統自動核銷前必須保證沒有任何負向流水記錄才可以去扣減贈送賬戶餘額。

考慮到極端情況下,剛好過期JOB在進行自動過期核銷,用戶又在此時使用過期賬戶,這點需要注意下。可以簡單通過加DB-X鎖解決,這個場景其實非常稀少。

數據層設計

在應用層設計的時候,我們通過異步化方式來繞開熱點問題。
同樣我們在設計數據層的時候也要考慮單次操作DB的性能,比如控制事務的大小,事務跨網絡的次數等問題。當然還包括金額存儲的精度問題,精度問題處理不好也會影響性能。

浮點數問題: 如果我們用浮點數近似值來存儲金額,那麼就一定會有偏差,隨着金額越大時間越長偏差就會越大。比較好的方式是通過整型來存儲,通過放大金額比例來達到不同的業務場景下對金額比率的要求。

正常的1.12元,存儲比率是1=100元,那麼表裡的存儲值就是112,不同的貨幣比例都可以自由縮放,永遠都可以保持最準確的精度。

分庫分表+讀寫分離: 根據業務特點和未來增量規劃,將DB分為16個邏輯庫,前期使用2個物理庫承載。16個邏輯庫,按照每次2倍擴容,最大擴容上限是16個物理庫。單實例的配置 8c 32g 2t 8000conn 9000iops

按照單次TPS-rt 1ms計算,TPS 1w 需求,每台承載5k TPS,單庫的活躍線程大概在8-10個(考慮網絡延遲)。
最後到達瓶頸的都是iops,因為只要rt足夠短,最終壓力都會在IO上。

分庫按照uid分為16個庫,賬戶表不分表默認16張。每張表按照 1kw*16=1.6 億個賬戶。

單表能存儲多少要綜合考慮,比如查詢類型,單次查詢的RT,冷熱數據佔比( innodb_buffer_pool 利用率)、是否充分發揮了索引,索引是否達到3星級別,索引片中沒有經常變更的字段等。

賬戶流水表按照日期分表365張,流水數據會隨着時間推移逐漸變成冷數據,定期歸檔冷數據。(這裏約定了,流水查詢只能按照uid+日期查詢。如果運營類的需求,要橫跨分片key獲取,走OLAP方案 clickhouse、hive等)

分庫分表採用阿里雲分佈式數據庫產品DRDS,1個主庫集群+2個讀庫集群(讀庫做了讀負載均衡,可以按需擴容)。

讀負載均衡器:https://github.com/Plen-wang/read-loadbalance

既然用了DRDS分佈式數據庫產品,那麼在查詢上需要充分考慮分片鍵的限制,如果存儲和查詢出現分片鍵衝突問題就需要我們手動計算分片路由,直接訪問物理節點。

訪問物理節點需要藉助DRDS專用SQL註釋子句來完成。

先通過 show node 查看物理DB ID、show topology from logic_table_name 查看物理表ID,然後在SQL帶上特定的註釋子句

SELECT /*+TDDL:scan('logic_table_name', real_table=("real_table_name"),node='real_db_node_id')*/ 
count(1) FROM logic_table_name ;

賬戶更新: 對賬戶更新都有一個前提就是賬戶已經開通,但是我們為了最大化賬戶系統在使用上的便利性,讓前台業務方不需要做初始化動作,由賬戶系統惰性初始化,比如發現賬戶不存在就自動初始化賬戶數據。

但是我們怎麼知道賬戶不存在,不可能每次都去查詢一次或者根據執行返回錯誤判斷。而且 update 語句是區分不了錯誤的 賬戶不存在 還是 餘額不足 或者其他原因。

那麼如何巧妙的解決這個問題,只要一次DB往返。

我們可以使用 Mysql INSERT INTO ... ON DUPLICATE KEY UPDATE ... 子句,但是該子句有一個限制就是不支持 where 子句。

-- cut_version 樂觀鎖、account_property 賬戶屬性
insert into tb_account(uid,balance,cut_version,account_property) values("%s",%d,%d,%d) ON DUPLICATE KEY UPDATE balance = balance + %d,cut_version = cut_version+1

其實不完全推薦使用這個方法,因為這個方法也有弊端就是將來 where 子句無法使用,還有一個辦法就是合併 賬戶查詢插入 為一條 sql 提交。

DB操作本身rt可能很短,但是如果跨網絡那麼事務的延遲會帶來DB的串行化增加,降低併發度,整體應用 rt就會增加。所以一個原則就是盡量不要跨網絡開事務,合併sql做一次事務提交,最短的事務周期,減少跨網絡的事務操作,如果我們將單次事務網絡交互減少2-3次,性能的提高可能會增加2-3倍,同樣由於網絡的不穩定抖動丟包對 999rt 線的影響也會減少2-3倍。

平衡好當前系統是業務密集型還是數據密集型
判斷當前系統是否有很強的業務層邏輯,是否要運用DDDRUP等強模型的工程方法。畢竟強模型高性能在落地的時候有些方面是衝突的,需要進一步藉助 CRQSGRASP等工程方法來解決。

單行熱點問題: 單行的TPS都是串行的,事務rt越短TPS就越高,按照1ms計算,差不多TPS就是1000。一般只有機構賬戶類型才會有這個需求。

我們可以將單行變成多行,增加行的并行度,加大賬戶操作的併發度。(這個方案要評估好寫入和查詢兩端需求)

id uid balance slot
1 10101010 1000 1
2 10101010 2000 2
3 10101010 3000 3
4 10101010 400 4
5 10101010 300 5
6 10101010 200 6
7 10101010 200 7
8 10101010 200 8
9 10101010 200 9
10 10101010 200 10
insert into tb_account (uid,balance,slot)
values(10101010, 1000, round(rand()*9)+1) 
on  duplicate key update balance=balance+values(balance)

這裏的 10slot*單個slot 1000TPS,理論上可以跑到1w,如果機構賬戶數據量很大,可以擴展slot個數。

賬戶的總餘額通過sum()匯總,如果業務場景中有餘額的頻繁sum()操作,可以通過增加餘額中間表,定期 insert into tb_account_total select sum(balance) total_balance from tb_account group by uid

通常機構賬戶的結算是有周期的(T+7、T+30等),而且基本是沒有併發,所以在賬戶餘額扣減方面就可以輕鬆處理。
有兩種實現方案:

第一種,賬戶餘額允許單個slot為負數,但是總的sum()是正數。通過子查詢來對餘額進行檢查。

insert into tb_account (uid, balance, slot)
select uid,-1000 as balance,round(rand() *9+ 1)
from(
    select uid, sum(balance) as ss
    from tb_account
    where uid= 10101010
    group by uid having ss>= 1000 for update) as tmp
on duplicate key update balance= balance+ values(balance)

第二種,如果條件允許可以藉助用戶自定義變量來在DB上完成餘額累計掃描,將可以扣減的slot的主鍵id返回給程序,但是只需要一次DB交互就可以獲取出可以扣減的賬戶solt,然後分別開始對slot賬戶進行扣減。

set @f:=0;
select * from tb_account where id in(select id from (select id, @f:=@f+balance from tb_account where @f<1000 order by id) as t);

第二種方案在默認的mysql數據庫上都是支持的,但是有些數據庫雲產品不支持,阿里雲rds是不支持的。

日切對賬

賬戶系統有一個基本的需求,就是每天餘額鏡像,簡單講就是餘額在每天的快照,用來做T+1對賬。
不管財務還是每季度的審計都會需要,最重要的是我們自己也需要對賬戶數據做摸底對賬。

由於每天產生上億的流水,這需要在大數據平台中完成。

日切對賬:昨天賬戶餘額前天賬戶餘額 = 昨天的流水前天的流水

比如,昨天的賬戶餘額是5000w,前台的賬戶餘額是4500w,差值就是500w。同樣道理,昨天的賬戶流水是5000w,前天的賬戶流水是4500w,那麼差值是500w,這就是沒問題的。

賬戶不僅有增加也有減少,可能昨天賬戶餘額比前天賬戶餘額差值是-500w,但是流水也要是-500w才行。

由於每天會產生億級的流水,用傳統的全量抽取不現實,這類數據抽取的速度都會有延遲,而且對賬最重要的是時間點必須非常精準,才能保證餘額和流水是對得上的。

要不然會出現HDFS的分區是2020-06-10號,但是該分區里有2020-06-11的數據,就是因為拉取的時候會延遲到第二天。這個問題也可以通過增加拉取sql的條件限制來解決這個問題,但是無法做到0點瞬間鏡像全部賬戶。

解決方案: 全量餘額+binlog增量更新
1.賬戶表,先做一次全量同步。
2.DB的所有變更通過binlog(默認row複製)進到數倉。(因為 binlog 是基於發生時間的,所以無所謂我們是不是在0點去計算鏡像)
3.T+1跑JOB的時候,獲取前一天的賬戶餘額,然後通過 binlog 來覆蓋前天與昨天的交集部分。

由於數倉的 binlog 數據都是增量的,所以要想取到正確的全量數據需要用到一定的技巧。

select app_id,sub_type,sum(amount) records_amount from (
      select *,row_number()over(partition by id order by updated_at) as rn
      from hive_db_table
      where dt='${YESTERDAY}'
  ) t where t.rn=1
       group by t.sub_type,t.app_id

使用 hive 開窗函數 row_number()over() 對同樣的id進行分組,然後獲取最新的一條數據就是賬戶在T的最後的值。

作者:王清培(趣頭條 Tech Leader)

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

FB行銷專家,教你從零開始的技巧

地球發燒了研究:2080年前熱死人數激增20倍

摘錄自2018年8月1日蘋果日報台北報導

天氣讓人熱得受不了,而這種情況若不加以控制,地球上熱死人的數目將增加20倍。最新研究指出,在各國政府完全不做任何事的最糟情況,部分國家在2080年前,因為高溫導致的死亡數字將增加2000%。

這項刊登在「公共科學圖書館醫學期刊」(PLOS Medicine)的報告指出,歐洲、部分亞洲與北美洲地區的氣溫逐年升高,熱浪造成數以千計的死亡案例。該報告首席研究員、澳洲蒙納士大學(Monash University)副教授郭玉明(Yuming Guo,音譯)說:「未來的熱浪會發生得更頻繁、更強烈,維持時間也更長」、「如果我們找不到減緩氣候變遷的方法,並幫助人民適應熱浪氣候,和熱浪有關的死亡案例將會急遽增加。」

報告中指出,最糟的情況下,哥倫比亞在2031至2080年間被高溫熱死的人數,會比1971年至2010年增加2000%,即增加20倍;菲律賓在2031到2080因熱浪而額外死亡的人數,是1971到2020的12倍;澳洲、美國是5倍,英國是4倍。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?