關(guān)于數(shù)據(jù)同步的一些思考與改進(jìn)

背景
閑的沒事,自己寫了個小網(wǎng)站,搭建在自己國外的VPS上,VPS內(nèi)存極小(512M),而且還要跑點(diǎn)別的(你懂的),內(nèi)存更緊張巴巴. 改造之前小網(wǎng)站用到了時髦Redis,Rabbmitmq,Mysql,那時候阿里云的學(xué)生主機(jī)內(nèi)存富足,裝這么多中間件壓力不大,可到了這樣的小內(nèi)存VPS上,一切都變得水土不服,索性啥中間件都不要了,數(shù)據(jù)庫也不要了.
沒了數(shù)據(jù)庫,網(wǎng)站的數(shù)據(jù)從哪里來?存在哪里? 文本形式持久化到本地磁盤?
國外的VPS不比國內(nèi),可能哪天說不能訪問就不能訪問了,VPS的磁盤存儲顯然不踏實(shí).
同事給我建議了萬能的Github,聽過Github托管代碼,托管靜態(tài)頁面,托管女裝大佬,但托管網(wǎng)站數(shù)據(jù)倒是第一次聽說,于是我對網(wǎng)站架構(gòu)進(jìn)行了重新設(shè)計.
Plan1 數(shù)據(jù)的同步

小網(wǎng)站數(shù)據(jù)不多,10M左右,所有數(shù)據(jù)直接加載到內(nèi)存中服務(wù)器也不會吃力,網(wǎng)站啟動,自動從Github Clone數(shù)據(jù),并定期把內(nèi)存中的數(shù)據(jù)序列化后Push到Github.
可以看到,整個過程中,好像沒有磁盤啥事了,在我的眼里,Github就是一塊延時略高的磁盤(其實(shí)延時也還好,國外的Github訪問速度飛快).
Plan2 同步的頻率
磁盤的讀取速度和內(nèi)存無法比,何況遠(yuǎn)程的Github,那么如果減少數(shù)據(jù)從內(nèi)存到Github的同步開銷呢?顯然就是減少同步的頻率.
一小時同步一次,應(yīng)該夠了.
但如果我的網(wǎng)站在這一小時掛了boom,而數(shù)據(jù)還沒來得及同步,那上次一同步到網(wǎng)站掛掉這個時間段內(nèi)的數(shù)據(jù)不就沒了嗎?細(xì)思極恐!
Plan3 多多不益善
既然一小時一次不安全,那就一分鐘同步一次!
其實(shí)這樣也是有問題的,小網(wǎng)站一般都是無人問津,如果以較高的頻率進(jìn)行數(shù)據(jù)同步,可以說絕大多數(shù)(用互聯(lián)網(wǎng)的所法是百分之N個9)的數(shù)據(jù)同步都是沒意義的,同時還增大了數(shù)據(jù)的同步開銷,沒準(zhǔn)Github還會把我的賬號給封了.
Plan4 內(nèi)存數(shù)據(jù)變更立即觸發(fā)數(shù)據(jù)同步
在我的網(wǎng)站中,有統(tǒng)一的數(shù)據(jù)訪問層,只要數(shù)據(jù)訪問層中的insert,update,delete處加入數(shù)據(jù)同步事件,即可實(shí)現(xiàn)一旦更新立即同步.
這樣是數(shù)據(jù)是安全了,可是一次訪問請求往往伴隨著多次數(shù)據(jù)更新,每更新一次同步一次,可能是最腦殘的做法吧.
Question
數(shù)據(jù)更改一次同步一次不合理,同步頻率太低數(shù)據(jù)不安全,頻率太高多數(shù)同步?jīng)]有意義,到底該怎樣呢?
局部性原理
在揭開我的設(shè)計方案前,我們先來過一下CPU訪問存儲器時所遵守的局部性原理.
在計算機(jī)存儲介質(zhì)這個金字塔中,越靠近金字塔頂端,空間越小,但是讀取數(shù)據(jù)越快;越靠近金字塔底端,空間越大,但訪問速度也越慢.
正式因?yàn)檫@樣,所以每次自下而上的數(shù)據(jù)數(shù)據(jù)流大小逐層遞增, 交換頻率逐層遞減,如何在時間與空間上取到平衡點(diǎn)是關(guān)鍵.
于是有了空間局部性原理和時間局部性原理,力求讓計算機(jī)的數(shù)據(jù)流動更高效.

空間局部性
如果一條數(shù)據(jù)被訪問,那么與它臨近的數(shù)據(jù)也可能要被用到. 比如數(shù)組,你訪問了索引1上的數(shù)據(jù),那么1附近的數(shù)據(jù)當(dāng)然很有可能被訪問,所以這個時候干脆把1附近的數(shù)據(jù)也往上加載一個層級.
時間局部性
如果一條數(shù)據(jù)項(xiàng)正在被訪問,那么在近期它很可能還會被再次訪問,所以這個時候干脆就把它留在當(dāng)前層級,先不急著回收掉.
而網(wǎng)站的數(shù)據(jù)的更新也是具有時間局部性的,像我這樣并冷門的網(wǎng)站,基本沒人訪問,但是一旦訪問了,立即就要進(jìn)行點(diǎn)擊量的更新,站點(diǎn)響應(yīng)速度的記錄,沒準(zhǔn)又會有評論留言,然后要通知管理員進(jìn)行留言審核.這大概就是不鳴則已,一鳴驚人,一次訪問短期內(nèi)往往立即觸發(fā)一連串的數(shù)據(jù)更新,我認(rèn)為這也是一種時間局部性.
所以,在數(shù)據(jù)同步上,我設(shè)計了如下方案.
另起一個線程作為定時任務(wù),主要負(fù)責(zé)定時數(shù)據(jù)同步
正常情況下,每小時與Github進(jìn)行數(shù)據(jù)同步.
一旦網(wǎng)站數(shù)據(jù)被更新,檢查剩余同步時間是否大于30秒.
** 如果大于三十秒,強(qiáng)行把計時器剩余時間設(shè)置為30秒.
** 如果小于三十秒,不做操作.計時器時間走完,立即同步數(shù)據(jù)到Github.
原本文章說到這里就可以結(jié)束了,但程序員注定愛代碼愛過文字,又恰好我天生愛造輪子,我從令牌桶得到靈感設(shè)計了一個乞丐版沙漏計時器,可以用于任何定時任務(wù)的執(zhí)行,班門弄斧,歡迎提出改進(jìn)意見.
public class BlogsTimer
{
private static Stack<int> _upFunnel; //沙漏上部分
private static Stack<int> _downFunnel; //沙漏下部分
private static readonly List<Action> TimerEvents; //定時執(zhí)行的事件
private static bool _timerSwitch; //沙漏開關(guān)
private static readonly int Speed; //每秒消費(fèi)令牌數(shù)量
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); //起一個線程消費(fèi)桶里的令牌
_timerThread.Start();
LunchEvents(); // 觸發(fā)事件
}
public static void Stop()
{
_timerSwitch = false;
}
//給沙漏注冊定時執(zhí)行事件
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) //當(dāng)前沙漏中剩余令牌小于設(shè)置中秒數(shù),則返回不加速
return;
while (_upFunnel.Count > accelerateSeconds && _upFunnel.Count > 1) //令牌數(shù)大于秒數(shù),則釋放出多余令牌
{
_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; //旋轉(zhuǎn)沙漏
_downFunnel = _upFunnel;
_upFunnel = tempStack;
}
}
Thread.Sleep(Speed);
}
}
}出處:cnblogs.com/CoderAyu/p/11839520.html
關(guān)注GitHub今日熱榜,專注挖掘好用的開發(fā)工具,致力于分享優(yōu)質(zhì)高效的工具、資源、插件等,助力開發(fā)者成長!
點(diǎn)個在看,你最好看
