實作問題 === ### 1. 請實作下列需求 (略) > github link(偷懶版): https://github.com/TerrenceChao/universe-tech > github link(laravel版): https://github.com/TerrenceChao/lara-universe-tech > (*可檢視 commit 時間) > > Laravel 版本已根據偷懶版做了調整。 > - 請先 migrate, 執行 seeder. > - 請重複 call API: (game_id = 1 ~ 6, preffer 3 & 6) > http://127.0.0.1:8000/api/lottery/update?game_id=3&issue=20190903001 觀其變化。 > [color=#13db88] > > 資料庫內容: > ![](https://i.imgur.com/jP1kR2L.png) > ref: https://drive.google.com/file/d/1UKQmEEKCEF198dVFlwCkQUA-Azrs-uPx/view?usp=sharing > [color=#13db88] <br/> ### 2. 上面程式或規格可能存在什麼潛在問題?還可以怎樣優化? > A. 每次 new xxxxClass 都需要重新再獲取資源 ( DB 中 'vendors' + 'game_vendor_mappings' 的資料)。其實可以透過 in memory 的方式儲存先前重複的資料。且 in memory 的方式比 cache(redis) 要好,因為 cache(redis) 也是個 DB, 當你的 AP Server 有多台的時候,大家都要去存取 cache(redis), redis 又是單線程的;而存在 AP Server 自己的記憶體,雖然 N 個 AP Server 對重複的資料要讀取 N 次,但最後大家都靠自己,不用去 cache 讀資料的。 > > [color=#13db88] > > B. 關於取得號源 API 時在 '回傳列表' 的情況下 (雖然是第三方 API,但這邊僅針對規格做討論): 當欲取得同彩種的列表時可能有大量的列表,這時一般來說可以用 SQL 的 limit, skip 來取得一部份的列表;但是用到 skip 表示 database 會從頭搜尋第一筆紀錄接著再擷取需要 shift 的筆數,透過分頁逐次取得部份列表時效能會越來越慢。 因此建議用 limit 和 opentime 取得列表。以 opentime 作為 where 條件式過濾,再以 limit 限制筆數。 > [color=#13db88] <br/> ### 3. 如果要加入第三家號源,會怎麼進行擴充? > 定義第三家號源的 class, 並且 class 的名稱 必須與資料庫中第三家號源的名稱相符。詳見下列項目之間的對映。 > 'App\Domain\Lottery\Gameservice' > 'database\seeds\VendorsTableSeeder' > [color=#13db88] <br/> ### 4. 每個號源有不同的速率限制,會如何實現限流,防止被 ban? 假設號源一限制 5 秒一次,號源二限制 3 秒一次 (同號源不同彩種是分開計算的) > A. 針對 **[特定號源+特定彩種]** 設定 throttle. 不一定要用 Laravel 本身的 middleware: throttle, 可尋找套件,該套件可以針對同樣的 route 不同的 querystring 設定 rate limit. 若回傳錯誤的原因是 rate limit 達到上限,則循序透過下一個副號源取得結果 (或把 **[特定號源+特定彩種]** 在 cache 中記錄下來)。 > [color=#13db88] > > B. 利用 cache (redis) 紀錄 **[特定號源+特定彩種]** 上一次 call API 的時間,若距離上次呼叫時間太近則不 call API. 循序透過下一個副號源取得結果 (或把 **[特定號源+特定彩種]** 在 cache 中記錄下來)。 > [color=#13db88] <br/> ### 5. 開獎時間並非準時,您會如何實現重試機制? 號源站並非能第一時間抓到該彩種中獎號碼,因此存在抓不到的可能性 > 根據 class UpdateWinningNumberJob 的意圖猜測屬於 queue 的機制;並且因每個號源有不同的速率限制,在同一次 job 中再次重試很大可能達到 rate limit 上限或是影響到其他不同 request 的需求。這裡我會選擇將此次 job 直接視為失敗並重新放入 queue 中排隊執行,也可設定推遲執行的時間。 > [color=#13db88] > > 補充 > - 當然推遲執行的時間是個辦法; > - 上述 “直接視為失敗” 的辦法並不是太好,畢竟程式還是花了 effort 去處理 “無效的” job; 此外,若是用 DB 當 queue 的話,當視為失敗的時候,table: failed_jobs 會越長越大: > - 較好的做法是: > - call API 之前,先檢查 cache 中 **[特定彩種]** 之前未成功的紀錄 (1 ~ N 筆),連帶此次一起更新;更新成功才將 “未成功的紀錄” 從 cache 中刪除。 > - call API 之後,結果要是沒抓到的話,在 cache 把此次 “未成功的” Job 紀錄下來 ( **[特定彩種+特定開獎期號]** , 當然不重複紀錄),交由下一個 Job 連帶更新。 > - 也就是說,雖然當下的 Job 是打算處理 **[特定彩種+特定開獎期號]** 的更新,但他會連帶之前失敗的一起更新了。(更新了 2 筆以上) > - 這裡針對的是 DB 中 'lotteries' 的資料,是大量且經常變動的。 > [color=#ff0000] <br/> ### 6. 可以實現哪些手段來減少程式運行時間? > #### 偷懶版 > A. 針對 hot data 多利用 cache. (比如限定時段內,針對特定彩種一次取得多個開獎號碼後緩存) 減少存取 DB 或 call API 的次數。 > [color=#13db88] > > B. v1/GameService.php 中的 getWinningNumber() 每次都會從 DB 取得資料,重複資料取了多次;改以 v2/GameService.php 中的方式,多一個 mapping 變數,呼叫 getWinningNumber(...) 時會把資料紀錄下來,之後便不重複存取 DB,以減少運行時間。(可比較 偷懶版: v1/GameService.php, v2/GameService.php 的差異。) > [color=#13db88] > > 為了有比對的效果,實際上我是從 v2 版本稍作修改「改回」v1, 過程中若有發現 v1 有什麼不順的地方還請多包涵。 > [color=#ff0000] > > #### Laravel 版本 > Laravel 版本已根據偷懶版做了調整。詳見 > github link(laravel版): https://github.com/TerrenceChao/lara-universe-tech ,已透過 in memory 的方式緩存先前從資料庫讀取的資料。 > - 請先 migrate, 執行 seeder. > - 請重複 call API: (game_id = 1 ~ 6, preffer 3 & 6) > http://127.0.0.1:8000/api/lottery/update?game_id=3&issue=20190903001 觀其變化。 > 1st call: > ![](https://i.imgur.com/NAuTSOh.png) > 2nd call ~ N call: > ![](https://i.imgur.com/MoMhVAC.png) > 記得清除快取 'php artisan cache:clear' > [color=#13db88]