二、springboot中實現讀寫分離
2.2、在代碼層面動態切換
一、讀寫分離相關的理論
1.1、ReadPreference讀偏好
在副本集Replica Set中才涉及到ReadPreference的設置,默認情況下,讀寫都是分發都Primary節點執行,但是對於寫少讀多的情況,我們希望進行讀寫分離來分攤壓力,所以希望使用Secondary節點來進行讀取,Primary只承擔寫的責任(實際上寫只能分發到Primary節點,不可修改)。
MongoDB有5種ReadPreference模式:
-
primary: 主節點,默認模式,讀操作只在主節點,如果主節點不可用,報錯或者拋出異常。
-
primaryPreferred:首選主節點,大多情況下讀操作在主節點,如果主節點不可用,如故障轉移,讀操作在從節點。
-
secondary:從節點,讀操作只在從節點, 如果從節點不可用,報錯或者拋出異常。
-
secondaryPreferred:首選從節點,大多情況下讀操作在從節點,特殊情況(如單主節點架構)讀操作在主節點。
-
nearest:最鄰近節點,讀操作在最鄰近的成員,可能是主節點或者從節點。
1.2臟數據
其實說的就是 MongoDB 的數據持久化,在一個數據寫到 journal 並 flush 到磁盤上之前,數據都是臟的,而在復制集內,數據會通過 Oplog 傳播到其它節點上,然后重復寫入的步驟。
假如這個過程中,主節點掛掉了,之前的某一個 Secondary 提升成為了 Primary,由於數據沒有寫到大部分節點上,於是新的 Primary 看不到之前的應該寫入的新數據,即使這時候舊的 Primary 回來了,它也只能是 Secondary,它之前的那些新數據就會丟失,從而導致數據的回滾。
1.3復制集的缺點
說了優點之后,再說說它的缺點,畢竟 CAP 原理還是統治着分布式領域。在 CAP 原理中,C 表示一致性,A 表示一致性,P 表示分區容忍性。
MongoDB 的默認復制集配置是顯然的 CP,因為 ReadPreference 默認為 Primary
;如果換成 Secondary
或者 SecondaryPreferred
,就相當於 AP 了,C 用了業界默認的最終一致性,因為它的復制是基於 Oplog 的異步方案。
但是,AP 方案容易導致的問題有復制延遲導致的:
注意:這些的例子只是隨便舉例,不一定會是真實情況。
- 寫后讀,或者說是讀己寫問題:即從 Primary 寫入數據后,然后馬上從 Secondary 讀,這時候由於延遲問題而有可能在 Secondary 讀不到最新數據,於是我剛發了個微博,刷新了下反而消失了,過一會兒又出現了;
- 單調讀問題,或者說是時光倒流問題:這時候由於多次從不同的 Secondary 讀取數據,比如微博的評論下面,如果兩次讀到的數據不一致后,容易導致先看到了回復,刷新后卻消失了,再過一會兒又出現了;
- 因果讀寫不一致問題:與上面的微博例子相似,即出現在一個微博下面,評論的回復比評論先到達的現象;
解決的辦法顯然是有的,MongoDB 分別從讀與寫提供了解決方案,讓你能夠調整配置來取舍復制集中的 C 與 A。
1.4讀隔離 Read Concern
目前一共有五種讀隔離的設置:
- local:不保證數據都被寫入了大部分節點,我們在使用的時候基本默認的選項;
- available:3.6 版本引入,與 因果一致性會話 有關,也是不保證數據都被寫入了大部分節點,暫時還沒用過;
- majority:保證數據都被寫入了大部分節點,但是必須使用 WiredTiger 存儲引擎;
- linearizable:這個也沒有用過,意思也不是很清楚,文檔大致意思理解為對文檔所有的讀寫都是順序,或者說線性執行的,會導致花費時間超過 majority,建議與 maxTimeMS 一起食用;
- snapshot:4.0 版本引入,與多文檔的事務有關,也是沒用過;
所以除了 local 與 majority,我都不能保證敘述的准確性,畢竟與實際用還是有區別的。但是基本上可以了解到:讀隔離的效果是需要用時間去交換的,或者說降低可用性去交換的。
另外特別提一下這句文檔中的話:
Regardless of the read concern level, the most recent data on a node may not reflect the most recent version of the data in the system.
不管 Read concern 的具體配置,節點上最新的數據,不一定意味着它也是系統中最新的數據。
因為不管 Read concern 如何配置,它始終是從單個節點讀的,這個設計的初衷只能保證不讀到臟數據。
1.5寫確認 Write Concern
{ w: <value>, j: <boolean>, wtimeout: <number> }
對於 w 參數,則有三種,表示寫入后得到多少個 Secondary 的確認后再返回:這三個參數,在進行寫操作的時候非常有用,常見的設置便是將 j
設置為 true
,表示等數據已經寫入了磁盤上的 journal 后再返回,這時候即便數據庫掛掉,也是能從 journal 中恢復的,注意這不是 oplog 它是高層次的日志,而 journal 是低層次的日志,是可以用來故障恢復后重建當前節點數據的日志5。
- 數字:那就是確切的個數了;
- majority:自動幫你計算 n/2 + 1;
- tag set,標簽組:即制定哪幾個 tag 的 Secondary;
最后一個 wtimeout
,則是在制定 w
參數的時候,推薦一並設置,防止超時,畢竟這種確認是犧牲性能的,很可能導致超時。
看到這里,大致可以得出結論,MongoDB 將讀隔離與寫確認交給客戶端去取舍,一定程度上解決了復制延遲導致的業務問題,而本質上,這種解決方案的原理就在於用事務6
------------------------------------------------------------------------------------------------------
readConcern 的是為了在於解決臟讀問題,用戶從 MongoDB 的 primary 上讀的數據並沒有同步到大多數節點,然后 primary 宕機恢復, primary節點會將未同步到大多數節點的數據回滾,導致用戶讀到了臟數據。
當指定 readConcern 級別為majority ,能保證用戶讀到的數據已經寫入到大多數節點,而這樣的數據肯定不會發生回滾,避免了臟讀的問題。
需要注意的是,readConcern 只是保證讀到的數據不會發生回滾,但並不能保證讀到的數據最新。
參考官網:
誤區: majority並非從多節點讀取,依然是單節點讀取。
readConcern 原理
snapshot 0,1,2,3......N的狀態是committed/uncommitted
同步到大多數節點時,對應的snapshot會標記為commmited。
用戶讀取:讀最新的 commited 狀態的 snapshot,這樣就保證了讀到的數據是已經同步到大多數節點。
secondary節點在自身oplog發生變化會同步信息到primary。
primary節點統計超過半數的節點的同步信息就修改該snapshot為uncommitted->commited。
同時secondary拉取oplog的同時從primary節點得到最新一條已經同步到大多數節點的oplog,更新自身的 snapshot 狀態。
------------------------------------------------------------------------------------------------------
-----------------------------------------------------
mongodb 的讀寫一致性由 WriteConcern 和 ReadConcern 兩個參數保證。
兩者組合可以得到不同的一致性等級。
指定 writeConcern:majority 可以保證寫入數據不丟失,不會因選舉新主節點而被回滾掉。
readConcern:majority + writeConcern:majority 可以保證強一致性的讀
readConcern:local + writeConcern:majority 可以保證最終一致性的讀
mongodb 對configServer全部指定writeConcern:majority 的寫入方式,因此元數據可以保證不丟失。
對 configServer 的讀指定了 ReadPreference:PrimaryOnly 的方式,在 CAP 中舍棄了A與P得到了元數據的強一致性讀。
---------------------------------------------------
二、springboot中的MongoDB讀寫分離實現
2.1 MongoDB連接池指定讀模式
再重申下在副本集Replica Set中才涉及到ReadPreference的設置才有意義。
連接池的配置中主要注意幾個參數:
// 客戶端配置(連接數、副本集群驗證) MongoClientOptions.Builder builder = new MongoClientOptions.Builder(); //... builder.readPreference(ReadPreference.secondaryPreferred()); builder.readConcern(ReadConcern.MAJORITY); //... MongoClientOptions mongoClientOptions = builder.build();
xml示例(沒有測試過):
<!-- mongodb配置 --> <mongo:mongo id="mongo" host="${mongo.host}" port="${mongo.port}" write-concern="NORMAL" > <mongo:options connections-per-host="${mongo.connectionsPerHost}" threads-allowed-to-block-for-connection-multiplier="${mongo.threadsAllowedToBlockForConnectionMultiplier}" connect-timeout="${mongo.connectTimeout}" max-wait-time="${mongo.maxWaitTime}" auto-connect-retry="${mongo.autoConnectRetry}" socket-keep-alive="${mongo.socketKeepAlive}" socket-timeout="${mongo.socketTimeout}" slave-ok="${mongo.slaveOk}" write-number="1" write-timeout="0" write-fsync="false" /> </mongo:mongo> <!-- mongo的工廠,通過它來取得mongo實例,dbname為mongodb的數據庫名,沒有的話會自動創建 --> <mongo:db-factory id="mongoDbFactory" dbname="uba" mongo-ref="mongo" /> <!-- 讀寫分離級別配置 --> <!-- 首選主節點,大多情況下讀操作在主節點,如果主節點不可用,如故障轉移,讀操作在從節點。 --> <bean id="primaryPreferredReadPreference" class="com.mongodb.TaggableReadPreference.PrimaryPreferredReadPreference" /> <!-- 最鄰近節點,讀操作在最鄰近的成員,可能是主節點或者從節點。 --> <bean id="nearestReadPreference" class="com.mongodb.TaggableReadPreference.NearestReadPreference" /> <!-- 從節點,讀操作只在從節點, 如果從節點不可用,報錯或者拋出異常。存在的問題是secondary節點的數據會比primary節點數據舊。 --> <bean id="secondaryReadPreference" class="com.mongodb.TaggableReadPreference.SecondaryReadPreference" /> <!-- 優先從secondary節點進行讀取操作,secondary節點不可用時從主節點讀取數據 --> <bean id="secondaryPreferredReadPreference" class="com.mongodb.TaggableReadPreference.SecondaryPreferredReadPreference" /> <!-- mongodb的主要操作對象,所有對mongodb的增刪改查的操作都是通過它完成 --> <bean id="mongoTemplate" class="org.springframework.data.mongodb.core.MongoTemplate"> <constructor-arg name="mongoDbFactory" ref="mongoDbFactory" /> <property name="readPreference" ref="primaryPreferredReadPreference" /> </bean>
對應的配置(在建立mongoDB的連接時,指定ReadPreference)
請仔細看好 spring.data.mongodb.uri 的配置,他的格式如下,可以參考mongodb連接:
mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]
例子:
# MongoDB URI配置 重要,添加了用戶名和密碼驗證 spring.data.mongodb.uri=mongodb://zhuyu:zhuyu@192.168.68.138:27017,192.168.68.137:27017,192.168.68.139:27017/ai?slaveOk=true&replicaSet=zypcy&write=1&readPreference=secondaryPreferred&connectTimeoutMS=300000 #每個主機的連接數 spring.data.mongodb.connections-per-host=50 #線程隊列數,它以上面connectionsPerHost值相乘的結果就是線程隊列最大值 spring.data.mongodb.threads-allowed-to-block-for-connection-multiplier=50 spring.data.mongodb.connect-timeout=5000 spring.data.mongodb.socket-timeout=3000 spring.data.mongodb.max-wait-time=1500 #控制是否在一個連接時,系統會自動重試 spring.data.mongodb.auto-connect-retry=true spring.data.mongodb.socket-keep-alive=true
驗證讀寫分離是否生效:
創建一個 Rest風格的 IndexController ,提供:添加與查詢接口,訪問這2個接口,看控制台輸出,是否查操作自動分配到從庫,寫操作分配到主庫
@RequestMapping("/index") @RestController public class IndexController { @Autowired private MongoTemplate mongoTemplate; @RequestMapping("/getList") public List<TestModel> getList(){ List<TestModel> list = mongoTemplate.findAll(TestModel.class,"test"); return list; } @RequestMapping("/add") public String add(){ TestModel model = new TestModel("zhuyu" + System.currentTimeMillis()); mongoTemplate.insert(model , "test"); return "success"; } }
2.2、在代碼層面動態切換
通過mongoTemplate對象動態指定 mongoTemplate.setReadPreference(readPreference);
例如,在同一個應用中定義2個mongoTemplate對象,一個設置從primary讀,一個設置從Secondary讀,根據應用場景選擇不同的mongoTemplate
三、MongoDB讀寫分離驗證
調整優先級的方法1:
改優先級,登錄指定shard主節點,mongo ip:22001 -u root --password=xxxx --authenticationDatabase admin
1. 先刪除節點,rs.remove("ip1:22002")
2. 再添加回節點,指定優先級
rs.add({
_id: 0,
host: "ip1:22002",
priority: 5
})
3. 執行rs.reconfig()使配置生效
rs.add({
_id: 0,
host: "ip1:22002",
priority: 5
})
調整優先級的方法2:
分別進行讀/寫的場景壓測,看服務器資源的消耗情況就知道讀寫分離是否生效了。
轉自:
https://blog.csdn.net/zhuyu19911016520/article/details/82998162?depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-3&utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-3
https://blog.xizhibei.me/2019/05/05/mongodb-replica-set/
https://blog.csdn.net/cxu123321/article/details/108897067