解答Redis高频面试题
链接:https://blog.csdn.net/qq_50446805/article/details/135648515
目录
Liunx Redis安装布隆(Bloom Filter)过滤器
解答
1.什么是缓存穿透 ? 怎么解决 ?
所谓缓存穿透,就是一个业务请求先查询redis,redis没有这个数据,那么就去查询数据库,但是数据库也没有的情况
正常业务下,一个请求查询到数据后,我们可以将这个数据保存在Redis
之后的请求都可以直接从Redis查询,就不需要再连接数据库了
但是一旦发生上面的穿透现象,仍然需要连接数据库,一旦连接数据库,项目的整体效率就会被影响
如果有恶意的请求,高并发的访问数据库中不存在的数据,严重的,当前服务器可能出现宕机的情况
什么是缓存穿透
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
方案一 布隆过滤器
业务逻辑
解决方案:业界主流解决方案:布隆过滤器
布隆过滤器的使用步骤
1.针对现有所有数据,生成布隆过滤器,保存在Redis中
2.在业务逻辑层,判断Redis之前先检查这个id是否在布隆过滤器中
3.如果布隆过滤器判断这个id不存在,直接返回
4.如果布隆过滤器判断id存在,在进行后面业务执行
思维导图
代码实现
使用布隆过滤器解决缓存穿透,我们在虚拟机中安装的redis是一个特殊版本的Redis,这个版本内置了操作布隆过滤器的lua脚本,支持布隆过滤的方法,我们可以直接使用,实现布隆过滤器。
第一步 添加依赖
<!-- Spring Boot支持Redis编程 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
第二步 在application.yaml文件中添加redis的配置
spring:
redis:
host: 192.168.138.128
port: 6379
password: redis
注意:配置文件一定以application.yaml命名,否则springboot项目扫描不到
第三步 操作布隆过滤器有一个专门的类,实现对布隆过滤器的新增元素,检查元素等方法的实现
package com.example.demo.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
@Component
public class RedisBloomUtils {
@Autowired
private StringRedisTemplate redisTemplate;
private static RedisScript<Boolean> bfreserveScript = new DefaultRedisScript<>("return redis.call('bf.reserve', KEYS[1], ARGV[1], ARGV[2])", Boolean.class);
private static RedisScript<Boolean> bfaddScript = new DefaultRedisScript<>("return redis.call('bf.add', KEYS[1], ARGV[1])", Boolean.class);
private static RedisScript<Boolean> bfexistsScript = new DefaultRedisScript<>("return redis.call('bf.exists', KEYS[1], ARGV[1])", Boolean.class);
private static String bfmaddScript = "return redis.call('bf.madd', KEYS[1], %s)";
private static String bfmexistsScript = "return redis.call('bf.mexists', KEYS[1], %s)";
public Boolean hasBloomFilter(String key){
return redisTemplate.hasKey(key);
}
/**
* 设置错误率和大小(需要在添加元素前调用,若已存在元素,则会报错)
* 错误率越低,需要的空间越大
* @param key
* @param errorRate 错误率,默认0.01
* @param initialSize 默认100,预计放入的元素数量,当实际数量超出这个数值时,误判率会上升,尽量估计一个准确数值再加上一定的冗余空间
* @return
*/
public Boolean bfreserve(String key, double errorRate, int initialSize){
return redisTemplate.execute(bfreserveScript, Arrays.asList(key), String.valueOf(errorRate), String.valueOf(initialSize));
}
/**
* 添加元素
* @param key
* @param value
* @return true表示添加成功,false表示添加失败(存在时会返回false)
*/
public Boolean bfadd(String key, String value){
return redisTemplate.execute(bfaddScript, Arrays.asList(key), value);
}
/**
* 查看元素是否存在(判断为存在时有可能是误判,不存在是一定不存在)
* @param key
* @param value
* @return true表示存在,false表示不存在
*/
public Boolean bfexists(String key, String value){
return redisTemplate.execute(bfexistsScript, Arrays.asList(key), value);
}
/**
* 批量添加元素
* @param key
* @param values
* @return 按序 1表示添加成功,0表示添加失败
*/
public List<Integer> bfmadd(String key, String... values){
return (List<Integer>)redisTemplate.execute(this.generateScript(bfmaddScript, values), Arrays.asList(key), values);
}
/**
* 批量检查元素是否存在(判断为存在时有可能是误判,不存在是一定不存在)
* @param key
* @param values
* @return 按序 1表示存在,0表示不存在
*/
public List<Integer> bfmexists(String key, String... values){
return (List<Integer>)redisTemplate.execute(this.generateScript(bfmexistsScript, values), Arrays.asList(key), values);
}
private RedisScript<List> generateScript(String script, String[] values) {
StringBuilder sb = new StringBuilder();
for(int i = 1; i <= values.length; i ++){
if(i != 1){
sb.append(",");
}
sb.append("ARGV[").append(i).append("]");
}
return new DefaultRedisScript<>(String.format(script, sb.toString()), List.class);
}
public static void main(String[] args) {
}
}
第四步 编写测试类
package com.example.demo;
import com.example.demo.utils.RedisBloomUtils;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDateTime;
@Slf4j
@SpringBootTest
class DemoApplicationTests {
@Autowired
private RedisBloomUtils redisBloomUtils;
@Test
void contextLoads() {
log.info("-----------------" + LocalDateTime.now() + "---------------");
// 定义一个数组,用于将其中的数据保存到布隆过滤器中
String[] colors = {"red", "orange", "yellow", "green", "blue", "black", "pink"};
// 定义布隆过滤器的常量key
final String COLOR_BLOOM = "color_bloom";
// RedisBloomUtils对象可以指定key对应的布隆过滤器的误判率
// bfreserve可以实现布隆过滤器的误判率设置,默认情况下存100个元素有1%的误判率
// redisBloomUtils.bfreserve(COLOR_BLOOM,0.001,10000);
// 下面将colors数组中元素保存到布隆过滤器中
redisBloomUtils.bfmadd(COLOR_BLOOM, colors);
// 下面判断指定元素是否保存在redis的布隆过滤器中
String el = "gray";
// 输出是否在布隆过滤器中
log.info("{}是否在布隆过滤器中:{}", el,
redisBloomUtils.bfexists(COLOR_BLOOM, el));
}
}
优点缺点
优点:内存占用较少,没有多余key
缺点: 实现复杂 存在误判可能
布隆过滤器介绍
什么是布隆过滤器
布隆过滤器能够实现使用较少的空间来高效判断一个指定的元素是否包含在一个集合中
布隆过滤器并不保存这些数据,所以只能判断是否存在,而并不能取出该元素
使用情景:凡是判断一个元素是否在一个集合中的操作,都可以使用它
布隆过滤器常见使用场景
-
idea中编写代码,一个单词是否包含在正确拼写的词库中(拼写不正确划绿线的提示)
-
公安系统,根据身份证号\人脸信息,判断该人是否在追逃名单中
-
爬虫检查一个网址是否被爬取过
......
为什么使用布隆过滤器
常规的检查一个元素是否在一个集合中的思路是遍历集合,判断元素是否相等
这样的查询效率非常低下
要保证快速确定一个元素是否在一个集合中,我们可以使用HashMap
因为HashMap内部的散列机制,保证更快更高效的找到元素
所以当数据量较小时,用HashMap或HashSet保存对象然后使用它来判定元素是否存在是不错的选择
但是如果数据量太大,每个元素都要生成哈希值来保存,我们也要依靠哈希值来判定是否存在,一般情况下,我们为了保证尽量少的哈希值冲突需要8字节哈希值做保存
long取值范围:-9223372036854775808-----9223372036854775807
5亿条数据 每条8字节计算后结果为需要3.72G内存,随着数据数量增长,占用内存数字可能更大
所以Hash散列或类似算法可以保证高效判断元素是否存在,但是消耗内存较多
所以我们使用布隆过滤器实现,高效判断是否存在的同时,还能节省内存的效果
但是布隆过滤器的算法天生会有误判情况,需要能够容忍,才能使用
布隆过滤器原理
-
巴顿.布隆于⼀九七零年提出
-
⼀个很长的⼆进制向量(位数组)
-
⼀系列随机函数 (哈希)
-
空间效率和查询效率⾼(又小又快)
-
有⼀定的误判率(哈希表是精确匹配)
如果我们向布隆过滤器中保存一个单词
semlinker
我们使用3个hash算法,找到布隆过滤器的位置
算法1:semlinker--> 2
算法2:semlinker--> 4
算法3:semlinker--> 6
会在布隆过滤器中产生如下影响
假设要查询 "Good" 这个单词在不在布隆过滤器中
算法1:Good-->7
算法2:Good-->3
算法3:Good-->6
我们判断Good单词生成的3,6,7三个位置,只要有一个位置是0
就表示当前集合中没有Good这个单词
一个布隆过滤器中不可能只存一个单词,一般布隆过滤器都是保存大量数据的
如果有新的元素保存在布隆过滤器中
kakuqo
算法1:kakuqo-->3
算法2:kakuqo-->4
算法3:kakuqo-->7
新的单词生成3,4,7三个位置
那么现在这个布隆过滤器中2,3,4,6,7都是1了
假如现在有单词bad,判断是否在布隆过滤器中
算法1:bad-->2
算法2:bad-->3
算法3:bad-->6
判断布隆过滤器2,3,6都是1,所以布隆过滤器会认为bad是存在于这个集合中的
误判就是这样产生的
布隆过滤器误判的效果:
-
布隆过滤器判断不存在的,一定不在集合中
-
布隆过滤器判断存在的,有可能不在集合中
过短的布隆过滤器如果保存了很多的数据,可能造成二进制位置值都是1的情况,一旦发送这种情况,布隆过滤器就会判断任何元素都在当前集合中,布隆过滤器也就失效了
所以我们要给布隆过滤器一个合适的大小才能让它更好的为程序服务
-
优点
空间效率和查询效率⾼
-
缺点
-
有⼀定误判率即可(可以控制在可接受范围内)。
-
删除元素困难(不能将该元素hash算法结果位置修改为0,因为可能会影响其他元素)
-
极端情况下,如果布隆过滤器所有位置都是1,那么任何元素都会被判断为存在于集合中
-
布隆过滤器不能保存集合的元素值
-
设计布隆过滤器
我们在启动布隆过滤器时,需要给它分配一个合理大小的内存
这个大小应该满足
1.内存占用在一个可接受范围
2.不能有太高的误判率(<1%)
内存约节省,误判率越高
内存越大,误判率越低
数学家已经给我们了公式计算误判率
上面是根据误判率计算布隆过滤器长度的公式
n 是已经添加元素的数量;
k 哈希的次数;
m 布隆过滤器的长度(位数的大小)
Pfp计算结果就是误判率
如果我们已经确定可接受的误判率,想计算需要多少位布隆过滤器的长度
布隆过滤器计算器
Liunx Redis安装布隆(Bloom Filter)过滤器
布隆过滤器(Bloom Filter)是 Redis 4.0 版本提供的新功能,最低redis版本为4.0;Linux下redis中添加布隆过滤器常见,网上也有很多方法,但是windows下我找了好久都没有找到合适的案例,因此使用了在docker中安装redisbloom。
安装参照
【Linux】Docker安装Redis之布隆过滤器-CSDN博客
方案二 缓存空对象
业务逻辑
1.从redis中查询信息 2.判断是否存在redis中 3.存在,直接返回 判断命中是否是空值"" 4.不存在,根据id查询数据库 5.数据库中不存在,返回错误 6.数据库中存在写入redis中
思维导图
代码实现
shop实体类
package com.example.demo.entity;
import lombok.Data;
@Data
public class Shop {
private String name;
private String price;
}
RedisTestController
package com.example.demo.controller;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.example.demo.entity.Shop;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
@RestController
public class RedisTestController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 缓存空值解决缓存穿透
*
* @param id
* @return
*/
@PostMapping("/setNullCache")
public Object cachePenetration(@RequestBody String id) {
String key = "CachePenetration" + id;
//1.从redis中查询信息
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
//3.存在,直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return shop;
}
//判断命中是否是空值""
if (shopJson != null) {//等价于 if ("".equals(shopJson))
//返回一个错误信息
return "店铺信息不存在";
}
//4.不存在,根据id查询数据库
Shop shop = null;//模拟从数据库中取值取到空值
if ("1".equals(id)) {
shop = new Shop();
shop.setName("苹果");
shop.setPrice("4999");
}
//5.不存在,返回错误
if (shop == null) {
//将空值缓存到redis
stringRedisTemplate.opsForValue().set(key, "", 2L, TimeUnit.MINUTES);
//返回错误信息
return "店铺不存在!";
}
//6.存在写入redis中
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), 5, TimeUnit.MINUTES);
//7.返回
return shop;
}
}
测试
优点缺点
优点:实现简单,维护方便 缺点: 额外的内存消耗 可能造成短期的不一致
其他方案
1.增强id的复杂度
2.避免被猜测id规律
3.做好数据的基础格式校验
4.加强用户权限校验
5.做好热点参数的限流