Redis缓存

1.缓存概述

  缓存(Cache)的作用是减少服务器对数据源的访问频率,从而提高数据库的稳定性。

  访问的流程如下。

  

redis缓存教程 redis如何缓存_Redis


redis缓存教程 redis如何缓存_数据_02


 代码逻辑

public Goods searchArticleById(Long goodsId){
	Object object = redisTemplate.opsForValue().get(String.valueOf(goodsId));
	if(object != null){// 缓存查询到了结果
		return (Goods)object;
	}
	// 开始查询数据库
	Goods goods = goodsMapper.selectByPrimaryKey(goodsId);
	if(goods!=null){
		// 将结果保存到缓存中
		redisTemplate.opsForValue().set(String.valueOf(goodsId),goods,60,TimeUnit.MINUTES);;
	}
	return goods;
}

2.缓存方式

    缓存中的数据在redis中的存储方式有两种,一种是永久存在,不设置过期时间,第二种是设置过期时间。
    这两种方式都需要尽可能的保证数据的一致性(和数据源中的数据保持同步)。
  
2.1不设置过期时间

    当我们将缓存数据的key设置为永久存在时会存在数据同步和内存消耗逐渐增大的情况,解决方式如下:
数据同步:

  1. 禁止直接操作数据源,避免因数据源直接被改动而造成缓存数据不一致的问题
  2. 如果有其他系统操作同一个数据源,这种情况肯定会产生数据不一致的情况。
  3. 系统执行DML操作时,应该将缓存中对应的数据删除。用户下一次相关请求时直接从数据源中获取。

内存消耗:

    随着业务的增多,缓存数据必然会越来越多,所占用的内存也随之增多,系统的压力也会变大,

 这时一种方式是给key设置过期时间,但是过期时间长短不太好把握,这时我们可以通过设置redis最大内存来实现,并让Redis按照一定的规则淘汰不需要的缓存键,这种方式在redis只作为缓存使用时非常实用。

    具体实现方式:修改redis配置文件(redis.conf)中的maxmemory参数既可,限制Redis最大可用内存大小(单位字节),当超出了这个限制时Redis会依据maxmemory-policy参数指定的策略来删除不需要的key直到Redis占用的内存小于指定内存。

redis缓存教程 redis如何缓存_缓存_03

规则

说明

volatile-lru

使用LRU算法删除一个key(只对设置了过期时间的key有效)

allkeys-lru

使用LRU算法删除一个key

volatile-lfu

使用LFU算法删除一个key(只对设置了过期时间的key有效)

allkeys-lfu

使用LFU算法删除一个key

volatile-random

随机删除一个key(只对设置了过期时间的key有效)

allkeys-random

随机删除一个key

volatile-ttl

删除过期时间最近的一个key

noeviction

