1、場景描述
比如說我們要做一款APP,需要通過api接口給app提供數據。假設我們是做商城,比如我們賣書的。我們可以想象下這個APP大概有哪些內容:
1)首頁:banner區域(可以是一些熱門書籍的圖片做推廣)、本周熱賣書籍區域、本月好評書籍區域、活動打折的書籍區域。。。
2)排行榜:比如第一季度熱銷榜、新書版。。。
3)書單:管理后台運營添加的書單,比如《程序員從入門到放棄》系列書單。。。
4)用戶相關的:比如用戶個人信息設置、訂單管理、消息管理、收藏的書籍。。。
數據是保存在數據庫中,考慮到高並發數據庫的瓶頸,采用DB+緩存的服務器架構。
2、重要接口匯總
看似簡單的一個app,需要調用的api接口是非常多的,總結下大概有這幾類接口:
1)列表接口:比如書單里面的書籍列表、排行榜的書籍列表;
2)詳情接口:書籍的詳細信息;
3)評論接口:書籍評論(這里可能要求購買了的才能評論)、星標;
4)點贊接口:給書籍點贊、給書單點贊;
5)收藏接口:收藏書籍、收藏書單;
6)“相關”接口:比如書籍《php從入門到放棄》相關的有哪些書籍;
7)關注接口:關注某本書或者書籍作者,一旦某本書有打折或者作者有新書,會推消息等等。或者是用戶間互相關注;
8)發布接口:比如用戶可以發布書單。A用戶發布了書單,B用戶可以關注A用戶,A用戶再發布新書單,會給B用戶推消息等等;
9)搜索接口:查詢書籍、查詢書單、查詢用戶等等
3、后台管理系統
1)書籍信息的來源:爬蟲抓取的數據、運營人員在管理后台添加;
2)書籍的價格、庫存等信息,是可以在后台設置;
3)書籍的分類、書籍的標簽,像爬蟲如果抓取不到的,運營可以手動設置;
4)書籍的排序:在管理后台設置,用於在app上展示的先后順序;
5)書單管理:運營可以添加、可以設置展示/隱藏;用戶提交的書單,在后台進行審核;
6)用戶的評論:在后台審核;
備注:管理系統還有很多功能,簡單先寫這幾個。
4、api接口設計
接口設計要遵循的一個想法:可以先從緩存讀取,讀取不到,再去數據庫讀取,然后寫回緩存。
1)banner數據: key-value類型(string類型)
redis key名: index:banners 這個key是首頁上面的banner 可以設置一個緩存時間:1小時
里面保存的數據是從數據庫獲取出來的banner數據,json之后保存的字符串。
2)實時性問題
比如數據是在管理設置的,比如banner數據添加了一條,或者某一個banner數據被刪除了或者改為不展示了,但是緩存里面還沒變化,怎么處理?
【方案一】不做處理,等待緩存自己過期,然后再有請求獲取的就是新的數據。緩存要有個過期時間,這個要看業務需要設置相應的緩存過期時間,比如5分鍾。
【方案二】管理后台對banner有添加刪除修改操作時,修改成功的同時把緩存中的數據也更新。這個方案不太贊成,因為這樣子數據就有2個地方維護了,
一個是api接口訪問的時候,如果緩存過期會回源寫緩存,一個是管理后台;
【方案三】解決方案二的缺點,就是管理后台有數據增加刪除修改的時候,把對應的緩存刪掉,這樣子api接口從緩存讀取不到key就會重新生成緩存。這樣也是比較實時的。
3)詳情數據
詳情信息可以放在緩存里面,key-value類型,設置一個過期時間
redis key名: book:detail:{book_id}
4)列表數據:key-value類型
【方案一】把每一頁的數據緩存起來,這樣api接口來獲取的時候,直接把一頁的數據從緩存取出來然后返回。
redis key名:index:book_list:{page} 緩存時間根據實際業務設置
假設每一頁20條數據,這里保存的就是20條數據的json后的字符串,key名里面有頁碼,每一頁的數據用不同的key名緩存起來。
這個方案有個問題,比如管理后台刪除一條數據,剛好緩存重新生成了,那么第一頁的20條數據肯定是最新的,但是第二頁如果緩存沒有重新生成的,
那么新的第一頁和舊的第二頁會有一條數據重復。當然,如果新增數據那么第一頁的緩存和第二頁的緩存數據中間有一條數據遺漏了。。。
【方案二】把id放入有序集合,每次獲取列表,從集合中獲取出20個id,然后去緩存中獲取詳情數據,然后返回。
管理后台如果有新增刪除,那么就在集合里面新增或刪除。 這里相比方案一,就是每次要去緩存獲取20條詳情信息,雖然緩存中獲取很快但是也有一定的開銷,但是數據最實時。
還有一個問題,感覺就是有點耦合,就是萬一集合失效,那么要重建集合,假如數據很多,比如有幾十萬幾百萬個id,那么要去數據庫獲取,然后寫入緩存,有這種隱患。
【方案三】不用緩存,采用像spinx、es這些搜索引擎。通常有些業務需要很多排序規則,比如sort字段、時間字段、銷量字段等等等,這個時候如果硬是要用方案二,
就必須是把排好序的數據保存如有序集合,比如id=1,序號=1;id=15,序號=2;id=8,序號=3,每次有新增、刪除,就要重新生成集合,很不好維護。
使用搜索系統來輔助,可以應該解決方案一和方案二的問題。當然了,看業務,如果不要緊的,方案一即可。像新浪博客、今日頭條,之前也看到他們有些分頁數據有重復的。
5)點贊接口
根據不同的業務要求,有些業務是只有點贊,沒有取消點贊,我們就討論這個有取消點贊的。
首先,假設我們有個業務是對書籍點贊,點贊之后可以取消點贊。比如說APP首頁的列表里面需要點贊數,而且要實時,怎么處理?
我們肯定有個數據表來存儲點贊信息,比如book_like 表,里面記錄了用戶對書籍的點贊信息。
鑒於點贊數如果每次用戶接口請求都去數據庫請求,會給數據庫增加壓力,我們使用redis來減壓。這里使用hash類型,下面會講為何。
redis key名: book:like:{hash_id} 內容: field =》 value
這里的hash_id是 book_id / 1000 的結果
field 的值是 book_id % 1000的結果
value的值是點贊數
為何這種設計:hash相比string類型有個優勢,在滿足2個條件的時候會進行壓縮,就是內存的占用會小很多。
這兩個條件是hash里面的field的數量小於指定數量(貌似默認是512還是1024),另外一個條件是value值的大小要小於64字節(這個也是可以調整的,配置信息里面改)。
舉個例子:
book_id = 100, 那么hash_id就是0,field是100,值是點贊數
book_id = 1001, 那么hash_id就是1,field是1,值是點贊數
列表接口里面有點贊數,可以從hash中獲取
多次redis的get請求(get、hget等),可以用管道(pipeline)來一次性獲取,也會提高速度
點贊的時候,數據肯定是要寫入數據庫的,有2種方案處理:
【方案一】
書籍表里面比如有個點贊數字段,每次點贊/取消點贊,就是往里面加1減1。
點贊表就是往里面增加或刪除數據(可以是軟刪除)。
像php的laravel的eloquent模型是支持直接給字段+-1這樣,不需要先取出數據再加。
【方案二】
先寫入緩存,然后定期把緩存的數據取出來然后寫入數據庫。這個方案的話有點耦合了,比如需要定時腳本去寫回數據庫。
【方案三】
不使用緩存,數據直接更新回book表,或者book表不需要這個字段,需要點贊數的時候去book_like表計算,如果是有搜索引擎系統,那么應該不成問題。
6)相關接口
比如根據分類關聯書籍。《php從入門到放棄》和《php從入門到奔潰》關聯。
數據庫里面可以根據分類id查找。簡單的業務可以通過集合存儲分類id下有哪些書籍。
如果復雜的業務,比如書籍有多個分類,比如分類id=1 , 分類名稱:php,分類id=2 ,分類名稱:程序開發,假設這2個分類是同級的,
那么可能有一些書是同時屬於這2個分類的。
又或者有些業務下,是根據標簽來關聯,一個書籍可以有多個標簽,標簽和書籍是多對多關系。
此時又要來一句搜索系統可以處理。。。緩存也不是不能處理,感覺有些場景緩存處理起來很費勁。
以上只是一些很膚淺的看法,肯定有更好的處理方案,待研究補充上。
5、api接口設計總結
考慮到高並發時數據庫的瓶頸,所以需要把請求的結果緩存起來。這里的策略是:
1)從緩存中獲取數據,緩存中有數據則直接返回,或者做簡單處理然后返回;
2)緩存沒有數據,則從DB中查找數據,然后寫回緩存;這種情況叫做“回源”。
3)緩存的key是需要設置過期時間的,避免數據一直占用內存;
4)過期時間的設計,最好是打亂,避免同一時間有大量的key過期導致請求集中去DB請求導致雪崩;
5)根據業務需求,“回源”的時候考慮申請到緩存鎖的請求去數據庫獲取數據並更新緩存,其他請求則sleep一段時間(比如5毫秒10毫秒)然后再去緩存請求,如果請求還是沒數據,可以繼續等待或者直接返回空數據。
6)沒有必要緩存起來的字段,不要緩存;
7)APP不需要用到的字段沒有必要返回給APP,比如評論的審核時間、審核者,返回給APP是沒用的,因為這些數據不需要展示出來,不會被用到;
8)減少api請求的次數,比如多次請求,要看下能否合並,比如首頁,有好幾個區域,每個區域都有幾條數據。當然多次請求一樣可以實現,但是請求次數少,則服務器可以接受更多客戶端的請求。