Redis学习补充与布隆过滤器
一、缓存雪崩
1、介绍
同一时间大面积失效,那一瞬间Redis跟没有一样
2、处理雪崩数据
在批量往Redis存数据的时候,把每个Key的失效时间都加个随机值就好了,这样可以保证数据不会在同一时间大面积失效。
setRedis(Key,value,time + Math.random() * 10000);
设置热点数据永远不过期,有更新操作就更新缓存就好了(比如运维更新了首页商品,那你刷下缓存就完事了,不要设置过期时间),电商首页的数据也可以用这个操作,保险。
二、缓存穿透
1、介绍
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,我们数据库的 id 都是1开始自增上去的,如发起为id值为 -1 的数据或 id 为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大,严重会击垮数据库。
如果不对参数做校验,数据库id都是大于0的,我一直用小于0的参数去请求你,每次都能绕开Redis直接打到数据库,数据库也查不到,每次都这样,并发高点就容易崩掉了。
2、处理穿透数据
缓存穿透我会在接口层增加校验,比如用户鉴权校验,参数做校验,不合法的参数直接代码Return,比如:id 做基础校验,id <=0的直接拦截等。
下面我有使用布隆过滤器解决穿透的案例
三、缓存击穿
缓存雪崩是因为大面积的缓存失效,打崩了DB,而缓存击穿不同的是缓存击穿是指一个Key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个Key在失效的瞬间,持续的大并发就穿破缓存。
2、处理击穿数据
缓存击穿的话,设置热点数据永远不过期。或者加上互斥锁就能搞定
public static String getData(String key) throws InterruptedException {
//从Redis查询数据
String result = getDataByKV(key);
//参数校验
if (StringUtils.isBlank(result)) {
try {
//获得锁
if (reenLock.tryLock()) {
//去数据库查询
result = getDataByDB(key);
//校验
if (StringUtils.isNotBlank(result)) {
//插进缓存
setDataToKV(key, result);
}
} else {
//睡一会再拿
Thread.sleep(100L);
result = getData(key);
}
} finally {
//释放锁
reenLock.unlock();
}
}
return result;
}
四、Redis其他知识
1、Redis为何这么快
- Redis完全是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。
- 采用单线程,避免了不必要的上下文切换和竞争条件,不存在多线程导致的CPU切换,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有死锁问题导致的性能消耗。
2、Redis的持久化策略
Redis的持久化策略有两种:1、RDB:快照形式是直接把内存中的数据保存到一个dump的文件中,定时保存,保存策略。2、AOF:把所有的对Redis的服务器进行修改的命令都存到一个文件里,命令的集合。Redis默认是快照RDB的持久化方式。当Redis重启的时候,它会优先使用AOF文件来还原数据集,因为AOF文件保存的数据集通常比RDB文件所保存的数据集更完整。你甚至可以关闭持久化功能,让数据只在服务器运行时存。
五、布隆过滤器
1、安装
要redis4.0以上才支持
wget http://download.redis.io/releases/redis-4.0.14.tar.gz
tar xzf redis-4.0.14.tar.gz
mv redis-4.0.14 redis
cd redis
make
cd src
make install
#修改redis.conf配置
注释掉bind 127.0.0.1
daemonize yes
#设置密码password为密码
requirepass password
#指定配置文件启动服务
redis-server /home/installed/redis/redis.conf
其他
#修改redis启动端口
vim redis.conf
#修改内容
port 6378
pidfile /var/run/redis_6378.pid
#远程测试连接
redis-cli -h 112.17.192.219 -p 6378 auth 123321
安装布隆过滤器
#下载插件压缩包
wget https://github.com/RedisLabsModules/rebloom/archive/v1.1.1.tar.gz
#解压
tar -zxvf v1.1.1.tar.gz
#编译插件
cd RedisBloom-1.1.1/
make
#编译成功后看到redisbloom.so文件即可
#在redis.conf配置文件中加入如RedisBloom的redisbloom.so文件的地址
loadmodule /home/installed/RedisBloom-1.1.1/rebloom.so
#添加完成后需要重启redis
redis-server /home/installed/redis/redis.conf
测试
127.0.0.1:6379> bf.add users user2
(integer) 1
127.0.0.1:6379> bf.exists users user2
(integer) 1
127.0.0.1:6379> bf.exists users user3
(integer) 0
2、介绍布隆过滤器
布隆过滤器的特点是判断不存在的,则一定不存在;判断存在的,大概率存在,但也有小概率不存在。并且这个概率是可控的,我们可以让这个概率变小或者变高,取决于用户本身的需求。
使用布隆过滤器时,我们只要对布隆过滤器进行初始化,将数据库的数据全都先载入到布隆过滤器中。这样操作后,当一个不存在的id再次进行请求时,在经过过滤器时,过滤器比较id转换成的hash值对应的byte数组位置,立刻就能发现该id不存在,直接返回空即可,速度几乎快到忽略不计。这样即可完美解决缓存穿透的问题。
布隆过滤器由一个 bitSet 和 一组 Hash 函数(算法)组成,是一种空间效率极高的概率型算法和数据结构,主要用来判断一个元素是否在集合中存在。
在初始化时,bitSet 的每一位被初始化为0,同时会定义 Hash 函数,例如有3组 Hash 函数:hash1、hash2、hash3。
将 bitSet 的这3个下标标记为1。
在查找的时候也是根据得到的三个Hash所指向的位置是否为1.
3、使用布隆过滤器BloomFilter解决Redis的缓存穿透问题(方法一)
当一个查询请求过来时,先经过布隆过滤器进行查,如果判断请求查询值存在,则继续查;如果判断请求查询不存在,直接丢弃。
依赖
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>21.0</version>
</dependency>
<!--redis设置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--redis连接池-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
配置文件
server:
port: 8000
spring:
redis:
database:
cache: 15 # cache索引
token: 15 # Token索引
mr: 15 # 病历索引
host: 111.111.112.111 #Redis服务器地址
port: 6379 # Redis服务器连接端口(本地环境端口6378,其他环境端口是6379)
password: 123321 # Redis服务器连接密码(默认为空)
lettuce:
pool:
max-active: 8 # 连接池最大连接数(使用负值表示没有限制)
max-idle: 5 # 连接池中的最大空闲连接
max-wait: -1 # 连接池最大阻塞等待时间(使用负值表示没有限制)
min-idle: 0 # 连接池中的最小空闲连接
timeout: 20000 # 连接超时时间(毫秒)
配置类
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheManager rcm = RedisCacheManager.create(connectionFactory);
return rcm;
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
redisTemplate.setConnectionFactory(factory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new
Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
//序列化设置 ,这样计算是正常显示的数据,也能正常存储和获取
redisTemplate.setKeySerializer(jackson2JsonRedisSerializer);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
return redisTemplate;
}
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
stringRedisTemplate.setConnectionFactory(factory);
return stringRedisTemplate;
}
//初始化布隆过滤器,放入到spring容器里面
@Bean
public BloomFilterHelper<String> initBloomFilterHelper() {
return new BloomFilterHelper<>((Funnel<String>) (from, into) -> into.putString(from, Charsets.UTF_8).putString(from, Charsets.UTF_8), 1000000, 0.01);
}
}
工具类
public class BloomFilterHelper<T> {
private int numHashFunctions;
private int bitSize;
private Funnel<T> funnel;
/**
* @param funnel
* @param expectedInsertions
* @param fpp
*/
public BloomFilterHelper(Funnel<T> funnel, int expectedInsertions, double fpp) {
Preconditions.checkArgument(funnel != null, "funnel不能为空");
this.funnel = funnel;
// 计算bit数组长度
bitSize = optimalNumOfBits(expectedInsertions, fpp);
// 计算hash方法执行次数
numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, bitSize);
}
/**
* @param value
* @return
*/
public int[] murmurHashOffset(T value) {
int[] offset = new int[numHashFunctions];
long hash64 = Hashing.murmur3_128().hashObject(value, funnel).asLong();
int hash1 = (int) hash64;
int hash2 = (int) (hash64 >>> 32);
for (int i = 1; i <= numHashFunctions; i++) {
int nextHash = hash1 + i * hash2;
if (nextHash < 0) {
nextHash = ~nextHash;
}
offset[i - 1] = nextHash % bitSize;
}
return offset;
}
/**
* 计算bit数组长度
*
* @param n
* @param p
* @return
*/
private int optimalNumOfBits(long n, double p) {
if (p == 0) {
// 设定最小期望长度
p = Double.MIN_VALUE;
}
int sizeOfBitArray = (int) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));
return sizeOfBitArray;
}
/**
* 计算hash方法执行次数
*
* @param n
* @param m
* @return
*/
private int optimalNumOfHashFunctions(long n, long m) {
int countOfHash = Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
return countOfHash;
}
}
@Service
public class RedisBloomFilter {
@Autowired
private RedisTemplate redisTemplate;
/**
* 根据给定的布隆过滤器添加值
*/
public <T> void addByBloomFilter(BloomFilterHelper<T> bloomFilterHelper, String key, T value) {
Preconditions.checkArgument(bloomFilterHelper != null, "bloomFilterHelper不能为空");
//就是把我们需要存入的value,通过算法计算出相关需要绑定 1的 bit位 的数组。
int[] offset = bloomFilterHelper.murmurHashOffset(value);
for (int i : offset) {
System.out.println("key : " + key + " " + "value : " + i);
//就是将计算完得到的bit位数组,存入redis里面的bit结构里面,i就是数组内的bit位位置,每个都设置为true。
redisTemplate.opsForValue().setBit(key, i, true);
}
}
/**
* 根据给定的布隆过滤器判断值是否存在
*/
public <T> boolean includeByBloomFilter(BloomFilterHelper<T> bloomFilterHelper, String key, T value) {
Preconditions.checkArgument(bloomFilterHelper != null, "bloomFilterHelper不能为空");
int[] offset = bloomFilterHelper.murmurHashOffset(value);
//循环遍历得到的所有值,判断是否都被标记
for (int i : offset) {
System.out.println("key : " + key + " " + "value : " + i);
if (!redisTemplate.opsForValue().getBit(key, i)) {
return false;
}
}
return true;
}
}
测试类
@RestController
public class BloomFilterController {
@Autowired
RedisBloomFilter redisBloomFilter;
@Autowired
private BloomFilterHelper bloomFilterHelper;
@RequestMapping("/add")
public String addBloomFilter(@RequestParam("orderNum") String orderNum) {
try {
redisBloomFilter.addByBloomFilter(bloomFilterHelper, "bloom", orderNum);
} catch (Exception e) {
e.printStackTrace();
return "添加失败";
}
return "添加成功";
}
@RequestMapping("/check")
public boolean checkBloomFilter(@RequestParam("orderNum") String orderNum) {
boolean result = redisBloomFilter.includeByBloomFilter(bloomFilterHelper, "bloom", orderNum);
return result;
}
}
打印出来的这些就是“sb”产生的Hash,我们逻辑用其标记redis相应的位置。
效果图
4、使用布隆过滤器BloomFilter解决Redis的缓存穿透问题(方法二集合Lua脚本)
这个我个人比较喜欢,前提是redis4.0以上版本。
代码块
@RestController
public class RedisController {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@RequestMapping("/lua/{id}")
public String sendLua(@PathVariable String id) {
//添加key值
String script = "return redis.call('bf.add',KEYS[1],ARGV[1])";
DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>(script, Boolean.class);
Boolean result1 = redisTemplate.execute(redisScript, Collections.singletonList("my_bloom_one"), String.valueOf("user" + id));
System.out.println(result1);
//判断是否存在
String scriptEx = "return redis.call('bf.exists',KEYS[1],ARGV[1])";
DefaultRedisScript<Boolean> redisScript1 = new DefaultRedisScript<>(scriptEx, Boolean.class);
Boolean result2 = redisTemplate.execute(redisScript1, Collections.singletonList("my_bloom_one"), String.valueOf("user" + id));
System.out.println(result2);
return "添加是否成功"+result1+"====================="+"查询是否存在"+result2;
}
}
再次创建时
redis程序中已成功创建
七、其他知识
设置过期时间(第三个参数表示单位为秒)
stringRedisTemplate.expire("baike",10 , TimeUnit.SECONDS);
添加单个元素(redis命令)
BF.ADD newFilter foo
添加 并检查多个元素
//添加已存在的元素返回0
BF.MADD myFilter foo bar baz
检查 过滤器中是否存在该元素
BF.EXISTS newFilter foo
批量检查 过滤器中是否存在该元素
BF.MEXISTS newFilter foo bar bbb
设置过滤器的错误率和储存量
//key :需要设置的键,必须是不存在的键
//error_rate:允许的错误率 0.0001等 默认值:0.01
//size:保存的数组大小,尽可能设大一些,防止不够用。默认值:100
BF.RESERVE <key> <error_rate> <size>