开发过程中会有非常频繁地查询某一类对象,尤其是通过主键查询整个对象的情况。比如user,对于前端UI来说,很可能展现任何业务列表的时候,都有相关的用户信息,需要显示用户的昵称、头像之类的,这是就要把列表中涉及的用户一个个查出来。
传统的方式是使用数据库联合查询,但如果用户表很大,和业务表关联查询的代价是很高的,而且如果用户表与业务表不在同一个数据库实例上,就没法联合了。另一种思路就是先查业务表——如果有分页机制的话,通常结果也就是几十条,再针对结果集的每个用户主键,一一查询对应的用户信息。后者的好处是查询压力是可控的,不至于让数据库爆掉;缺点在于对数据库的查询请求还是过于频繁。
在这种情况下,如果用户信息不经常变动,就可以将其缓存起来,每次从缓存中获取数据,从而减轻数据库压力。Java中最简单的内存缓存实现就是用HashMap,以数据库主键为key,整个对象序列化以后的字符串作为value。但HashMap不是线程安全的,并发情况下,可能出现意想不到的错误,所以应该是用concurrent·包中的ConcurrentHashMap类实现。代码示例如下:
package com.nuanxinli.cache;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import com.nuanxinli.bo.instance.User;
@IocBean
public class UserCache {
private ConcurrentMap<String, User> userMap = new ConcurrentHashMap<String, User>();
@Inject("refer:system_dao")
private Dao dao;
public User get(String username) {
User user = userMap.get(username);
if (user == null) {
user = dao.fetch(User.class, username);
userMap.put(username, user);
}
return user;
}
public void updateOneUser(User user) {
userMap.put(user.getUsername(),user);
}
}
从代码逻辑可以看出,调用get方法时,首先从map里查找,如果map中没有,再从数据库里取,并且把结果加入到缓存map中,下次在使用即可直接从map中取到。另外,提供了一个updateOneUser方法,以便在用户信息发生变化时更新缓存。下面是使用UserCache的例子
package com.nuanxinli.logic.livecast;
import java.util.List;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.QueryResult;
import org.nutz.dao.pager.Pager;
import org.nutz.dao.sql.Criteria;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import com.nuanxinli.application.ImHttpException;
import com.nuanxinli.bo.instance.livecast.LiveComment;
import com.nuanxinli.cache.UserCache;
import com.nuanxinli.util.StrUtil;
@IocBean
public class LiveCommentLogic {
@Inject("refer:gold_dao")
private Dao goldDao;
@Inject
private UserCache userCache;
public QueryResult getList(Integer liveId, String type, Integer pageNum, Integer pageSize) {
//查询业务对象列表
Criteria cri = Cnd.cri();
if (StrUtil.isNotNullOrBlank(type)) {
cri.where().and("type", "=", type);
}
cri.where().and("is_deleted", "=", 0).and("live_id", "=", liveId);
Pager pager = goldDao.createPager(pageNum, pageSize);
List<LiveComment> list = goldDao.query(LiveComment.class, cri, pager);
pager.setRecordCount(goldDao.count(LiveComment.class, cri));
//遍历列表,把其中的用户信息补全
for (LiveComment liveComment : list) {
liveComment.setCreateUser(userCache.get(liveComment.getCreator()));
}
return new QueryResult(list, pager);
}
}
这里的代码使用了Nutz的ioc、dao和Json序列化框架。
这个缓存方法当然是简单粗暴的,很多问题都没有考虑到,比如:
- ConcurrentMap的内存使用效率不高,一旦缓存用户量大,会撑爆内存。好在只有被查询到的用户才会被缓存,而大部分应用活跃用户数都远小于总用户数,对内存的压力不算太大。
- Java应用服务器一旦重启,缓存全部丢失
- 如果应用服务器分布式部署,相互之间缓存无法共享
后两个问题,就需要引入独立的集中缓存方案了,后面继续总结。