Go 在新零售營銷領域的實踐
從事過服務端開發、移動開發、大資料開發、機器學習、專案管理等工作。
前言
下午好,很榮幸今天來到這裡為大家分享。我分享的題目是《Go在新零售營銷領域的實踐》。與Go有聯絡,是因為我們有用Go實現了一套優惠券系統。最近有給客戶做私有化部署,客戶有2000萬用戶。以下是我認為比較重要的幾個點,做下分享:
一、券碼設計
二、分庫分表:怎麼定sharding key
三、分散式事務:使用者間轉贈
四、效能測試
說下背景。"新"零售,技術上理解就是用線上運營顧客的方式運營線下顧客。
首先要做的是線下的數字化,與線上資料打通。現在大家比較熟悉的超市、便利店讓你辦會員卡,微信掃一掃就可以了,就是一種線下資料數字化的手段。
有了使用者資料,就能刻畫出使用者畫像,做使用者分層,與線上那套是一樣的。營銷的傳播方式可能多樣,但是送的東西是不變的,比如給沉睡客戶送一張券,期望啟用它。
回到主題,優惠券系統簡單來說一般有這幾個場景:
1、商家向用戶發一張券
2、匯出券碼,異業交換
3、H5活動,使用者主動領一張券
4、APP裡使用者向用戶轉贈一張券
優惠券系統怎麼設計的?下面是非常簡易的架構,"小米+步槍",最底層是SQL/">MySQL(包含中介軟體)、redis,往上是對比如資料庫層比較細粒度封裝,UseCase就是場景,最後暴露出來的是API。
券碼設計
券碼的設計是這麼定的,產品說券碼不可重複,這是當然的,重複發出去就是一個BUG。券碼也是不可預測的,比如有001、002、003,那從第三張就開始薅羊毛了,因為使用者有了券碼生成的邏輯。B端商戶的服務員除了掃碼核銷還有手工核銷的場景,因此券碼不能太長,然後是要純數字。有了這些前提條件,以往的ID生成規則基本上都夠用了。比如UUID,雖然是非常的唯一,但是太長又不是純數字,MongoDB的生成方式也挺不錯的,但是MongoDB瞬時連續生成的ID是順序的,也就是可預測了。同理,Twitter Snowflake的生成方式也是一樣的問題。
這時候需要使用一些偽隨機數生成演算法。所謂的偽隨機數就是假的,是通過一些數學規則或數學工程式推演出來的,最多做一些高斯分佈、均勻分佈之類的變換,我找了一個最簡單隨機數生成演算法,就是線性同餘(Java的Random就是利用了這個演算法)。它的公式很簡單,是一個線性方程,Xn是一個種子,a、c、m都是引數,m是設定隨機數最得大上限,通過種子生成下一個,下一個作為種子生成下下一個隨機數。工程上非常簡單。附帶說下,如果直接使用語言的隨機數包,因為隨機數種子是固定或是沒指定的,每次生成的隨機數都是一樣的。
線性同餘還有這樣的特性,精心挑選的引數可保證在整個週期內(m)隨機數都是不重複的,需要滿足的條件就是m和c是互為質數,a-1能被m的所有質數整除,如果m能被4整除,a-1也能被4整除。精心挑選這些,假設m定的是100,最後就會出現完全不會重複的100個數字,生成超過100個,因為數字總共就一百,那就進入下一個週期。
前面是提的一個假設,我們需要做試驗驗證下。先講線性同餘,引數acm再加一個種子,next就是線性公式,生成的就是輸入m是模數,m是100就生成一百個數。這些都是圖表顯示,如果我們沒有精心挑選的acm會導致什麼樣的結果?就會有很明顯的週期性,比如m是3,這裡生成的就是99,你能看到的99都能出現。如果是用直方圖顯示,那不是所有的數字都是隨機產生的,會有很多是空白,也有很多是重複被生成,這樣一定是不合格的,如果精心挑選的a是21,c是3,然後m是100,3是初始值,看起來好像是有一點重複,但是其實並不是,你看這個直方圖就比較漂亮,最高是出現的次數就是x軸上面的一百,這個是出現的0-100有多少個數字,然後左邊就會出現的頻次,這樣的隨機就是最完美的。 (https://github.com/XUJiahua/gomeetup20181021/blob/master/code/lcg_randomness/lcg_randomness.ipynb)
試驗下來確實不錯,這樣就已經滿足了隨機(人看起來),確實是隨機並且不會重複,這樣生成一百個,那一百個都是不同的。可預測性其實還是有問題(下文分解),知道了公式,如果知道了x0,X1,X2,就可以解方程組了,反正是初中的水平了就可以搞定,這樣就可以推匯出來a、c、m。
前面講的是單程序的LCG,種子都維護在程序裡。從高可用與高效能的角度講,多程序LCG是必須的。種子儲存在資料庫,LCG每次去生成,都要去從資料庫裡讀種子出來,讀完種子再更新一下。這個是有寫鎖的,如果說生成一個又一個,每次都要讀資料庫,相當於每次都是從資料庫讀的資料,這樣對效能是非常差,所有的壓力都壓在資料庫上面了,因此我們要生成一批。生成一批除了提高併發,還有解決了可預測性的問題,因為我知道了X0,X1,X2,知道10個以上就可以解出方程組推匯出引數了,這個地方一定做shuffle,亂序之後就摸不準順序。
分庫分表
為什麼分庫分表,前面有提到使用者特別多的情況。我們私有化案例裡客戶有2000萬,券也會達到上10億級,百億級這麼多的情況。而優惠券表有兩個獨立的查詢場景,一是我要看我APP裡券包,要根據uid查出所有我的券。第二個是根據券碼查,服務員掃描券碼做核銷就可以了。這裡,因為sharding key只有一個,比較糾結。其實有一些非常成熟的方案,異構索引,原理上就是維護兩張內容一樣的表,一張為uid索引,一張為code索引,通過冗餘提高查詢效能;但是這個是有成本的,維護第二份資料伺服器的成本,為了保證兩份資料的一致性,在考慮真正要落實異構索引這個方案之前,就要引入很多的中介軟體。不妨考慮一下其他的輕量方案。下面是一個樸素的想法,也只是一個假設。
引入了一個sharding key,根據UID和券碼都能推匯出這個sharding key。這樣兩個索引都能用了,通過提取券碼中shardID就知道在哪個分片上;如果是uid查,就從uid上面生成shardId就行了。至於兩次HASH是否影響資料分片平衡性呢?做一次試驗吧。
這個是HASH取模,預設都是按UID做HASH,現在用shardid做HASH,需要說明的是,一次Hash就是根據UID做分片只有一次資料庫分的HASH。對資料庫分片就是HASH取模這麼簡單,如果是16進位制的,那就想象是非常大的整數,16進制度給它32取模,因為最多是32,所以最多取兩位就可以。這裡生成了UID,左邊的直方圖是對一次HASH的結果,第二次是兩次HASH的結果,這裡程式碼也都有顯示,大家後面可根據連結再去看一下。看起來效果是不錯的,然後我們就可以放到測試,或者是生產上,生產上線之前生產還是做測試的,就是試驗一下,最後結果還是不錯的,看出來其實沒有什麼區別,HASH的比較均勻。 (https://github.com/XUJiahua/gomeetup20181021/blob/master/code/hash_randomness/hash_randomness.ipynb)
分散式事務
現在比較常用的就是基於佇列的分散式事務。假如我要對A做刪除然後對B做新增,以前一個MySQL事務就可以搞定,但現在資料不一定是在同一個例項和機器,需要MQ中介軟體來輔助。
比如有一個轉贈的場景,A使用者要對B使用者做轉贈操作,他需要把A的券碼刪掉,然後再插入別的券碼。現在變成兩階段,第一階段先刪除A券碼,新增一個轉贈的訊息到佇列裡,這裡是一個本地事務操作。然後第二個是從佇列裡面取消訊息插入再清理訊息。
因為沒有裝備事務的訊息,如果訊息服務不是事務的,上面有一種情況,把這個新增轉贈訊息包在刪除事務裡,這裡轉贈可能成功了,但是刪除可能是失敗的。這裡的處理方式,是在第二階段做一次預檢,檢查一下A券碼是不是已經刪掉了。還有一個問題,就是第二階段的清理訊息,如果插入成功後清理失敗。這裡的處理方式,是對插入操作做冪等處理。總之,在沒有事務訊息中介軟體的情況下我們就做一些業務上的補償。
資料庫事務與業務邏輯怎麼寫?我的方法是所有跟資料庫有關的都包在DAL層,業務邏輯通過方法回撥的方式進入。
效能測試
對於效能測試,我之前也用過WRK,AB這種,WRK和AB這種只能處理一些簡單的場景,如一次HTTP GET之類的。但是優惠券這種場景,使用者領一張券、查詢一次、B端核銷一次,沒法方便得模擬。Locust介紹給大家,比起Jmeter,這是寫程式碼而不是配置的方式。如果自動化測試是用Python寫的,可以把效能測試和自動化測試放一起。因為是Python寫的,本身有GIL限制,一個程序只能利用一個CPU,所以有多少個CPU就起多少個Slave。也有Go的Slave實現,這樣Slave的數量可以少一些。效能測試解決兩個問題,一個是瓶頸調優,一個是預估生產配置。
生產上取樣,如果你有無數臺機器,可以找一臺機器每60秒取樣到本地,然後傳回自己電腦線上下分析,不用直接暴露端口出來。測試環境你可以直接pprof,然後後面跟一下地址。最新的pprof已經自帶了火焰圖,Y軸是呼叫棧,水平的寬度越寬,代表了時間佔用比較長一點。然後越寬說明越是一個瓶頸,然後通過不斷消除寬的長條來解決效能問題。
Best Practice
最後再講一些有關Go方面有趣的經驗。go generate真的很好用,我們公司會用它做API文件的生成。可以使用開源的swagger,也可以自己寫。基於程式碼生成文件,這樣文件更新和程式打包都是同步的,這樣程式碼只要是最新的文件也是最新的,不會有那種文件太老,然後要去查程式碼的情況。
舉個例子,我們之前列舉定義的比較失敗,型別是int。脫離程式碼久了,也不知道哪些是哪些。如果要定位一個問題,我要從1開始數,數錯了重新開始,後面該怎麼解決呢?通過反射,然後再加上go generate,直接生成文件,有什麼問題直接看下。
另外一個是proxy,是用struct embedding+interface。Go裡面沒有繼承,通過embedding的方式就“繼承“了原有的方法。如果原有的struct實現了A介面,如果做了Embedding,struct2也相當於實現了A的介面,這樣就可以選擇性的覆蓋一些方法。比如做快取或者是埋點,這樣可以隔離mysql, redis。每一層、每個方法都做單一的事情,這個也是設計模式裡提倡的東西。
以上就是我的分享,謝謝大家!