某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="${mongo3.db}"/>
</bean>
<!-- 读取策略为PRIMARY_PREFERRED,避免主从数据不一致问题 -->
<bean id="mongoTemplate3Primary" class="org.springframework.data.mongodb.core.MongoTemplate">
<constructor-arg ref="mongo3Primary"/>
<constructor-arg name="databaseName" value="${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秒内。上述发布后,主库的读不会很多,基本上还是落到从库。