對於數據存儲層高併發問題,最早想到的可能就是讀寫分離,在網站訪問量大而且讀寫不平均的狀況下,將存儲分爲master,slave兩臺,全部的寫都路由到master上,全部的讀都路由到slave上,而後master和slave同步。若是一臺salve不夠,能夠加多臺,好比一臺master,3臺slave。對於什麼是讀寫分離,以及讀寫分離有什麼好處,這裏再也不敘述,有興趣的能夠參考
這裏 。
在設計讀寫分離的時候,有幾種解決方案:
1. 將讀寫分離放在dao層,在dao層, 全部的insert/update/delete都訪問master庫,全部的select 都訪問salve庫,這樣對於業務層是透明的。
2. 將讀寫分離放在ORM層,好比mybatis能夠經過mybatis plus攔截sql語句,全部的insert/update/delete都訪問master庫,全部的select 都訪問salve庫,這樣對於dao層都是透明。
3. 放在代理層,好比MySQL-Proxy,這樣針對整個應用程序都是透明的。
對於絕大多數情景,讀寫分離都適用,可是讀寫分離有一個問題是master slave同步,這個同步是會有必定延遲,所以有幾個場景仍是要注意的:
考慮下面一個用戶註冊場景。
boolean addUserSuccess = userDao.addUser(registUser);
if (addUserSuccess) {
cachedThreadPool.execute(() -> {
EmailService.SendActivateEmail(userId);
});
}
public static void SendActivateEmail(int userId) {
User user = userDao.getUser(userId);
String userName = user.getUserName();
String userEmail = user.getUserEmail();
String subject = "...";
String body = "...";
//調用郵件服務發郵件
}
如上,當新用戶註冊,在註冊完成後,會發一封激活郵件,新增用戶是insert,發郵件獲取用戶是select ,若是master slave存在延遲,有可能在這個時候獲取不到用戶。
對於分佈式服務系統,都會有一些獨立的子服務,好比用戶服務,訂單服務,這些服務經過http或者rpc訪問, 以下是用戶服務下的兩個接口。
userservice.xx.com/user/userinfo post請求,用戶新增或者更新用戶
userservice.xx.com/user/userinfo/1 get請求,用於獲取用戶信息
這是兩個接口服務,一個用於更新用戶數據,一個用戶獲取數據,若是手機APP有一個操做是修改用戶名,在調用更新用戶接口修改用戶名後,緊接着調用獲取用戶信息的接口,兩個請求間隔短,而若是同步延遲,就有可能讀取到髒數據。
對於上面第一種狀況
1. 咱們在能夠在dao的方法中,再加一個參數,好比:
public User getUser(int userId, boolean isMaster)
由
業務層決定要在哪一個庫操做。
2. 根據具體的業務類型,將讀寫標誌位放到線程上下文中。好比對於註冊用戶的操做,能夠在開始處理的時候,在線程上下文中放入一個標誌位master,在全部的dao 方法內,判斷該標誌位,若是是master,則從master讀取。這樣讀寫分離是由具體的
業務場景決定的。
對於上面第二種狀況
1. 簡單的方法,就是在請求參數中再加一個參數,服務端根據參數決定要在哪一個庫操做, 這樣增長了前端的一些工做量。
2. 複雜一點的,能夠在服務端處理,當修改了用戶信息後,能夠在redis或者memcache中新增一條Id記錄,5秒過時,每次請求的時候,先到memcache中判斷一下,對應的id是否存在,若是存在讀master, 不然slave。只是這樣無形之中增長了服務端的開銷。
其實數據延遲沒有那麼嚴重,基本都是秒級的,對於上面第二個場景,可能兩個請求來回,數據就已經同步好。不會出現髒讀的狀況,可是在一些特殊的場景下,好比網絡抖動,新加字段,可能數據同步延遲會變大,此時master slave的數據會出現不一致,而若是業務上出現上面的兩種情景,即insert/update/delete後馬上select,就有可能讀不到或者髒讀。因此具體把讀寫分離放在哪一層,仍是要根據業務類型和實際狀況來決定。