程式設計師過關斬將--從每秒6000寫請求談起
阿新 • • 發佈:2020-03-09
![image](https://bdn.135editor.com/files/users/553/5534532/202003/wACNKZQZ_KhvW.png)
### 背景
每一個片子的幕後,都保留了你的觀看記錄,詳細的記著你觀看了幾次,跳過了那些時長 ,據說根據這些資料可以分析出你喜歡哪個日本明星,以此來做定向推送......
雖然看起來很簡單的一個功能,其實涉及到的資料量非常大,極限情況下為你的使用者數*視訊數的乘積。
那麼在只有兩個網站伺服器,一臺sqlserver的情況下,該如何面對這樣不算大資料量的寫請求呢?為什麼說是寫請求呢?因為使用者觀看視訊的每一秒你都需要記錄下來,例如:視訊的第十秒使用者觀看了。要想把這個功能搞定,首先需要定義幾個事情:
1. 記錄使用者觀看視訊情況的資料定義
2. 和客戶端互動的資料協議
3. 資料庫中記錄的資料格式
4. 如何解決伺服器寫的壓力(畢竟單臺伺服器請求數還是比較大)
### 解決方案
##### 使用者觀看視訊進度定義
對於一個視訊來說,假如有1個小時的時長,這3600秒對應著3600個是否已經觀看的狀態,對於觀看狀態來說,只有觀看和未觀看兩種狀態,所以一個bit足以,一個位元組(byte)有8個bit,所以一個byte可以表示8秒的觀看狀態,以此為基礎,進位制越高,同樣數量的字元表示的狀態就越多。
>客戶端每次上傳新的資料,需要和服務端已經存在的資料做位運算,例如: 01000 表示第二秒觀看了 ,客戶端新上傳:00011 表示第4,5秒都觀看了,對於使用者而言這個視訊第2,4,5 秒都看過,雖然只是一個簡單的運算,但是量大的時候,對cpu的消耗不容小覷。
```
第一位元組 第二位元組
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
bit: 1 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0
二進位制: 0x88 0x40
字串: 8840
```
##### 和客戶端互動協議
使用者觀看視訊的進度實時資訊,只有客戶端知道,客戶端需要上傳使用者的觀看進度資料,和服務端互動的進位制可以選擇通用性比較強的16進位制,當然你選擇100進位制也無所謂,只要雙方能同時支援,並且能正常解析即可
##### 資料庫資料格式
每種資料庫支援的資料型別有差異,所以這裡不在過多敘述,當然無論什麼格式,佔用空間越少越好,但也要根據業務的計算量來綜合考慮。
### 解決問題
##### cpu效能問題
畢竟要把使用者每次最新的觀看資料和老資料做合併工作,在使用者量大的情況下不容小覷。在綜合了各種條件之後,最終採用10進位制來做合併工作,客戶端上傳上來16進位制資料,然後轉化為十進位制,然後和觀看記錄(10進位制)做合併運算,這部分cpu省略不了,具體轉化程式為:
```
//需要新加的資料
ConcurrentQueue AddQueue = new ConcurrentQueue();
//把16進位制的字串按照兩位 分割成十進位制陣列
protected List ConvertToProgressArray(string progressString)
{
if (string.IsNullOrWhiteSpace(progressString))
{
return null;
}
//驗證是否為2的倍數長度
if (progressString.Length % 2 != 0)
{
return null;
}
var proStrSpan = progressString.AsSpan();
List ret = new List();
int i = 0;
while (i < proStrSpan.Length)
{
ret.Add(int.Parse(proStrSpan.Slice(i, 2).ToString(), System.Globalization.NumberStyles.HexNumber)); ;
i = i + 2;
}
return ret;
}
```
##### 客戶端請求數量問題
如果同時一萬用戶在同時觀看視訊,上傳資料時間間隔為2秒,意味著每秒有5000請求。由於這個業務只是一個使用者log型業務,何為log型,就是說可以容忍一部分資料丟失,針對這個資料形態,客戶端可以先在本地做緩衝記錄,沒有必要一秒上傳一次記錄,例如現在約定的客戶端30秒上傳一次記錄,如果使用者關掉客戶端,下次啟動的時候會重新上傳未成功的記錄。
##### 資料庫壓力
如果每次請求都單獨更新資料庫,按照第二條的計算每秒高達5000次update請求。使用者觀看每次視訊都載入記憶體中快取,仔細分析這種業務,由於是log型資料,所以每次你請求沒有必要都去更新資料庫,而是先更新了快取,然後定時去更新資料庫。
由於資料量的問題,所有的更新操作都會發送到一個任務佇列,佇列的執行者會根據配置批量更新資料庫,這樣比單條更新資料庫效能要高很多,其實這種方案在很多log型的業務中都有使用,批量更新對資料庫的壓力要小很多,程式碼類似以下
```
public async Task AddUserVideoData(UserVideoInfo data, DBProcessEnum processType = DBProcessEnum.Update)
{
if(processType== DBProcessEnum.Add)
{
AddQueue.Enqueue(data);
}
return 1;
}
void MulProcessData()
{
//每次更新的條數
int maxNumber = 50;
List data = new List();
while (true)
{
if (data == null)
{
data = new List();
}
try
{
if (!AddQueue.Any() && !UpdateQueue.Any())
{
System.Threading.Thread.Sleep(500);
}
else
{
//先處理 需要更新的
data.Clear();
while (data.Count <= maxNumber && AddQueue.Any())
{
if (!AddQueue.TryDequeue(out UserVideoInfo value))
{
continue;
}
//判斷是否有重複物件
if (data.Any(s => s.UserId == value.UserId && s.VideoId == value.VideoId))
{
var exsitItem = data.First(s => s.UserId == value.UserId && s.VideoId == value.VideoId);
exsitItem = value;
}
else
{
data.Add(value);
}
}
if (data != null && data.Any())
{
var ret = UserVideoProgressProxy.Add(data);
}
}
}
catch (Exception err)
{
}
}
}
```
### 寫在最後
其實這種高IO的操作用sqlserver這種關係型資料庫反而不好,Nosql在這種簡單高IO的情境下要很多,改天可以改為redis試一試,估計會比sqlserver要好