高并发缓存常见问题及解决方案

大纲:

高并发场景的解决方案最常用的有两种,一是缓存,二是异步
谈到缓存可能会遇到各种各样的问题:现在介绍最常见的三种并发缓存问题及解决方案

  1. 缓存穿透
  2. 缓存击穿
  3. 缓存雪崩

接下来将一一对以上问题进行描述和解决方案的提供:

缓存穿透:

现象:大量不存在的key查询,越过缓存查询数据库,一些恶意攻击、爬虫等造成大量空命中。注意这里数据库也查询不到

解决方案:采用布隆过滤器或者bigmap,它需要在访问和缓存之间加一层屏障,里面存储数据库目前存储的所有key.

BloomFilter

布隆过滤器是一种节省空间的 空间效率型数据结构,较哈希有更好的效率,但是使用时存在false positive(误报率)的问题。也就是说,有可能把不属于这个集合的元素误认为属于这个集合(False positive) , 但不会把属于这个集合的元素误认为不属于这个集合(False Negative)。最优的哈希函数个数参考:

缓存击穿:

现象: 高并发情况下,某一key失效,此时大量的查询数据库的操作

解决方案: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同时失效

解决方案:区分冷热数据,设置不同的实效时间;加锁,采用阻塞队列保证单线程访问数据库

建议采用第一种方案:冷热数据区分,采用不同的实效时间

针对于每次访问可以采用刷新实效时间的处理方式来解决

猜你喜欢

转载自my.oschina.net/LucasZhu/blog/1813043