數據同步的一些思考與改進
背景
閑的沒事,自己寫了個小網站,搭建在自己國外的VPS上,VPS內存極小(512M),而且還要跑點別的(你懂的),內存更緊張巴巴. 改造之前小網站用到了時髦的Redis,Rabbmitmq,Mysql,那時候阿里雲的學生主機內存富足,裝這么多中間件壓力不大,可到了這樣的小內存VPS上,一切都變得水土不服,索性啥中間件都不要了,數據庫也不要了.
沒了數據庫,網站的數據從哪里來?存在哪里? 文本形式持久化到本地磁盤?
國外的VPS不比國內,可能哪天說不能訪問就不能訪問了,VPS的磁盤存儲顯然不踏實.
同事給我建議了萬能的Github,聽過Github托管代碼📜,托管靜態頁面🔮,托管女裝大佬💃,但托管網站數據倒是第一次聽說,於是我對網站架構進行了重新設計.
Plan1 數據的同步
小網站數據不多,10M左右,所有數據直接加載到內存中服務器也不會吃力,網站啟動,自動從Github Clone數據,並定期把內存中的數據序列化后Push到Github.
可以看到,整個過程中,好像沒有磁盤啥事了,在我的眼里,Github就是一塊延時略高的磁盤(其實延時也還好,國外的Github訪問速度飛快).
Plan2 同步的頻率
磁盤的讀取速度和內存無法比,何況遠程的Github,那么如果減少數據從內存到Github的同步開銷呢?顯然就是減少同步的頻率.
一小時同步一次,應該夠了.
但如果我的網站在這一小時掛了boom🌋,而數據還沒來得及同步,那上次一同步到網站掛掉這個時間段內的數據不就沒了嗎?細思極恐😱!
Plan3 多多不益善
既然一小時一次不安全,那就一分鍾同步一次!
其實這樣也是有問題的,小網站一般都是無人問津,如果以較高的頻率進行數據同步,可以說絕大多數(用互聯網的所法是百分之N個9)的數據同步都是沒意義的,同時還增大了數據的同步開銷,沒准Github還會把我的賬號給封了.
Plan4 內存數據變更立即觸發數據同步
在我的網站中,有統一的數據訪問層,只要數據訪問層中的insert,update,delete處加入數據同步事件,即可實現一旦更新立即同步.
這樣是數據是安全了,可是一次訪問請求往往伴隨着多次數據更新,每更新一次同步一次,可能是最腦殘🙈的做法吧.
Question
數據更改一次同步一次不合理,同步頻率太低數據不安全,頻率太高多數同步沒有意義,到底該怎樣呢?
局部性原理
在揭開我的設計方案前,我們先來過一下CPU訪問存儲器時所遵守的局部性原理.
在計算機存儲介質這個金字塔中,越靠近金字塔頂端,空間越小,但是讀取數據越快;越靠近金字塔底端,空間越大,但訪問速度也越慢.
正式因為這樣,所以每次自下而上的數據數據流大小逐層遞增, 交換頻率逐層遞減,如何在時間與空間上取到平衡點是關鍵.
於是有了空間局部性原理和時間局部性原理,力求讓計算機的數據流動更高效.
空間局部性
如果一條數據被訪問,那么與它臨近的數據也可能要被用到. 比如數組,你訪問了索引1上的數據,那么1附近的數據當然很有可能被訪問,所以這個時候干脆把1附近的數據也往上加載一個層級.
時間局部性
如果一條數據項正在被訪問,那么在近期它很可能還會被再次訪問,所以這個時候干脆就把它留在當前層級,先不急着回收掉.
而網站的數據的更新也是具有時間局部性的,像我這樣並冷門的網站,基本沒人訪問,但是一旦訪問了,立即就要進行點擊量的更新,站點響應速度的記錄,沒准又會有評論留言,然后要通知管理員進行留言審核.這大概就是不鳴則已,一鳴驚人,一次訪問短期內往往立即觸發一連串的數據更新,我認為這也是一種時間局部性.
所以,在數據同步上,我設計了如下方案.
- 另起一個線程作為定時任務,主要負責定時數據同步
- 正常情況下,每小時與Github進行數據同步.
- 一旦網站數據被更新,檢查剩余同步時間是否大於30秒.
** 如果大於三十秒,強行把計時器剩余時間設置為30秒.
** 如果小於三十秒,不做操作. - 計時器時間走完,立即同步數據到Github.
定時沙漏⏳
原本文章說到這里就可以結束了,但程序員注定愛代碼愛過文字,又恰好我天生愛造輪子,我從令牌桶得到靈感設計了一個乞丐版沙漏計時器
,可以用於任何定時任務的執行,班門弄斧,歡迎提出改進意見.
Show time
public class BlogsTimer
{
private static Stack<int> _upFunnel; //沙漏上部分
private static Stack<int> _downFunnel; //沙漏下部分
private static readonly List<Action> TimerEvents; //定時執行的事件
private static bool _timerSwitch; //沙漏開關
private static readonly int Speed; //每秒消費令牌數量
private static Thread _timerThread;
private static readonly object TimerLock;
static BlogsTimer()
{
_upFunnel = new Stack<int>();
_downFunnel = new Stack<int>();
Speed = 1 * 1000;
TimerEvents = new List<Action>();
TimerLock = new object();
}
//計時器開始
public static void Start(TimeSpan timeSpan)
{
lock (TimerLock)
{
_upFunnel.Clear();
_downFunnel.Clear();
for (var i = 0; i < timeSpan.TotalSeconds; i++)
{
_upFunnel.Push(i);
}
}
_timerSwitch = true;
_timerThread = new Thread(Consume); //起一個線程消費桶里的令牌
_timerThread.Start();
LunchEvents(); // 觸發事件
}
public static void Stop()
{
_timerSwitch = false;
}
//給沙漏注冊定時執行事件
public static void Register(Action timeEvent)
{
TimerEvents.Add(timeEvent);
timeEvent.Invoke();
}
//把沙漏加速到指定的時間
public static void AccelerateTo(TimeSpan timeSpan)
{
var accelerateSeconds = timeSpan.TotalSeconds;
lock (TimerLock)
{
if (_upFunnel.Count < accelerateSeconds) //當前沙漏中剩余令牌小於設置中秒數,則返回不加速
return;
while (_upFunnel.Count > accelerateSeconds && _upFunnel.Count > 1) //令牌數大於秒數,則釋放出多余令牌
{
_downFunnel.Push(_upFunnel.Pop());
}
}
}
private static void LunchEvents()
{
TimerEvents.ForEach(a => a.Invoke());
}
private static void Consume()
{
while (_timerSwitch)
{
lock (TimerLock)
{
if (_upFunnel.TryPop(out var item))
{
_downFunnel.Push(item);
}
else
{
LunchEvents();
var tempStack = _downFunnel; //旋轉沙漏
_downFunnel = _upFunnel;
_upFunnel = tempStack;
}
}
Thread.Sleep(Speed);
}
}
}
源碼地址: https://github.com/liuzhenyulive/iBlogs/blob/master/Src/iBlogs.Site.Core/Common/iBlogsTimer.cs
演示地址: https://www.iblogs.site