---
# System prepended metadata

title: 實作問題

---

實作問題
===
### 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]