不删除key,只返回错误

   LRU:(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”

   LFU:(Least Frequently Used)算法根据数据的历史访问频率来淘汰数据,其核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”。

设置过期时间

     Redis中有个设置时间过期的功能,就是对存储在Redis 数据库中的值可以设置一个过期时间。
     作为一个缓存数据库,这是非常实用的。
     比如我们一般项目中的token或者一些登录信息,尤其是短信验证码都是有时间限制的,按照传统的数据库处理方式,一般都是自己判断过期,这样无疑会严重影响项目性能。
     我们 set key 的时候,都可以给一个 expire time,就是过期时间,通过过期时间我们可以指定这个 key 可以存活的时间。
     但同样也会遇到问题,比如过期时间怎么设置,内存资源同样也会过大。
     如果假设你设置了一批 key 只能存活1个小时,那么接下来1小时后,Redis是怎么对这批key进行删除的?

内存资源

     同样需要设置maxmemory来限制redis使用的最大内存和配置maxmemory-policy来指定删除策略。
  
过期时间设置
     过期时间不要设置统一固定的时间,比如60分钟,这样会造成相同时间点大量缓存被清空,数据库访问量突然增大的情况,我们应该对过期时间设置合理范围内的随机值。
     比如:采取不同分类商品,缓存不同周期。在同一分类中的商品,加上一个随机因子。这样能尽可能分散缓存过期时间,而且,热门类目(衣服)的商品缓存时间长一些,冷门类目(图书)的商品缓存时间短一些,也能节省缓存服务的资源。

定期删除+惰性删除

     定期删除:Redis默认是每隔 100ms 就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。注意这里是随机抽取的。为什么要随机呢?因为假如 Redis 存了几十万个 key ,每隔100ms就遍历所有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载!

     惰性删除 :定期删除可能会导致很多过期 key 到了时间并没有被删除掉。所以就有了惰性删除。假如你的过期 key,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个 key,才会被Redis给删除掉,这就是所谓的惰性删除。

public Goods searchArticleById(Long goodsId){
	Object object = redisTemplate.opsForValue().get(String.valueOf(goodsId));
	if(object != null){// 缓存查询到了结果
		return (Goods)object;
	}
	// 开始查询数据库
	Goods goods = goodsMapper.selectByPrimaryKey(goodsId);
	if(goods!=null){
		Random random = new Random();
		// 将结果保存到缓存中
		if(goods.getGoodsCategory().equals("衣装")){
			int time = 3600 + random.nextInt(3600);
			// 热门商品
			redisTemplate.opsForValue()
						.set(String.valueOf(goodsId)
							,goods
							,time
							,TimeUnit.MINUTES);
		}else{
			int time = 600 + random.nextInt(600);
			// 冷门商品
			redisTemplate.opsForValue()
						.set(String.valueOf(goodsId)
							,goods
							,time
							,TimeUnit.MINUTES);
		}
	}else{
		// 防止缓存穿透
		redisTemplate.opsForValue()
						.set(String.valueOf(goodsId)
							,null
							,60
							,TimeUnit.MINUTES);
	}
	return goods;
}

3.名称解释

缓存穿透

  缓存穿透,是指查询一个数据库一定不存在的数据。
  如:黑客故意去请求缓存中不存在的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉
  最常见的解决方法是采用布隆过滤器,将所有可能存在的数据hash到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
  
  还有一个更为简单的方法,即使一个查询返回的数据为空,我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
  正常的使用缓存流程大致是,数据查询先进行缓存查询,如果key不存在或者key已经过期,再对数据库进行查询,并把查询到的对象,放进缓存。如果数据库查询对象为空,则不放进缓存。

public Goods searchArticleById(Long goodsId){
	Object object = redisTemplate.opsForValue().get(String.valueOf(goodsId));
	if(object != null){// 缓存查询到了结果
		return (Goods)object;
	}
	// 开始查询数据库
	Goods goods = goodsMapper.selectByPrimaryKey(goodsId);
	if(goods!=null){
		// 将结果保存到缓存中
		redisTemplate.opsForValue().set(String.valueOf(goodsId),goods,60,TimeUnit.MINUTES);
	}else{
redisTemplate.opsForValue().set(String.valueOf(goodsId),null,60,TimeUnit.SECONDS);
	}
	return goods;
}

   采用缓存空值的方式,如果从数据库查询的对象为空,也放入缓存,只是设定的缓存过期时间较短,比如设置为60秒。

缓存雪崩

   缓存雪崩,是指在某一个时间段,缓存集中过期失效,后面的请求全部都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。
  
解决方式:    就是上面设置过期时间中使用的方式,灵活设置过期时间。
      事前:尽量保证整个Redis集群的高可用性,发现机器宕机尽快补上。选择合适的内存淘汰策略
      事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL崩掉
      事后:利用Redis持久化机制保存的数据尽快恢复缓存

处理流程图:

redis缓存教程 redis如何缓存_Redis_04

缓存击穿

   缓存击穿,是指一种key是非常"热点"的数据,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
    缓存击穿和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。
    缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候高并发的请求可能会瞬间把后端DB压垮。
  
解决方式   直接设置为永久key就可以了。

  • 从Redis上看,确实没有设置过期时间,这就保证了,不会出现热点key过期问题,也就是“物理”不过期
  • 从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期
  • 从实战上看,这种方法对于性能非常友好,唯一不足的就是构建缓存时候,其余线程可能访问的是老数据,但是对于一般的互联网功能来说这个还是可以忍受

 mutex key互斥锁可以学习下,但一般情况下用不上!
  
使用互斥锁

      业界比较常用的做法,是使用mutex。
      就是在缓存失效的时候,不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;
      否则,就重试整个get缓存的方法。
  
“提前”使用互斥锁

      在value内部设置1个超时值(timeout1), timeout1比实际的memcache timeout(timeout2)小。
      当从cache读取到timeout1发现它已经过期时候,马上延长timeout1并重新设置到cache。
      然后再从数据库加载数据并设置到cache中。

缓存 “无底洞” 现象

简介

    指的是为了满足业务要求添加了大量缓存节点,但是性能不但没有好转反而下降了的现象。
  
产生原因

    缓存系统通常采用 hash 函数将 key 映射到对应的缓存节点,随着缓存节点数目的增加,键值分布到更多的节上,导致客户端一次批量操作会涉及多次网络操作,这意味着批量操作的耗时会随着节点数目的增加而不断增大。
    此外,网络连接数变多,对节点的性能也有一定影响。
  
解决方案

  1. 优化批量数据操作命令
  2. 减少网络通信次数
  3. 降低接入成本,使用长连接 / 连接池,NIO 等

缓存一致性

简介

    缓存一致性要求数据更新的同时缓存数据也能够实时更新
  
解决方案

  1. 在数据更新的同时立即去更新缓存
  2. 在读缓存之前先判断缓存是否是最新的,如果不是最新的先进行更新
  3. 要保证缓存一致性需要付出很大的代价,缓存数据最好是那些对一致性要求不高的数据,允许缓存数据存在一些脏数据

总结

搞清楚了缓存的这些知识点我们选择就比较清楚了,具体的灵活使用。

  1. 设置Redis最大使用内存是必须的。
  2. 通过不同的策略设置过期时间。
  3. 如果是热点key我们可以直接设置为永久key。