2016年1月份,我們的產(chǎn)品上線版已經(jīng)是1.1.0,開發(fā)版已經(jīng)做到1.2.0。這些版本之間除了UI上做了一些改動(可以在我的產(chǎn)品分類文章里看到我為什么會做這些改動),大部分是些底層的優(yōu)化,比如App端緩存架構(gòu)的修改,后臺Server的API優(yōu)化等等。第一個(gè)階段的產(chǎn)品規(guī)劃此時(shí)已經(jīng)進(jìn)入一個(gè)相對平緩的時(shí)期,這個(gè)平緩不是說產(chǎn)品做完了,而是說產(chǎn)品從無到有(0到1)之后,進(jìn)入2(大需求改動)之前一個(gè)節(jié)奏相對平衡的時(shí)期。在這個(gè)時(shí)候,我終于開始騰出手來重點(diǎn)關(guān)注一個(gè)一直懸而未決的需求——線上競價(jià)。
注:本文里競價(jià)和拍賣代表一個(gè)概念。這里的競價(jià)指的是傳統(tǒng)意義上的拍賣的線上版本,不是指像某些SP商提供的線上廣告位自動競價(jià)系統(tǒng)。兩者有一定區(qū)別。
線上競價(jià)這個(gè)概念,在有淘寶的今天,已經(jīng)不是什么新鮮事了。但是親手做一個(gè)完全模擬線下拍賣流程的在線競價(jià)系統(tǒng),這樣的需求不會很多。這對我們團(tuán)隊(duì)對于我個(gè)人來說,即是一個(gè)機(jī)會也是一個(gè)挑戰(zhàn)。
好,怎么做呢?首先看看能不能借用別人的輪子,能不重復(fù)造當(dāng)然不要自己造。可是在網(wǎng)上搜索一大圈,幾天下來,能用的資料寥寥無幾。可能是一個(gè)競價(jià)系統(tǒng)太簡單了,做的人都不屑于不好意思寫下來;又或者是做的人實(shí)在是太少了,都沒來得及寫;也可能是凡是做競價(jià)的都是商業(yè)需要,不能寫;還有可能我搜索技術(shù)太爛,找不到。不管怎樣,看起來我們又是得從零做起,而且我得寫點(diǎn)什么,好歹將來一無是處的時(shí)候給自己的子孫吹個(gè)NB說,“你爺爺曾經(jīng)做過競價(jià)系統(tǒng)!”“啥破玩意?…”“……”
從頭做,還是得分幾步:
1. 首先要熟悉拍賣本身,至少得先懂;沒有太多的時(shí)間讓我一點(diǎn)點(diǎn)的完全熟悉拍賣這個(gè)市場,于是我拉上運(yùn)營部門里做過真實(shí)拍賣交易的人,不斷聊天了解概念,然后又親自和他們參加了幾場真實(shí)的現(xiàn)場拍賣會,首先對真實(shí)線下情況混個(gè)八九成熟;
2. 分析競品,了解別人都做了什么;拉上UI設(shè)計(jì)師一起,測試機(jī)器上裝上4,5個(gè)有相同功能的App,我們兩個(gè)人一起分析每一個(gè)頁面里的內(nèi)容和操作流程,互相討論;
3. 根據(jù)競品和我們的實(shí)際需求,整理出基本的數(shù)據(jù)模型;在這個(gè)過程中,需要非常多得取舍決策,因?yàn)橐粋€(gè)完整的線下拍賣有太多的概念可以抽象到數(shù)據(jù),哪些真的需要用在產(chǎn)品里,哪些不重要,這個(gè)完全考察的是經(jīng)驗(yàn)和理解決策力;
4. 根據(jù)線下規(guī)則,競品常態(tài)和我們的實(shí)際需求,設(shè)計(jì)我們自己的業(yè)務(wù)流程;這部分,有時(shí)間的話,我想在產(chǎn)品分類文章里分享下我是怎么設(shè)計(jì)我們的競價(jià)產(chǎn)品的;
5. 根據(jù)流程,設(shè)計(jì)系統(tǒng)架構(gòu)。
6. 根據(jù)架構(gòu)原型實(shí)現(xiàn)中遇到的問題,優(yōu)化細(xì)節(jié)。
下文將著重討論5,6這兩部分。
一、競價(jià)的系統(tǒng)架構(gòu)
我們來看下常規(guī)狀態(tài)下,一個(gè)線下拍賣的核心模式。拍賣,簡單來說,就是一個(gè)人吆喝賣,多個(gè)人搶著買,在賣的人規(guī)定的條件下(一般為時(shí)間限制),出價(jià)最高者得。好,那么整體模式可以概括成下面這張圖:
那我現(xiàn)在把這張圖,變化一下,只保留他們之間傳遞的信息,再看一下:
這像什么?對了,聊天室。整個(gè)拍賣過程在結(jié)構(gòu)上本質(zhì)上就是一個(gè)群聊。只不過,普通的聊天室的實(shí)現(xiàn),服務(wù)器端只是單純的廣播消息,客戶端只是單純的收發(fā)消息;而競價(jià)系統(tǒng),服務(wù)器端除了負(fù)責(zé)廣播消息之外,還需要對每一條消息進(jìn)行判定,并在最終聊天室關(guān)閉時(shí)(也就是競價(jià)結(jié)束時(shí))對所有消息進(jìn)行一次綜合判定,而各個(gè)不同客戶端發(fā)出的消息時(shí),互相之間有依賴關(guān)系(后一次出價(jià)必定在前一次基礎(chǔ)之上)。
這樣做起來思路就相對清晰了:兩大模塊:聊天系統(tǒng)+判定邏輯。
俗話說,條條大路通羅馬。同樣的需求,做起來有不同的方法,只是不同的方法最后的效果可能不同。同樣是做巧克力,Teuscher的手工巧克力就是比超市里賣的廉價(jià)巧克力口感好,賣的價(jià)格高。就看具體的要求標(biāo)準(zhǔn)是什么。同理,做一個(gè)聊天系統(tǒng),可以是實(shí)時(shí)系統(tǒng),也可以是一個(gè)非實(shí)時(shí)系統(tǒng);可以用長連接,也可以用短連接;可以用輪詢驅(qū)動,也可以用事件驅(qū)動。關(guān)鍵看需求標(biāo)準(zhǔn)。
那么,回到我們的產(chǎn)品。最深層次的需求是:
1) 客戶端能發(fā)起出價(jià)請求;
2) 當(dāng)價(jià)格更新時(shí),所有參與出價(jià)的客戶端能夠看到最新價(jià)格;
3) 系統(tǒng)功能要求遠(yuǎn)高于性能要求;
4) 開發(fā)周期緊。
擺在我們面前的,最明顯的有2種做法:
1) 非實(shí)時(shí)的polling:
所有客戶端的頁面定時(shí)向服務(wù)器發(fā)起更新請求,拉取最新價(jià)格和出價(jià)歷史列表。
2) 實(shí)時(shí)事件驅(qū)動:
所有價(jià)格更新以事件方式主動更新到每一個(gè)激活客戶端。
兩種方式優(yōu)劣對比:
到這肯定有人會有這樣的問題:聊天系統(tǒng)想都不用想——很自然的應(yīng)當(dāng)選擇第二種直接上Node.js+socket.io啊。可是,我做任何設(shè)計(jì),總是喜歡考慮這些問題:
我是不是非得用這種技術(shù),是否有其他更快的方法,“殺雞要不要用牛刀”,“我是不是只有一把刀”……
那么落到手頭的實(shí)際項(xiàng)目,我要考慮的是:
a) 不能直接使用Node.js操作數(shù)據(jù)庫,因?yàn)镹ode的ORM和系統(tǒng)的Django ORM不匹配
b) 必須盡可能復(fù)用已有系統(tǒng)架構(gòu),在最短時(shí)間內(nèi)完成功能
c) 必須顧及競價(jià)系統(tǒng)可能存在的復(fù)雜功能的要求
d) 用戶體驗(yàn)優(yōu)先級最高
這些東西已考慮下來,最終就形成了現(xiàn)在的系統(tǒng)方案,方案1)和方案2)的結(jié)合:
我們利用原有系統(tǒng)架構(gòu)(圖中右下部框內(nèi)部分),使用Redis作為橋梁,直接讓Node服務(wù)器連接在Django服務(wù)器:
1)Node服務(wù)器負(fù)責(zé)維護(hù)WebSocket連接并且廣播消息,通知客戶端實(shí)時(shí)更新價(jià)格;
2)Django服務(wù)器負(fù)責(zé)響應(yīng)出價(jià)POST請求,直接操作數(shù)據(jù)庫;然后通知Node服務(wù)器更新事件;
這樣,我們做出的權(quán)衡就是:
舍棄Node的高并發(fā)的能力,把系統(tǒng)壓力仍然嫁接在已有Server之上;
利用Node對WebSocket的優(yōu)秀支持,提升用戶的實(shí)時(shí)體驗(yàn);
復(fù)雜度上,增加客戶端(Web和App)支持WebSocket的能力,但是利用Redis隊(duì)列潤滑系統(tǒng)整體框架,復(fù)用已有系統(tǒng)的處理邏輯和方式。
二、細(xì)節(jié)的打磨
上面提到的是競價(jià)基礎(chǔ)架構(gòu)的設(shè)計(jì)思路,那么想要把一個(gè)聊天系統(tǒng)真正的變成一個(gè)競價(jià)系統(tǒng),還得靠消息判定邏輯。
原本來說,來一個(gè)出價(jià)POST請求,通過App Server判斷價(jià)格是否生效,生效修改數(shù)據(jù)庫并通知,失效則拒絕。多個(gè)請求到來時(shí),F(xiàn)IFO排隊(duì),或者數(shù)據(jù)加鎖。
很簡單,和普通的Web請求讀寫思路沒有太大區(qū)別。但是競價(jià)系統(tǒng)在這里有一個(gè)不得不去考慮的特殊需求:代理出價(jià)。
在這里需要解釋一下代理出價(jià):代理出價(jià)就是“替你出價(jià),當(dāng)你不在場的時(shí)候”。相當(dāng)于一個(gè)自動機(jī)器人,每當(dāng)別人的報(bào)價(jià)高于自己的報(bào)價(jià)則在價(jià)格到達(dá)自己限定的最高上限之前都會自動加價(jià)后報(bào)價(jià)。比如,某件商品起拍價(jià)1000,加價(jià)幅度為100,我設(shè)定的代理出價(jià)額度為2000,那么只要價(jià)格低于2000,你無論出價(jià)多少,我都會自動比你高100出價(jià)。你出1100,我出1200,你出1300,我出1400……這個(gè)過程中,我不必在線出價(jià),系統(tǒng)自動幫我出價(jià)。
那這個(gè)代理出價(jià)的需求會帶來哪些新的問題:
1) 驚群效應(yīng):在有多個(gè)代理人的情況下,因?yàn)樽詣痈偁帲瑑r(jià)格會在最短時(shí)間內(nèi)上升到最高上限。比如有3個(gè)人都設(shè)置了代理出價(jià),分別設(shè)定最高限額是M1,M2,M3。那么一旦競價(jià)開始價(jià)格將飚升至Max(Mi)。那么,系統(tǒng)要不要控制這個(gè)過程?
2) 如果M1=M2=M3,最終獲勝的應(yīng)該是誰?
3) 如果把系統(tǒng)自動出價(jià)的行為看做是一個(gè)后臺的機(jī)器人,那么這個(gè)機(jī)器人的“地位”如何?是把每一個(gè)設(shè)置代理出價(jià)的用戶都看做是單獨(dú)的機(jī)器人獨(dú)立出價(jià),還是由一位機(jī)器人統(tǒng)一負(fù)責(zé)協(xié)調(diào)所有的代理出價(jià)?
其中,前2個(gè)問題是需求策略問題,最后一個(gè)是技術(shù)實(shí)現(xiàn)問題。
關(guān)于驚群的問題,首先來看從產(chǎn)品的角度要不要“刻意”控制:系統(tǒng)刻意延緩這個(gè)“互相抬價(jià)”的過程,還是放任讓價(jià)格在很短的時(shí)間內(nèi)的飆漲到代理人群中的最高出價(jià)。開發(fā)人員在第一版的設(shè)計(jì)里,是考慮了“用戶體驗(yàn)”,加入了一個(gè)定時(shí)機(jī)制,刻意延緩了整個(gè)代理“互相出價(jià)”的進(jìn)程,這樣還能讓手動出價(jià)的用戶能夠在代理競爭出價(jià)的“間隙”里也能有機(jī)會出價(jià),即時(shí)這個(gè)價(jià)格并沒有競爭力。理由是:這樣看起來更像是大家坐在一起互相出價(jià),不然看起來“很假”。由于做這個(gè)設(shè)計(jì)的時(shí)候我正好休假,所以開發(fā)人員并沒有和我溝通就直接把代碼寫完了。我回來以后,我的第一反應(yīng)是很開心,因?yàn)橹辽僬f明團(tuán)隊(duì)的開發(fā)人員能“主動思考用戶體驗(yàn)”,而且證明他做的很快,想到方案并快速原型實(shí)現(xiàn)是必須的。但是,我習(xí)慣性的問了自己一個(gè)問題:“真的有必要讓它看起來不那么假么?用戶真的在意出價(jià)過程中如果出現(xiàn)多個(gè)代理出價(jià)的情形,自己還必須要有機(jī)會出價(jià)么?”。要回答這個(gè)問題,我們退一步,退回到線下拍賣的真實(shí)場景中:
現(xiàn)實(shí)拍賣中,拍賣公司或者組織者會給每一個(gè)有代理出價(jià)需求的客戶提前安排好專門的出價(jià)代表,并和現(xiàn)場其他到場客戶坐在一起舉牌。線上“驚群效應(yīng)”其實(shí)就是現(xiàn)實(shí)場景中,這些代表們爭先恐后的舉到自己代理的客戶承受的最高價(jià)才能力保客戶最大可能獲得最終勝利。那么在這個(gè)過程中,現(xiàn)場其他的客戶很有可能沒有機(jī)會來的及舉牌,會在意么?我看不會,為什么,因?yàn)楦們r(jià)不是“享受過程”,而是“獲得結(jié)果”!你們在爭相舉牌,我沒機(jī)會,但是我無所謂,因?yàn)楫?dāng)代理舉牌完畢后,只可能有兩種結(jié)果:當(dāng)前價(jià)格我還能再加,或者當(dāng)前價(jià)格已經(jīng)超過我的預(yù)期。那么這兩種情況都不會降低我的體驗(yàn),我想加我就等你們消停了我再舉牌,這個(gè)時(shí)候只要你們代理不修改代理價(jià)格,你們就都不是我的競爭對手了;而如果當(dāng)前哄抬的價(jià)格已經(jīng)超過我的心理價(jià)位,前面我有沒有舉過牌對我來說顯然一點(diǎn)意義都沒有,反正我肯定是拿不到這個(gè)拍品了。所以,我問開發(fā)“把這個(gè)定時(shí)器拿掉對你的代碼有多大改動?”“不大”“好,拿掉。不需要增加額外復(fù)雜度。”
所以這里我的權(quán)衡是:即使讓開發(fā)翻工(當(dāng)然是在可以接受的范圍內(nèi)),也必須去掉過度的“結(jié)構(gòu)設(shè)計(jì)”。
第二個(gè)問題,理解好線下拍賣對這個(gè)問題的處理方式就比較好辦。線下的方法是:“拍賣師說的算!”其實(shí)就是說,系統(tǒng)保留所有解釋權(quán)。那就好辦了,自己定義了一個(gè)規(guī)則就可以。
關(guān)于第三個(gè)問題,字面上不太好理解,我們來看一個(gè)圖:
這就是描述第一種情況中的“機(jī)器人是作為獨(dú)立出價(jià)人單獨(dú)出價(jià)”,指的就是系統(tǒng)并不對代理出價(jià)者做特殊處理,每一個(gè)代理在接收到價(jià)格更新事件后都可以直接對競價(jià)系統(tǒng)發(fā)起出價(jià)請求。這種設(shè)計(jì)優(yōu)劣如下:
優(yōu)點(diǎn):系統(tǒng)功能角色劃分清晰,每個(gè)代理可以享有獨(dú)立出價(jià)權(quán),更接近真實(shí)拍賣場景;
缺點(diǎn):因?yàn)槊總€(gè)代理都可以接收到更新事件,每個(gè)代理都可以發(fā)出新的出價(jià)請求,而這些出價(jià)請求又會被其他代理收到,再響應(yīng)出新的出價(jià)請求。所以必須額外增加一個(gè)機(jī)制,防止多個(gè)代理循環(huán)出價(jià)帶來監(jiān)聽的“競價(jià)事件”爆發(fā)問題。
我們再來看看第二種可能的設(shè)計(jì):
在這里,所有的代理出價(jià)者被納入內(nèi)部的“Coordination System”中,而不再和外部的手動出價(jià)人并列。當(dāng)任何一個(gè)出價(jià)事件發(fā)生時(shí),首先由這個(gè)“Coordinator”在眾多代理出價(jià)者中協(xié)調(diào)出一個(gè)最終勝者,然后再向競價(jià)服務(wù)器發(fā)出一個(gè)最終競價(jià)請求。Coordinator選出唯一勝者的同時(shí),也把所有其他的代理給淘汰掉了。也就是說,只需要1次出價(jià),就能夠把所有可能的代理者全部“洗清”,只剩一個(gè)最高代理者和其他手動出價(jià)人競爭。在這個(gè)“協(xié)調(diào)”過程中,忽略所有其他手動出價(jià)。這種設(shè)計(jì)優(yōu)劣如下:
優(yōu)點(diǎn):系統(tǒng)實(shí)現(xiàn)清晰簡單,不會產(chǎn)生“驚群”問題;
缺點(diǎn):系統(tǒng)邏輯和真實(shí)場景有直觀上的差距;
這兩種都有明顯的優(yōu)缺點(diǎn),從模擬真實(shí)線下邏輯來說,第一種更優(yōu),理解起來也更容易;而從系統(tǒng)結(jié)構(gòu)實(shí)現(xiàn)角度來說,我更喜歡第二種,做起來更直接更簡單。你會問我第二種的話,如果用戶體驗(yàn)很差怎么辦?其實(shí)這種場景,用戶體驗(yàn)差只有一種情況,就是很多人真實(shí)在線出價(jià),同時(shí)有大量的人設(shè)置了代理出價(jià)。而這種情況對于我們目前的業(yè)務(wù)而言,顯然遙不可及。更何況,如果真的是這種大規(guī)模并發(fā)的場景,整個(gè)系統(tǒng)結(jié)構(gòu)都可能要做調(diào)整了。雖然我個(gè)人傾向第二種但是我們線上版本用的是第一種,因?yàn)椤谖倚菁倨陂g我們NB的開發(fā)把第一種做完了!(攤手)。最后想來好像應(yīng)該怪我應(yīng)該提前把設(shè)計(jì)提前討論到這層再交給開發(fā),但是仔細(xì)想想,以目前的情況看,這兩種并不會對產(chǎn)品本身有本質(zhì)影響。所以既然已經(jīng)做完了,那就先用。
至于使用第一種設(shè)計(jì),我們額外設(shè)計(jì)了附加的措施來解決具體的“驚群”問題:
因?yàn)槲覀兪鞘褂帽闅v列表的方式掃描所有的出價(jià)代理的,那么當(dāng)某個(gè)代理成功發(fā)起出價(jià)請求后,在本次掃描循環(huán)周期內(nèi),后續(xù)的代理不再出價(jià),而只是掃描其設(shè)置的封頂價(jià)格是否失效,是則忽略,否則從列表中移除。而當(dāng)每個(gè)代理接收到價(jià)格更新事件時(shí),首先確認(rèn)此次出價(jià)是否為本人,是則忽略此次更新,否則繼續(xù)發(fā)起出價(jià)事件。
三、總結(jié)
本文主要記錄的是我在做產(chǎn)品的競價(jià)功能的前后遇到的問題和設(shè)計(jì)思路。我個(gè)人認(rèn)為,這是一個(gè)很好的關(guān)于產(chǎn)品需求和架構(gòu)設(shè)計(jì)之間互相權(quán)衡乃至制衡的例子。做互聯(lián)網(wǎng)產(chǎn)品的架構(gòu)師,不能只是掉在架構(gòu)的黑洞里鉆研架構(gòu)本身,而是應(yīng)該千方百計(jì)的用最合理的架構(gòu)用最快的時(shí)間去滿足產(chǎn)品的功能。我一直認(rèn)為,做產(chǎn)品和做架構(gòu)本身是不應(yīng)該分離的,我堅(jiān)持認(rèn)為研發(fā)出身的人如果去能做產(chǎn)品,應(yīng)該比其他人更有優(yōu)勢,至少在產(chǎn)品本身的實(shí)現(xiàn)上,更有說服力更有效。當(dāng)然,關(guān)于市場和運(yùn)營本身的能力也是至關(guān)重要,但那是另外的話題了。
2016年2月25日,完稿于南京。