CODY

CODY 关注TA

一坑未平,一坑起

CODY

CODY

关注TA

一坑未平,一坑起

  •  深圳南山
  • java酱油党
  • 写了59,448字

该文章投稿至Nemo社区   编程综合  板块 复制链接


redis缓存雪崩和穿透

发布于 2017/09/03 23:13 2,898浏览 4回复 5,266

数据准备

订单表: order_detail

CREATE TABLE `order_detail` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `user_code` varchar(10) NOT NULL COMMENT '用户id',
  `name` varchar(40) NOT NULL COMMENT '订单名',
  `price` decimal(10,0) NOT NULL COMMENT '金钱',
  `create_time` datetime DEFAULT '0000-00-00 00:00:00' COMMENT '创建时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=12000308 DEFAULT CHARSET=utf8;

存储过程:  batch_orders

CREATE PROCEDURE `batch_orders`(in max int)
begin
declare start int default 0;
declare i int default 0;
set autocommit = 0;  
 while i < max do
	set i = i + 1;
	insert into order_detail(userId,name,price) 
	values (i%8,concat('订单title-',i%8),i%50);
 end while;
commit;
end

# 创建 100W 数据

call batch_orders(1000000);

jmeter 工具

模拟高并发请求


Druid 监控

druid 是数据库连接池,提供强大的监控和扩展功能



缓存雪崩

场景

概念

当缓存服务器重启或者大量缓存集中在某一个时间段失效,而这这时遇到高并发,数据库操作时间比较长,会给后端系统(比如DB)带来很大压力,甚至DB宕机

解决

在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量

模拟雪崩代码

         /**
	 * 模拟缓存雪崩
	 */
	public BigDecimal noSyn(String userCode) {
		String json = redisService.get(userCode);
		if(StringUtils.isNotEmpty(json)){
			System.err.println("*****************************从redis中读取*****************************" + json);
			return new BigDecimal(json);
		}
		
		BigDecimal countPrice = orderDetailDao.countPrice(userCode);
		System.err.println("----------------从DB中读取----------------" + json);
		if(countPrice != null){
			redisService.set(userCode, countPrice.toString());
			//设置失效时间,这里是模拟 缓存雪崩,有效时间1秒
			redisService.expire(userCode, 1, TimeUnit.SECONDS);
		}

		return countPrice;
	}
	

修改:


/**
	 * 解决缓存雪崩 
	 */
	public BigDecimal syn(String userCode) {
		String json = redisService.get(userCode);
		if(StringUtils.isNotEmpty(json)){
			System.err.println("*****************************从redis中读取*****************************" + json);
			return new BigDecimal(json);
		}
		
		synchronized (userCode) {
			json = redisService.get(userCode);
			if(StringUtils.isNotEmpty(json)){
				System.err.println("*****************************从redis中读取*****************************" + json);
				return new BigDecimal(json);
			}
			
			BigDecimal countPrice = orderDetailDao.countPrice(userCode);
			System.err.println("----------------从DB中读取----------------" + json);
			if(countPrice != null){
				redisService.set(userCode, countPrice.toString());
				//设置失效时间,这里是模拟 缓存雪崩,有效时间1秒
				redisService.expire(userCode, 1, TimeUnit.SECONDS);
			}

			return countPrice;
		}
	}

思考: 为何不同步方法?

               为何同步代码里,还需缓存中读取?

druid 监控分析


 缓存穿透

场景


概念

高并发查询缓存中不存在的数据,每次都会到数据库中查询。大并发时,会导致用户体验差,数据库宕机,系统瘫痪,难以恢复

解决

1. 如果查询数据库也为空,直接设置一个默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库,这种办法最简单粗暴。

·2根据缓存数据Key的规则顾虑不符合规则的key;这种办法只能缓解一部分的压力,过滤和系统无关的查询,但是无法根治。

·3,采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的BitSet中,不存在的数据将会被拦截掉,从而避免了对底层存储系统的查询压力。关于布隆过滤器,详情查看:基于BitSet的布隆过滤器(Bloom Filter)

模拟缓存穿透代码:

        /**
	 * 模拟缓存穿透,缓存雪崩
	 */
	public BigDecimal noBloomFilter(String userCode) {
		
		String json = redisService.get(userCode);
		if(StringUtils.isNotEmpty(json)){
			System.err.println("*****************************从redis中读取*****************************" + json);
			return new BigDecimal(json);
		}
		
		BigDecimal countPrice = orderDetailDao.countPrice(userCode);
		System.err.println("----------------从DB中读取----------------" + json);
		if(countPrice != null){
			redisService.set(userCode, countPrice.toString());
		}

		return countPrice;
	}

修改:

        private BloomFilter<String> bf;   //布隆过滤器
	
	@PostConstruct		//将用户userCod 放到  BloomFilter 中,可将usercode放入缓存,然后从缓存中读取
	private void init(){
		List<String> userCodes = orderDetailDao.getDistinctUserCode();
		bf = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), userCodes.size());
		for(String s : userCodes){
			bf.put(s);
		}
	}
 
         /**
	 * 解决缓存穿透
	 */
	public BigDecimal bloomFilter(String userCode) {
		
		if( !bf.mightContain(userCode)){
			System.err.println("#########################被bloomfilter 过滤#########################");
			return null;
		}
			
		String json = redisService.get(userCode);
		if(StringUtils.isNotEmpty(json)){
			System.err.println("*****************************从redis中读取*****************************" + json);
			return new BigDecimal(json);
		}
		
		BigDecimal countPrice = orderDetailDao.countPrice(userCode);
		System.err.println("----------------从DB中读取----------------" + json);
		if(countPrice != null){
			redisService.set(userCode, countPrice.toString());
		}

		return countPrice;
	}

druid 监控分析


区别

  缓存雪崩 缓存穿透
 产生原因                            高并发下缓存数据失效                                           高并发查询缓存中不存在的key                                                
 发生频率 缓存时效期间 随时发生
 产生场景 业务需求导致的高并发 黑客攻击
 破化效果 DB宕机,系统瘫痪 用户体验差,DB宕机,系统瘫痪,难以恢复
 解决之道 JVM锁或分布式锁 bloomfilter...


代码详见

https://git.oschina.net/codygit/cody-redis-breakdown 

本文标签
 {{tag}}
点了个评