Redis 我们一般是用作缓存, 扛并发; 或者用于某些特定的业务场景, 比如前面说到 Redis 各种数据类型的使用场景以及 Redis 的哨兵和集群模式.
这里主要整理了下 Redis 用作缓存, 存在的一些问题, 以及改善方案.
简单的流程就像这个样子, 一般请先到缓存区获取, 如果缓存没有再到后端的数据库去查询.
1. 缓存穿透
缓存穿透是指, 是指查询一个根本不存在数据, 这样缓存层里面没有, 就会去访问后面的存储层了. 如果有大量的这种恶意请求过来, 都打向后面的存储层. 显然我们的存储层是扛不住这样的压力. 这样缓存就失去了保护后面存储的意义了.
解决方案:
1. 缓存空对象
对于缓存穿透, 可以采用缓存空对象, 第一次进来缓存和 DB 都没有, 就存个空对象到缓存里面. 但是如果大批量的恶意请求过来, 这样做就会导致缓存的 key 暴增, 显然不是一个很好的方案.
2. 布隆过滤器
对于不存在的数据布隆过滤器一般都能够过滤掉, 不让请求再往后端发送. 当布隆过滤器说某个值存在时, 这个值可能不存在; 但是它说不存在时, 那就肯定不存在. 布隆过滤器是一个大型的位数组和几个不一样的无偏 hash 函数. 所谓无偏就是能够把元素的 hash 值算得比较均匀. 向布隆过滤器中添加 key 时, 会使用多个 hash 函数对 key 进行 hash 分别算得一个整数索引值然后对位数组长度进行取模运算得到一个位置, 每个 hash 函数都会算得一个不同的位置. 再把位数组的这几个位置都置为 1 就 完成了 add 操作.
向布隆过滤器询问 key 是否存在时, 跟 add 一样, 也会把 hash 的几个位置都算出来, 看看位数组中这几个位置是否都为 1, 只要有一个位为 0, 那么说明布隆过滤器中这个 key 肯定不存在. 但是都是 1, 这并不能说明这个 key 就一定存在, 只是极有可能存在, 因为这些位被置为 1 可能是因为其它的 key 存在所致.
guvua 包布隆过滤器的使用, 导包
- <dependency>
- <groupId>com.google.guava</groupId>
- <artifactId>guava</artifactId>
- </dependency>
伪代码:
- public void bloomFilterTest() {
- BloomFilter<CharSequence> bloomFilter = BloomFilter.create(
- Funnels.stringFunnel(Charset.forName("UTF-8")),
- 1000, // 期望存入的数据个数
- 0.001);// 误差率
- // 添加到布隆过滤器
- String[] keys = new String[1000];
- 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 也有布隆过滤器的实现.
2. 缓存失效
由于大批量的 key 同时失效, 导致, 大量的请求同时打向数据库, 造成数据库压力过大, 甚至直接挂掉. 我们在批量写入缓存的时候, 设置超时时间, 可以是一个固定时间 + 随机时间方式来生成, 这样就可以错开失效时间.
3. 缓存雪崩
缓存雪崩是指缓存层挂掉之后, 所有请求都打向数据库, 数据库扛不住, 也可能挂掉, 就导致对应的服务也挂掉, 也会影响上游的调用服务. 这样的级联问题. 就像雪崩最开始一小片, 然后越来越大, 导致整个服务崩溃.
解决方案:
1. 保证缓存层的高可用性, 比如 Redis 哨兵或者 Redis 集群.
2. 各依赖服务之间做限流, 熔断, 降级等, 比如 Hystri, 阿里的 sentinel
4. 缓存一致性
引入缓存之后, 随之而来的问题就是当 DB 数据更新时, 缓存中的数据就会与 db 数据不一致. 所以数据修改时是先更新缓存还是先更新 DB?
如果先更新缓存, 然后更新 DB 失败, 那么下一个请求过来读取的缓存数据不是最新的. 而我们实际上最终数据肯定都是以 DB 为准的.
先更新 db 在更新缓存, 这是在更新 DB 的时候来的请求读取的数据也是不是最新的
淘汰缓存 -- 更新 DB-- 重新刷进缓存, 在更新 db 是来的请求在缓存没有数据, 就会去请求 DB, 如果并发 可能操作多各请求去写 DB, 那么就需要加锁了
加锁 -- 淘汰缓存 -- 更新 DB-- 重新刷进缓存, 这样相对而言就比较保险了
5.bigkey 问题
Bigkey 是什么? 在 Redis 中, 一个字符串最大 512MB;hash,list,set,zset 可以存储 2^31 - 1 个元素.
一般来说字符串超过 10kb, 其他的几种元素个数不要超过 5000 个.
可以使用 src/Redis-cli --bigkeys 来查看 bigkey, 我这里设置了一个 30 多 K 的字符串, 看下扫描结果, 扫除了一个字符串类型的 bigkey,4084 字节.
Bigkey 有哪些危害. 一是删除时阻塞其他请求, 比如一个 bigkey, 平时都没什么, 但是设置了过期时间, 到期了删除时, 可能就会阻塞其他请求, 4.0 之后可以开启 lazyfree-lazy- expire yes 来异步删除; 二是造成网络拥堵, 比如一个 key 数据量达到 1MB, 假设并发量 1000, 这个时候获取它就会产生 1000MB 的流量, 千兆网卡, 峰值的速率也才 128MB/S, 并不是扛不住并发, 而是会占用大量网络带宽.
对于很大 list,set 这些, 我们可以将数据拆分, 生成一个系列的的 key 去存放数据. 如果是 Redis 集群这些 key 自然就可以分到不同的小主从上面去, 如果是单机, 那么可以自己实现一个路由算法, 来如何获取这一系列 key 中的某一个.
6. 客户端使用
1. 避免多个服务使用一个 Redis 实例, 如果实在有, 可以看下将业务拆分, 把这些公共数据服务化.
2. 使用连接池, 控制有效连接, 同时也提高效率. 连接池重要参数设置:
1 maxActive 资源池中最大连接数 默认值 8
2 maxIdle 资源池允许最大空闲 的连接数 默认值 8
3 minIdle 资源池确保最少空闲 的连接数 默认值 0
4 blockWhenExhausted 当资源池用尽后, 调用者是否要等待. 只有当为 true 时, 下面的 maxWaitMillis 才会生效, 默认值 true 建议使用默认值
5 maxWaitMillis 当资源池连接用尽后, 调用者的最大等待时间 (单位为毫秒) -1: 表示永不超时 不建议使用默认值
6 testOnBorrow 向资源池借用连接时是否做连接有效性检测 (ping), 无效连接会被移除 默认值 false 业务量很大时候建议 设置为 false(多一次 ping 的开销).
7 testOnReturn 向资源池归还连接时是否做连接有效性检测 (ping), 无效连接会被移除 默认值 false 业务量很大时候建议 设置为 false(多一次 ping 的开销).
8 jmxEnabled 是否开启 jmx 监控, 可用于监控 默认值 true 建议开启, 但应用本身也要开启
前面三个参数相对而言更重要, 单独拎出来再说下:
最大连接数 maxActive:
可以从业务希望的并发量, 客户端执行时间, Redis 资源设置 (应用个数 (集群部署多少个实例) * maxActive <= maxclients(Redis 最大连接数, Redis 配置中设置的)), 等因素考虑.
比如一次客户端执行时间 2ms, 那么一个连接的 QPS 就是 500, 业务期望的 QPS 是 3000, 那么理论上连接池大小 3000/500=60 个, 实际上考虑其他影响, 一般设置比理论值稍微大点. 但这个值不是越大越好, 一方面连接太多占用客户端和服务端资源, 另一方面对 于 Redis 这种高 QPS 的服务器, 一个大命令的阻塞即使设置再大资源池仍然会无济于事.
最大空闲连接数 maxIdle:
maxIdle 实际上才是业务需要的最大连接数, 空闲的连接造好放在那儿, 进来一个请求就可以直接拿来用了. maxActive 是为了给出总量, 所以 maxIdle 不要设置过小, 否则会有当空闲连接不够, 就会创建新的连接, 又会有新的开销, 最佳就是 maxActive = maxIdle. 这样就避免连接池伸缩带来的性能干扰. 但是如果并发量不大或者 maxActive 设置过高, 会导致不必要的连接资源浪费. 一般推荐 maxIdle 可以设置为按上面的业务期望 QPS 计算出来的理论连接数, maxActive 可以再放大一些.
最小空闲连接数 minIdle:
至少保持多少空闲连接, 在使用连接的过程中, 如果连接数超过了 minIdle, 那么继续建立连接, 如果超过了 maxIdle, 当超过的连接执行完业务后会慢慢被移出连接池释放掉.
3. 缓存预热
比如说上线一个抢购活动, 肯定到点开始就会有很多人来请求了, 这个时候就可以提前做数据的预热, 既可以把连接池初始化好, 也可以把数据放好.
来源: https://www.cnblogs.com/nijunyang/p/12587429.html