高并发环境下JavaWeb的缓存过期策略
最近公司的几个平台经常在高峰期挂掉,经检查是因为数据库有太多Slow Query导致的,当初也没细想为什么会出现这么多的Slow Query,而且大部分还是相同的查询,单独拿某个Sql查询消耗时间大都在毫秒级别,为了安全起见,对所有Sql又做了一次优化,并且写了监测脚本,定期杀掉太慢的查询,但这样的话还是会影响到有些用户的访问。
网站采用了Spring+SpringJDBC+Servlet+Memcache的架构,数据库是一对Master-Slave的Mysql,有大概几十个接口和4个网站共用这个数据库,采用了proxool数据库连接池。因为数据的实时性,所以CDN和Memcache的缓存过期时间都是在5分钟左右,好了环境介绍完毕,开始着手解决这个问题。
为了模拟高峰期并发环境,使用Apache的ab命令对网站进行压力测试,此时测试环境是没有Cache的,果不其然,log里出现了数据库连接已经占满的异常信息,猜测是在大并发环境下,缓存正好过期,所有的访问都去请求数据库导致连接占满,经过考虑,有了以下解决方案,不足之处请说明。
备注:以下过程中出现的client为Memcache的实例,省略了初始化的过程,所用到的Memcache库为xmemcache1.3.3
1、 增加数据备份,防止缓存过期后同时请求数据库
2、 增加同步机制,保证并发环境下只有一个用户在更新数据
3、 增加数据更新回调接口,当缓存过期后,调用接口更新数据
4、 验证数据正确性,防止在更新pojo类时出现的ClassCastException
定义数据更新回调接口:
1 public interface MemcachedCallback {
2 Object update(Map<String, Object> args);
3 boolean validate(Object data);
4 }
写入缓存时,增加数据备份
1 public static void setCallBack(String keyName, Object object) {
2 if (client == null)
3 return;
4 try {
5 client.set(keyName, MemcachedMgr.DEFAULT_TIMEOUT, object);
9 client.set(keyName + "_OLD", MemcachedMgr.OLD_DATA_TIMEOUT, object);
10 } catch (Exception e) {
11 log.error("Cache set timeout for key" + keyName + " with value: "
12 + object.toString() + " Error: " + e.getMessage());
13 }
14 }
操作线程池更新缓存
1 public class MemcachedPolicy{
2 private static ThreadPoolExecutor pool = new ThreadPoolExecutor(10, 200, 500,
3 TimeUnit.MILLISECONDS,new ArrayBlockingQueue<Runnable>(10),
4 new ThreadPoolExecutor.CallerRunsPolicy());
5 public static synchronized void exec(String key, MemcachedCallback callback, Map<String, Object> args) {
6 //判断当前是否有更新缓存操作
7 if(MemcachedMgr.get(key + "_MPFlag") == null || "0".equals(MemcachedMgr.get(key + "_MPFlag"))) {
8 MemcachedMgr.set(key + "_MPFlag", "1");
9 pool.execute(new PolicyHandler(key, callback, args));
10 }
11 }
12
13 public static synchronized void remove(String memKey) {
14 MemcachedMgr.set(memKey + "_MPFlag", "0");
15 }
16 }
17
18 class PolicyHandler extends Thread{
19 private MemcachedCallback callback;
20 private Map<String, Object> args;
21 private String memKey;
22 public PolicyHandler(String key,MemcachedCallback callback, Map<String, Object> args) {
23 this.memKey = key;
24 this.callback = callback;
25 this.args = args;
26 }
27
28 public void run() {
29 MemcachedMgr.setCallBack(memKey, callback.update(args));//更新数据
30 MemcachedPolicy.remove(memKey);
31 }
32 }
准备工作都已经做好,开始实现具体的策略,MemcacheMgr将只对外提供get方法:
1 public static Object get(String keyName, Map<String, Object> args, MemcachedCallback callback) {
2 if (client == null){ //如果memcache意外重启,则读取数据库(适用于极端情况)
3 return callback.update(args);
4 }
5 try {
6 Object data = client.get(keyName);
7 boolean hasError = false;
8 if(data != null && !callback.validate(data)) { //如果数据校验失败,则更新数据
9 data = null;
10 hasError = true;
11 }
12 if(data == null) {
13 if(!hasError) {
14 data = client.get(keyName + "_OLD");//获取备份缓存
15 }
16 //将缓存更新任务加入线程池队列
17 MemcachedPolicy.exec(keyName, callback, args);
18 if(data == null) { //当memcache重启,备份数据为空的情况下
19 int count = 10;//最多5秒超时
20 while(count > 0) {
21 if((data = client.get(keyName)) != null) {
22 break;
23 }
24 count -- ;
25 Thread.sleep(500);
26 }
27 }
28 }
29 return data;
30 } catch (Exception e) {
31 return null;
32 }
33 }
Memcache的工具类已经重构好了,接下来开始使用吧:
at IndexServlet
1、在Servlet里有一些从request获取到的参数,可以直接通过加final关键词的方式让update里直接使用,不过对于参数的校验还是应该跟数据操作隔离开的。
1 Map<String, Object> args = new HashMap<String, Object>();
2 args.put("page", page);
3 args.put("sort", sort);
4
5 Map<String, Object> data = (Map<String, Object>) MemcachedMgr.get("your key", args, new MemcachedCallback() {
6
7 public Object update(Map<String, Object> args) {
8 Map<String, Object> map = new HashMap<String, Object>();
9 //所有的参数都可以从args拿到
10 //TODO 查询数据库,并将结果存入map
11 return map;
12 }
13
14 public boolean validate(Object data) {
15 Map<String, Object> map = (Map<String, Object>) data;
16 try {
17 //TODO 通过强制类型转换来判断是否有转换错误,或者自定义校验
18 } catch (Exception e) {
19 return false;
20 }
21 return true;
22 }
23 });
24 //TODO request.setAttribute & 转发
算是告一段落,开始压力测试,模拟300个并发测试该接口,数据库只有一个process,而且QPS基本没什么变化。
据同事讲,缓存过期请求击穿数据库这种情况叫“Dogpile”,google了下dogpile,只发现hibernate里自带了DogpilePrevention,百度没有找到相关资料……
采用map存放页面所需所有数据感觉上还是不太好,暂时没想到更好的办法,先这么着吧,如果有好的解决方案,请大家不吝指教。