某Java項目是讀多寫少的情況,雖然用緩存格擋了很多讀的請求,但還是會有不少請求落到mongodb庫上。mongodb進行了讀寫分離,寫入往主庫,讀取從從庫,這樣減輕了主庫的壓力。但由於從庫同步數據的延時性,某數據在主庫寫入後馬上從從庫讀,會讀取到舊數據並且會將舊數據塞入了緩存。mongodb寫的操作有相關設置,writeConcern可設置為Replica Acknowledged級別,即數據被寫入到至少兩個從庫節點返回。這樣雖然一定程度上解決從庫延時,但寫的性能大大降低,不可取。項目采用的方案是在剛寫入的一小段時間內都從主庫讀,過了一小段時間後從從庫讀。具體實現是每次寫入時往緩存中標記該條數據的寫入時間,當落入數據庫讀時,先從緩存中取出取出該數據的時間標記,如果標記存在並且離寫入時間很近,則從主庫讀,否則從從庫讀。
mongo 連接spring配置:
<mongo:mongo-client id="mongo3"
replica-set="${mongo3.host}"
port="${mongo3.port}"
credentials="${mongo3.user}:${mongo3.password}@${mongo3.db}">
<mongo:client-options
connections-per-host="200"
connect-timeout="5000"
max-wait-time="5000"
write-concern="ACKNOWLEDGED"
read-preference="SECONDARY_PREFERRED"
socket-keep-alive="true"
socket-timeout="10000"
/>
</mongo:mongo-client>
<mongo:mongo-client id="mongo3Primary"
replica-set="${mongo3.host}"
port="${mongo3.port}"
credentials="${mongo3.user}:${mongo3.password}@${mongo3.db}">
<mongo:client-options
connections-per-host="200"
connect-timeout="5000"
max-wait-time="5000"
write-concern="ACKNOWLEDGED"
read-preference="PRIMARY_PREFERRED"
socket-keep-alive="true"
socket-timeout="10000"
/>
</mongo:mongo-client>
<bean id="mongoTemplate3" class="org.springframework.data.mongodb.core.MongoTemplate">
<constructor-arg ref="mongo3"/>
<constructor-arg name="databaseName" value="https://my.oschina.net/passerman/blog/${mongo3.db}"/>
</bean>
<!-- 讀取策略為PRIMARY_PREFERRED,避免主從數據不一致問題 -->
<bean id="mongoTemplate3Primary" class="org.springframework.data.mongodb.core.MongoTemplate">
<constructor-arg ref="mongo3Primary"/>
<constructor-arg name="databaseName" value="https://my.oschina.net/passerman/blog/${mongo3.db}"/>
</bean>
mongo封裝類部分代碼:
private MongoOperations mongoTemplate;
private CacheService cacheService;
private boolean enableCached = true;
private boolean readFromPrimaryStorage = true;
private MongoTemplate mongoTemplate3Primary;
private int updateSignExprieSecond; // 標記數據更新信息 在緩存中失效時間
/**
* 更新
*/
public int update(Object obj, String collectionName) {
//.....
// 將更新時間存入緩存
putUpdateInfoCache(id, collectionName);
WriteResult result = mongoTemplate.upsert(query, update, collectionName);
int number = result.getN();
// Clear cache after update
removeFromCache(id, collectionName);
return number;
}
/**
* 標記數據更新時間放入緩存
* @param id
* @param collection
*/
private void putUpdateInfoCache(String id, String collection){
if(readFromPrimaryStorage){
if(cacheService == null || !enableCached) {
return;
}
String key = genUpdateInfoCacheKey(id, collection);
if(StringUtil.isEmpty(key)) {
return;
}
long updateTime=system.currentTimeMillis();
cacheService.setObject(key, updateTime, updateSignExprieSecond);
}
}
/**
* 更新信息的緩存key
* @param id
* @param collection
* @return
*/
private String genUpdateInfoCacheKey(String id, String collection) {
if(StringUtil.isEmpty(id)) {
return null;
}
return "monup_" + collection + "_" + id;
}
/**
* 根據id讀
*/
public <T> T findById(String id, Class<? extends T> entityClass,
String collectionName) {
T obj = getFromCache(id, collectionName, entityClass);// Query by id from cache
if(obj != null) {
return obj;
}
Long updateTime=getUpdateTimeFromCache(id, collectionName);
if(updateTime!=null){
// 剛更新讀主庫
obj = mongoTemplate3Primary.findById(id, entityClass, collectionName);
}else{
// 更新過了一段時間 讀從庫
obj = mongoTemplate.findById(id, entityClass, collectionName);
}
// ...
if(obj != null) {
setCacheObject(id, collectionName, obj);
}
return obj;
}
/**
* 從緩存獲取對應記錄的更新時間,如果返回為null,表示更新時間比較久了
* @param id
* @param collection
* @return
*/
private Long getUpdateTimeFromCache(String id, String collection){
if(cacheService == null || !enableCached) {
return null;
}
String key = genUpdateInfoCacheKey(id, collection);
if(StringUtil.isEmpty(key)) {
return null;
}
return cacheService.getObject(key, Long.class);
}
由於memcached有失效時間設置,上述判斷數據最近更新是查看標記存不存在。updateSignExprieSecond的值可以設置幾十秒,從庫同步時間一般都在5秒內。上述發布後,主庫的讀不會很多,基本上還是落到從庫。
Tags: 解決方案 java項目 Replica spring 數據庫
文章來源: