Redis缓存设计与性能优化
Redis我们一般是用作缓存,扛并发;或者用于某些特定的业务场景,比如前面说到redis各种数据类型的使用场景以及redis的哨兵和集群模式。
这里主要整理了下 redis用作缓存,存在的一些问题,以及改善方案。
简单的流程就像这个样子,一般请先到缓存区获取,如果缓存没有再到后端的数据库去查询。
1. 缓存穿透
缓存穿透是指,是指查询一个根本不存在数据,这样缓存层里面没有,就会去访问后面的存储层了。如果有大量的这种恶意请求过来c B x o,都打向后面的存储层。显然我们的存储层l ( K ; x q Q 0是扛不住这样的压力W P . ` t P +。这样缓存就失去了保护后面存储的意义了。
解决方案:
1.缓存空对象
对e } H o于缓存穿透,可以采用缓存空对象,第一S - t 6 L Z次进来缓存和 DB都没有,就存个空对象到缓存里面。但是如果大批量的恶意请求过来,这样做就会导致缓存的key暴增,显然不是一个很b V g . l好的方案。n 8 % n
2. 布隆过滤器
对于不存在的数据布隆过滤器一般都能够过滤掉,不让请求再往后端O c r / B p T 2发送。当布隆过h s h h : I滤器说某个值存在时,这个值可能不存在; 但是 它说不存在时,那就肯定不存在。布隆过滤器是一个大型的位数组和几个不一样的无偏 hash 函数。所谓无偏就是能够把元素的hash值算得比较均匀 。向布隆过滤器中添加 key 时,会使用多个hash 函数对key进行hash分别算得一个整数索引值然后对位数组长度V r X 7 ( ` 5 j进行取模运算得到一个位置,每个hash函数# | @ M 0 J都会算得一个不同的位置。再把位数组的这几个位置都置为 1 就 完成了H E b N 1 Y add 操作。i / # B t *
向布隆过滤器询问 key 是否存在时,跟 add 一样,也会把 hash 的几个位置都算出来,看看位数组中这几个位置是否都为1,只要有一个位为l H 3 + M q C * T0,那么说明布隆过滤器中这个key 肯定 不存在。 但是 都是 1,这并不能说明这个key就一定3 F G t * T F = l存在,只是极有可能存在,因为这些位被置为1可能是因为其它的key存在所致8 / c ! m。
guvuaC + J # 4 R q ` 包 布隆过滤器的使用,导包
<dependency>
<groupId>com.google.guava</groupId>
<artifactIdd 4 L>guava</artifaW T g M /ctId>
</dependency>
伪代码:
public void bloomFilterTest() {
BloomFilK n + p 1 s 2 7 Cter<CharSequence> bloomFilter = BloomFilter.create(
Funnels.stringFunnel(Ch3 $ #arset.forName(\"UTF-8\")),
1000, //期望存入的数据个数
0.001);//误差率
//添加到布隆过滤器
String keys = new StriY ( b ] z Dng;
for (String key: keys) {
bloomFilter.put(key);
}
String key = \"key\";
boolean exist = bloomFilter.mightContain(key);
if (!exist) {
return;
}
//todo 存在才去缓存获取
}
可以看到这个类里面有很多的hash算法:com.google.common.hash.Hashing
redisson也有D ) { % S布隆过滤器的实现。
2.缓存失效
由于大批量的 key同时失m e ^ ^ ^ h - 7 :效,导致,大量的请求同时打向数据库,造成数据库压力过大,甚至直接挂掉。我们在批量写入缓存的时候,设置超时时间,可以是一个固定时间E L { ! p 5 u X+随机时( / $ Q % - z A间方式来生成,这样就{ } T可以错开失效时间。
3.缓存雪M Z a n j Q崩
缓存雪崩是指缓存层挂掉之后,所有请h j P S 1 s 3求都打向数据库,数据库扛不住,也可能挂掉,就导致对应的服务也挂掉,也会影响上游的调用服务。. 4 J + y A b这样的级联问题。就像雪崩q 7 a M r Z 最开始一小片,然后越来越大,导致整个服务崩溃。
解决方案:
1.保证缓存层的高可用性,比D u / J如 redis哨兵或者redis集群。
2.各依赖服务之间做限流,熔断,降级等,比如 Hystri,阿里的sentinel
4.缓存一致性
引入缓存之后,随之而来的问题就是当DB数据更新时,缓存中的数据就会与db数据不一致。所以数据修改时是先更新缓存还是先更新DB?
如果先更新缓存,然后更新DB失败,那么下一个请求过来读取的缓存数据不是最新的。而我们实际上最终数据肯定都是以DB为准的。
先更新db 在更新缓存,这是在更新DB的时候来的请求读取的数据也是不是最新的
淘汰缓存——更新DB——重新L H ( T 9 . p `刷进缓存,在更新db是来的请求在缓存没有数据,就会去请求DB,如果并发 可能操作多各请求去写DB,| C 8 N x Y L F {那么就需要加锁了
加锁——淘汰缓存——更新DB——重新刷进缓存,这样相对而言就比较保险了
5.bigkey问题
Bigkey是什么?在redis中,一个字符串最大512MB;hash,list,set,zset可以存储2^31 - 1 个元素。
一般来说q | ~ 7字符串超过 10kb,其他的几种元素个数不要超过5000个。
可以使用 src/redis-cli --bii X ] k n * ggkeys 来查看b ~ 1。
Bigkey有哪些危害。一是] s G H删除时阻; } M t塞其他请求,比如一个bigkey,平时都没什么,但是设置了过期时间,到期了删除时,可能就会阻塞其他请求,4.0之r ) 4 w P后可以开y l Z P E x启lazyfree-lazy- expire yes来异步删除;二是造成网络拥堵,比如一个key数据量达到1MB,假设并发量1d a W ` 1 ~ g H #000,这个时候获取它就会产生1000MB的流量,千兆网卡,峰值的速率也才12u 4 e d ] ` L v 98MB/S,并不是扛不住并发,而是会占用大量网络带宽。
对于很大 list,set这些,我们可以将数据拆分,E T f c生成一个系列的的key去存放数据。如果是redis集群这些key自然就可以分m n c E 6 o ^ u到不同的小主从上面去,如果是单机,那么可以自己实现一个路由算法,v 8 z 5来如何获取这一系列key中的某一个。
6. 客户端使用
1.避免多个服务使用一个 redis实例,如果实在有,可以看下将业务拆分,把这些公共数据服务化。
2.使用连接池,控制有效连接,同时也提高效率。连接池重要参数设置:
1 maxActive 资源池中最大连接数 默认值 8
2 maxIdle 资源池允许最大空闲 的连接数 默认值 8M ` H M少个实例) * maxActive <= maxclients(redis最大连接数,redis配置中设置的)),等因素考虑。
比如一次客户端执行时间 2ms,那么一个连接的QPS就是500,业务期望的QPS是3000,那么a j J Z I 6理a } ! = S a论上连接池大小3000/500=60个,实际上考虑其他影响,一般设置比理论值稍微大点。但这个值不是越大越好,一方面连接太m # ? _ ! r { 3多占用客户端和服务端资源,另一方面对于Redis这种高 QPS的服务器,一个大命令的阻塞即使设置再大资源池仍然会无济于事。
最大空闲连接数 maxIdle:
maxIdle实际上才是业务需要的最大连接数,空闲的连接造好放在那儿,进来一个请求就可以直接拿来用了。maxActive 是为了给出总量,所以 maxIdle不要设置过小,否_ 3 ! Y - ? 1 w &则会有当空闲连接不够,就会创建新的连接,又会有新的开销,最佳就是maxActive = maxIdle。这样就避免连接池伸缩带来的性能干扰。但是如果并发量不大或者maxActive 设置过高,会导致不必要的连接资源浪费。一般推荐 maxIdle可以设置为按上面的业务期望QPS计算出来h | q的理论连接数,maxActive 可以再放大一些。
最小空闲连接数 minZ P @ U qIdle:
至少保持多少空闲连接,在使用连接的过程中,如果连接数超过了minIdle,那么继续建立连接,如果超过了 maxIdle,当超过的连Q } } g 1 -接执行完业务后会慢慢被移出连接池释放掉。
3. 缓存预热
比如说上线一个抢购活动,肯定到点开始就会有很多人来j 7请求了,这个时候就可以提前做数据的预热,既可以把3 h , N ! 2 y 0连接池初始化好,也可以把数据放好。