zoukankan      html  css  js  c++  java
  • Redis缓存的设计

    一、Redis的缓存设计不合理会存在的问题

    Redis作为缓存,但是缓存设计的不合理就会有以下的问题:

    • 缓存失效 
    • 缓存穿透
    • 缓存雪崩

    缓存失效

    由于大批量的缓存在同一个时间点失效,可能造成大量请求同时穿透缓存直达数据库,可能造成数据库的压力瞬间增大,甚至数据库挂掉的情况。

    例如:热点缓存在初始化的时候,会有拿出很多的数据,为保证数据的最新特性,一般都会设置一个超时时间;但是当这个超时时间到的时候,数据缓存就会全部失效,造成所有请求压力全部作用到数据库上。

    解决方法在缓存初始化的时候,超时时间设置的不一样。

    伪代码,如下:

    String get(String key) {
    	// 从缓存中获取数据
    	String cacheValue = cache.get(key);
    	
    	// 缓存为空
    	if (StringUtils.isBlank(cacheValue)) {
    		// 从存储中获取
    		String storageValue = storage.get(key);
    		cache.set(key, storageValue);
    		
    		//设置一个过期时间(300到600之间的一个随机数)
    		int expireTime = new Random().nextInt(300) + 300;
    		if (storageValue == null) {
    			cache.expire(key, expireTime);
    		}
    		 return storageValue;
    	 } else {
    		 // 缓存非空
    		 return cacheValue;
    	 }
     }
    

    缓存穿透  

    缓存穿透是指查询一个根本不存在的数据,缓存层不会命中,大量的请求全部落到数据库存储层上,严重时造成数据库挂掉。

    通常是出于容错的考虑,如果从存储层查询不到的不到数据,则不写入到缓存层。

    造成缓存穿透的原因主要有两个:

    (1)自身业务代码或数据出现问题;

    (2)一些恶意攻击、爬虫等造成大量空命中;

    解决方法

    方法一:将空对象缓存到Redis,并设置超时时间;但是若黑客制造了上千万个key,那存储到redis就会占用很大的空间。

    伪代码如下:

    String get(String key) {
    	// 从缓存中获取数据
    	String cacheValue = cache.get(key);
    	// 缓存为空
    	if (StringUtils.isBlank(cacheValue)) {
    		// 从存储中获取
    		String storageValue = storage.get(key);
    		cache.set(key, storageValue);
    		
    		// 如果存储数据为空, 需要设置一个过期时间(300秒)
    		if (storageValue == null) {
    			cache.expire(key, 60 * 5);
    		}
    		return storageValue;
    	} else {
    		// 缓存非空
    		return cacheValue;
    	}
    }
    

    方式二布隆过滤器

       对于恶意攻击,向服务器请求大量不存在的数据造成的缓存穿透,还可以用布隆过滤器先做一次过滤,对于不存在的数据布隆过滤器一般都能够过滤掉,不让请求再往后端发送。当布隆过滤器说某个值存在时,这个值可能不存在;当它说不存在时,那就肯定不存在

      布隆过滤器的底层实际上一个大型的二进制数组(BitArray,即里面只能放0和1,它的里面存储的不是真正的值而是0或1;真正的数据值经过hash函数计算后,得到一个数字n,那么就设置 BitArray[n] = 1 (即数据为n的下标存储的值变为1)。为了防止 hash冲突,可以使用多个 hash函数经过多次计算得到。因为hash冲突的存在,所以说某个值存在时,它可能不存在;当它不存在时,那肯定就不存在。

      布隆过滤器占用的空间很少,效率也高。

    (1)用guvua包自带的布隆过滤器,引入依赖

    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>22.0</version>
    </dependency>

    (2)示例伪代码

    import com.google.common.hash.BloomFilter;
    
    //初始化布隆过滤器
    //1000:期望存入的数据个数,0.001:期望的误差率
    BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.forName("utf‐8")), 1000, 0.001);
    
    //把所有数据存入布隆过滤器
    void init(){
    	for (String key: keys) {
    	    bloomFilter.put(key);
    	}
    }
    
    String get(String key) {
    	// 从布隆过滤器这一级缓存判断下key是否存在
    	Boolean exist = bloomFilter.mightContain(key);
    	if(!exist){
    	        return "";
    	}
    	
    	// 从缓存中获取数据
    	String cacheValue = cache.get(key);
    	// 缓存为空
    	if (StringUtils.isBlank(cacheValue)) {
    		// 从存储中获取
    		String storageValue = storage.get(key);
    		cache.set(key, storageValue);
    		
    		// 如果存储数据为空, 需要设置一个过期时间(300秒)
    		if (storageValue == null) {
    			cache.expire(key, 60 * 5);
    		}
    		return storageValue;
    	} else {
    		// 缓存非空
    		return cacheValue;
    	}
    }
    

     注意此处使用的是单机版的布隆过滤器,实际上 Redisson 也实现了布隆过滤器。

    缓存雪崩

    缓存雪崩指的是缓存层支撑不住或挂掉后,流量全部作用到存储层上,造成存储层也给挂掉。

    解决方法

    (1)保证缓存层服务的高可用,使用 Redis的哨兵或Redis的集群方式;

    (2)依赖隔离组件为后端限流和降级。使用 springCloud 的组件 Hystrix 来限流降级;

    热点缓存key重建

    Redis的缓存层中没有数据,但是在同一时刻获取该数据的请求多达几十W的QPS,造成这么多的请求全部作用到数据库上。

    例如:某一个突发的新闻,但是没有缓存到Redis,但是同一个时刻查看该新闻的人多达几十万,造成这么多的请求全部作用到数据库上,导致数据库挂掉。

    解决方法:并发量较大的时候,可以让一个请求去数据库查询,其他请求等待。(使用分布式锁实现

    伪代码如下:

    String get(String key) {
    	// 从Redis中获取数据
    	String value = redis.get(key);
    	
    	// 如果value为空, 则开始重构缓存
    	if (value == null) {
    	
    		// 只允许一个线程重建缓存, 使用setnx, 并设置过期时间ex
    		String mutexKey = "mutext:key:" + key;
    		if (redis.set(mutexKey, "1", "ex 180", "nx")) {
    			// 从数据源获取数据
    			value = db.get(key);
    // 回写Redis, 并设置过期时间 redis.setex(key, timeout, value);13 // 删除key_mutex redis.delete(mutexKey); } // 其他线程休息50毫秒后重试 else {         Thread.sleep(50);
    get(key); } } return value; }

      

    二、Redis使用的规范

    1、键值的设计

    (1)key的命名

    • 建议】以业务名或数据库名为前缀,用冒号分割;  trade:order:1 
    • 建议】保证语义的前提下,控制 key 的长度(key比较多时占用的内存比较大)
    user:{uid}:friends:messages:{mid} 
    简化为
    u:{uid}:fr:m:{mid}
    

    (2)value 的设计

    • 强制】拒绝 bigkey (防止网卡流量、查询慢)

    在Redis中,一个字符串最大512MB,一个二级数据结构(例如hash、list、set、zset)可以存储大约40亿个(2^32-1)个元素,但实际中如果下面两种情况,我就会认为它是bigkey。

      a. 字符串类型:它的big体现在单个value值很大,一般认为超过10KB就是bigkey。

      b. 非字符串类型:哈希(hash)、列表(list)、集合(set)、有序集合(zset),它们的big体现在元素个数太多,不要超过5000个。

    问题:假如出现了非字符串类型的 bigkey,我们怎么去处理呢?

    解答:非字符串的bigkey,不要使用del删除,使用hscan、sscan、zscan方式渐进式删除,同时要注意防止bigkey过期时间自动删除问题(例如一个200万的zset设置1小时过期,会触发del操作,造成阻塞)

    BigKey的危害

    • 导致Redis的阻塞;
    • 造成网络阻塞;

      bigkey也就意味着每次获取要产生的网络流量较大,假设一个bigkey为1MB,客户端每秒访问量为1000,那么每秒产生1000MB的流量,对于普通的千兆网卡(按照字节算是128MB/s)的服务器来说简直是灭顶之灾,而且一般服务器会采用单机多实例的方式来部署,也就是说一个bigkey可能会对其他实例也造成影响,其后果不堪设想。

      有个bigkey,它安分守己(只执行简单的命令,例如hget、lpop、zscore等),但它设置了过期时间,当它过期后,会被删除,如果没有使用Redis 4.0的过期异步删除(lazyfree-lazyexpire yes),就会存在阻塞Redis的可能性。

    • 建议】选择适合的数据类型。

    例如:实体类型(要合理控制和使用数据结构,但也要注意节省内存和性能之间的平衡)

    反例:
    set user:1:name tom
    set user:1:age 19
    set user:1:favor football
    
    正例:
    hmset user:1 name tom age 19 favor football

    2、命令的使用

    • 【推荐】遍历的需求可以使用hscan、sscan、zscan,来代替 hgetall、lrange、smembers、zrange、sinter等。
    • 【推荐】禁用命令;通过redis的rename机制禁掉命令:keys、flushall、flushdb等。
    • 【推荐】使用批量操作提高效率;但要注意控制一次批量操作的元素个数(例如500以内,实际也和元素字节数有关)。
    原生命令:例如mget、mset。
    非原生命令:可以使用pipeline提高效率。

    注意两者不同:
     a. 原生是原子操作,pipeline是非原子操作;
     b. pipeline可以打包不同的命令,原生做不到;
     c. pipeline需要客户端和服务端同时支持。

    • 【建议】Redis事务功能较弱,不建议过多使用,可以用lua替代。

    3、客户端使用

    • 【推荐】避免多个应用使用一个Redis实例。正例:不相干的业务拆分,公共数据做服务化。
    • 【推荐】使用带有连接池的数据库,可以有效控制连接,同时提高效率。

      连接池参数说明

    参数名 含义 默认值 使用建议
    maxTotal 资源池中最大连接数 8  
    maxIdle 资源池允许最大空闲
    的连接数
    8  
    minIdle 资源池确保最少空闲
    的连接数
    0  
    blockWhenExhausted
    当资源池用尽后,调用者是否要等待。只有当为true时,下面的maxWaitMillis才会生效
    true 建议使用默认值
    maxWaitMillis 当资源池连接用尽后,调用者的最大等待时间(单位为毫秒)
    -1:表示永不超时 不建议使用默认值
    testOnBorrow 向资源池借用连接时是否做连接有效性检测(ping),无效连接会被移除
    false 业务量很大时候建议
    设置为false(多一次
    ping的开销)。
    testOnReturn 向资源池归还连接时是否做连接有效性检测(ping),无效连接会被移除
    false 业务量很大时候建议
    设置为false(多一次
    ping的开销)。
    jmxEnabled 是否开启jmx监控,可用于监控

    true 建议开启,但应用本身也有开启


    (1)maxTotal
    :最大连接数,早期的版本叫 maxActive; 

    设置 maxTotal 的值时,需要考虑以下场景:

    • 业务希望Redis并发量;
    • 客户端执行命令时间;
    • Redis资源:例如 nodes(例如应用个数) * maxTotal 是不能超过redis的配置文件中最大连接数 maxclients。
    • 资源开销:例如虽然希望控制空闲连接(连接池此刻可马上使用的连接),但是不希望因为连接池的频繁释放创建连接造成不必靠开销,例如:

      假设一次Redis命令时间的平均耗时约为1ms,那么一个连接的QPS大约是1000;业务期望的QPS是50000,那么理论上需要的资源池大小是50000 / 1000 = 50个。但事实上这是个理论值,还要考虑到要比理论值预留一些资源,通常来讲maxTotal可以比理论值大一些。但这个值不是越大越好,一方面连接太多占用客户端和服务端资源,另一方面对于Redis这种高QPS的服务器,一个大命令的阻塞即使设置再大资源池仍然会无济于事。
    (2)maxIdle 

      maxIdle实际上才是业务需要的最大连接数,maxTotal是为了给出余量,所以maxIdle不要设置过小,否则会有new Jedis(新连接)开销。
    连接池的最佳性能是maxTotal = maxIdle(maxTotal设置过大就不能一样),这样就避免连接池伸缩带来的性能干扰。但是如果并发量不大或者maxTotal设置过高,会导致不必要的连接资源浪费。一般推荐maxIdle可以设置为按上面的业务期望QPS计算出来的理论连接数,maxTotal可以再放大一倍。

    (3)minIdle

      minIdle(最小空闲连接数),与其说是最小空闲连接数,不如说是"至少需要保持的空闲连接数",在使用连接的过程中,如果连接数超过了minIdle,那么继续建立连接,如果超过了maxIdle,当超过的连接执行完业务后会慢慢被移出连接池释放掉。

     Redis连接池创建的过程

    假如Redis的连接池设置参数:maxTotal=100,maxIdle=50,minIdle=10;

    (1)业务服务启动之后连接池是不会去创建连接的,接着业务系统要去连接池里面拿连接操作Redis,这才会 new Jedis操作,用完之后就会把该连接放入到连接池里面;

    (2)当并发量比较大变为 70 的时候,这个时候连接池里面创建了 70个连接;

    (3)过来一会,Redis的并发量变得比较小了,就会慢慢的去释放连接池中多余的 70 - 50 = 20个连接;默认释放到 maxIdle=50 就可以了;

      如果系统启动完马上就会有很多的请求过来,那么我们可以给redis连接池做预热,比如快速的创建一些redis连接,执行简单命令,类似ping(),快速的将连接池里的空闲连接提升到minIdle的数量。要根据实际系统的QPS和调用redis客户端的规模整体评估每个节点所使用的连接池大小。

    连接池预热实例代码:

    List<Jedis> minIdleJedisList = new ArrayList<Jedis>(jedisPoolConfig.getMinIdle());
    
    for (int i = 0; i < jedisPoolConfig.getMinIdle(); i++) {
    	Jedis jedis = null;
    	try {
    		jedis = pool.getResource();
    		minIdleJedisList.add(jedis);
    		jedis.ping();
    	} 
    	catch (Exception e) {
    		logger.error(e.getMessage(), e);
    	} 
    	finally {
    	    //注意,这里不能马上close将连接还回连接池,否则最后连接池里只会建立1个连接。。
    	    //jedis.close();
    	}
     }
     
     //统一将预热的连接还回连接池
     for (int i = 0; i < jedisPoolConfig.getMinIdle(); i++) {
    	 Jedis jedis = null;
    	 try {
    		 jedis = minIdleJedisList.get(i);
    		 //将连接归还回连接池
    		 jedis.close();
    	 } 
    	 catch (Exception e) {
    		logger.error(e.getMessage(), e);
    	 } 
    	 finally {
    	 }
     }
    

    Redis的过期键的三种清除策略

    • 被动删除:当读/写一个已经过期的key时,会触发惰性删除策略,直接删除掉这个过期key;
    • 主动删除:由于惰性删除策略无法保证冷数据被及时删掉,所以Redis会定期主动淘汰一批已过期的key;
    • 当前已经使用内存超过 maxmemory 限定时,会触发主动清理策略

      Redis一定要根据实际情况设置 maxmemory, 因为若不设置 maxmemory 就会一直使用物理内存,物理内存使用完之后就会去使用磁盘,那么Redis的性能就会急剧的下降。  

      当REDIS运行在主从模式时,只有主结点才会执行被动和主动这两种过期删除策略,然后把删除操作”del key”同步到从结点。

    当前已用内存超过maxmemory限定时,会触发主动清理策略

    默认策略是volatile-lru,即超过最大内存后,在过期键中使用lru算法进行key的剔除,保证不过期数据不被删除,但是可能会出现OOM问题。

    其他策略如下:

    • allkeys-lru:根据LRU算法删除键,不管数据有没有设置超时属性,直到腾出足够空间为止。
    • allkeys-random:随机删除所有键,直到腾出足够空间为止。
    • volatile-random: 随机删除过期键,直到腾出足够空间为止。
    • volatile-ttl:根据键值对象的ttl属性,删除最近将要过期数据。如果没有,回退到noeviction策略。
    • noeviction:不会剔除任何数据,拒绝所有写入操作并返回客户端错误信息"(error)OOM command not allowed when used memory",此时Redis只响应读操作。


  • 相关阅读:
    SQL语句快速入门
    分享一些不错的sql语句
    放弃一键还原GHOST!!使用强大WIN7自带备份
    ZEND快捷方式
    eWebEditor在IE8,IE7下所有按钮无效之解决办法
    MySQL中文乱码解决方案集锦
    A+B Problem II(高精度运算)
    矩形嵌套(动态规划)
    贪心——会场安排
    擅长排列的小明(递归,暴力求解)
  • 原文地址:https://www.cnblogs.com/yufeng218/p/13817958.html
Copyright © 2011-2022 走看看