大纲:
高并发场景的解决方案最常用的有两种,一是缓存,二是异步
谈到缓存可能会遇到各种各样的问题:现在介绍最常见的三种并发缓存问题及解决方案
- 缓存穿透
- 缓存击穿
- 缓存雪崩
接下来将一一对以上问题进行描述和解决方案的提供:
缓存穿透:
现象:大量不存在的key查询,越过缓存查询数据库,一些恶意攻击、爬虫等造成大量空命中。注意这里数据库也查询不到
解决方案:采用布隆过滤器或者bigmap,它需要在访问和缓存之间加一层屏障,里面存储数据库目前存储的所有key.
布隆过滤器是一种节省空间的 空间效率型数据结构,较哈希有更好的效率,但是使用时存在false positive(误报率)的问题。也就是说,有可能把不属于这个集合的元素误认为属于这个集合(False positive) , 但不会把属于这个集合的元素误认为不属于这个集合(False Negative)。最优的哈希函数个数参考:
缓存击穿:
现象: 高并发情况下,某一key失效,此时大量的查询数据库的操作
扫描二维码关注公众号,回复:
1015977 查看本文章
解决方案:double-check + 单节点同步锁
引入: 模块方法 + 回调设计模式
// TODO
解决缓存穿透的源代码:
public /*synchronized*/ List<MenuInfo> query(){
// MenuInfoExample example = new MenuInfoExample();
// MenuInfoExample.Criteria criteria = example.createCriteria();
String key = "senvonQueryCache";
String json = memcacheClient.get(key)+"";
if(StringUtils.isNotEmpty(json) && !json.equalsIgnoreCase("null")){
logger.info("cache===========");
return JSON.parseArray(json, MenuInfo.class);
}else{
//线程1
//线程2
synchronized(this){
json = memcacheClient.get(key)+"";
if(StringUtils.isNotEmpty(json) && !json.equalsIgnoreCase("null")){
logger.info("cache===========");
return JSON.parseArray(json, MenuInfo.class);
}
//2s
List<MenuInfo> result = menuInfoDao.selectByPage(new MenuInfoExample() , new Page(1 , 3));
memcacheClient.set(key, JSON.toJSON(result) , DateUtils.addMinutes(new Date(), 1));
return result;
}
}
}
优化:采用模板模式和回调模式来解决,代码书写不需要关注内层业务逻辑(如何从缓存当中取数据,没取到怎么办,等等...) 只需要关注:如果从数据库中取,如何取。
package com.senvon.newApp.service;
import java.util.Date;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.whalin.MemCached.MemCachedClient;
@Repository
public class CacheTemplateService {
@Autowired
private MemCachedClient memcacheClient;
private Logger logger = LoggerFactory.getLogger(getClass());
/**避免缓存穿透的模板
* 读取缓存的模板操作
* @param key 缓存的key
* @param expire 缓存的失效时间
* @param clazz 缓存的类型
* @param loadBack 如果缓存失效,怎么获取
* @return
*/
public <T> T findCache(String key , Date expire , TypeReference<T> clazz , CacheLoadback<T> loadBack){
String json = memcacheClient.get(key)+"";
if(StringUtils.isNotEmpty(json) && !json.equalsIgnoreCase("null")){
logger.info("load cache========{}" , key);
return JSON.parseObject(json, clazz);
}else{
synchronized(this){
json = memcacheClient.get(key)+"";
if(StringUtils.isNotEmpty(json) && !json.equalsIgnoreCase("null")){
logger.info("load cache========{}" , key);
return JSON.parseObject(json, clazz);
}
T result = loadBack.load();
if(result != null){
memcacheClient.set(key, JSON.toJSON(result) , expire);
}
return result;
}
}
}
}
public List<MenuInfo> queryTemplate(){
String key = "senvonQueryCache";
return templateService.findCache(key, DateUtils.addMinutes(new Date(), 1), new TypeReference<List<MenuInfo>>(){}, new CacheLoadback<List<MenuInfo>>(){
@Override
public List<MenuInfo> load() {
return menuInfoDao.selectByPage(new MenuInfoExample() , new Page(1 , 3));
}
});
}
缓存雪崩:
现象:大量的key同时失效
解决方案:区分冷热数据,设置不同的实效时间;加锁,采用阻塞队列保证单线程访问数据库
建议采用第一种方案:冷热数据区分,采用不同的实效时间
针对于每次访问可以采用刷新实效时间的处理方式来解决