看完这篇Redis缓存三大问题,够你和面试官battle几回合了
日常的开发中,无不都是使用数据库来进行数据的存储,由于一般的系统任务中通常不会存在高并发的情况,所以这样看起来并没有什么问题。
Redis
技术就是
NoSQL
技术中的一种。
Redis
缓存的使用,极大的提升了应用程序的性能和效率,特别是数据查询方面。
缓存穿透
-
缓存空对象:代码维护较简单,但是效果不好。 -
布隆过滤器:代码维护复杂,效果很好。
缓存空对象
缓存空对象的实现代码如下:
public class UserServiceImpl {
@Autowired
UserDAO userDAO;
@Autowired
RedisCache redisCache;
public User findUser(Integer id) {
Object object = redisCache.get(Integer.toString(id));
// 缓存中存在,直接返回
if(object != null) {
// 检验该对象是否为缓存空对象,是则直接返回null
if(object instanceof NullValueResultDO) {
return null;
}
return (User)object;
} else {
// 缓存中不存在,查询数据库
User user = userDAO.getUser(id);
// 存入缓存
if(user != null) {
redisCache.put(Integer.toString(id),user);
} else {
// 将空对象存进缓存
redisCache.put(Integer.toString(id), new NullValueResultDO());
}
return user;
}
}
}
// 在缓存的时候,添加多一个该空对象的过期时间60秒
redisCache.put(Integer.toString(id), new NullValueResultDO(),60);
布隆过滤器
-
一个非常大的二进制位数组 (数组里只有0和1) -
若干个哈希函数 -
空间效率和查询效率高 -
不存在漏报(False Negative):某个元素在某个集合中,肯定能报出来。 -
可能存在误报(False Positive):某个元素不在某个集合中,可能也被爆出来。 -
不提供删除方法,代码维护困难。 -
位数组初始化都为0,它不存元素的具体值,当元素经过哈希函数哈希后的值(也就是数组下标)对应的数组位置值改为1。
可能很多读者看完上面的特点和原理图,还是看不懂,别急下面通过图解一步一步的讲解布隆过滤器,总而言之一句简单的话概括就是布隆过滤器是一个很大二进制的位数组,数组里面只存0和1。
以上只是画了布隆过滤器的很小很小的一部分,实际布隆过滤器是非常大的数组(这里的大是指它的长度大,并不是指它所占的内存空间大)。
当再次进行存入第二个值的时候,修改后的结果的原理图如下:
所以每次存入一个数据,就会哈希函数的计算,计算的结果就会作为下标,在布隆过滤器中有多少个哈希函数就会计算出多少个下标,布隆过滤器插入的流程如下:
-
将要添加的元素给m个哈希函数 -
得到对应于位数组上的m个位置 -
将这m个位置设为1
当我们要查询的时候,比如查询a这个数,实际中a这个数是不存在布隆过滤器中的,经过2个哈希函数计算后得到a的哈希值分别为2和13,结构原理图如下:
经过查询后,发现2和13位置所存储的值都为1,但是2和13的下标分别是x和z经过计算后的下标位置的修改,该布隆过滤器中实际不存在a,那么布隆过滤器就会误判改值可能存在,因为布隆过滤器不存元素值,所以存在误判率。
-
布隆过滤器大小:越大,误判率就越小,所以说布隆过滤器一般长度都是非常大的。 -
哈希函数的个数:哈希函数的个数越多,那么误判率就越小。
当你删除z元素之后,将对应的下标10和13设置为0,这样导致x和y元素的下标受到影响,导致数据的判断不准确,所以直接不提供删除元素的api。
-
若干哈希函数 -
存值的Api -
判断值得Api
public class MyBloomFilter {
// 布隆过滤器长度
private static final int SIZE = 2 << 10;
// 模拟实现不同的哈希函数
private static final int[] num= new int[] {5, 19, 23, 31,47, 71};
// 初始化位数组
private BitSet bits = new BitSet(SIZE);
// 用于存储哈希函数
private MyHash[] function = new MyHash[num.length];
// 初始化哈希函数
public MyBloomFilter() {
for (int i = 0; i < num.length; i++) {
function [i] = new MyHash(SIZE, num[i]);
}
}
// 存值Api
public void add(String value) {
// 对存入得值进行哈希计算
for (MyHash f: function) {
// 将为数组对应的哈希下标得位置得值改为1
bits.set(f.hash(value), true);
}
}
// 判断是否存在该值得Api
public boolean contains(String value) {
if (value == null) {
return false;
}
boolean result= true;
for (MyHash f : func) {
result= result&& bits.get(f.hash(value));
}
return result;
}
}
public static class MyHash {
private int cap;
private int seed;
// 初始化数据
public MyHash(int cap, int seed) {
this.cap = cap;
this.seed = seed;
}
// 哈希函数
public int hash(String value) {
int result = 0;
int len = value.length();
for (int i = 0; i < len; i++) {
result = seed * result + value.charAt(i);
}
return (cap - 1) & result;
}
}
public static void test {
String value = "4243212355312";
MyBloomFilter filter = new MyBloomFilter();
System.out.println(filter.contains(value));
filter.add(value);
System.out.println(filter.contains(value));
}
Google Guava
,只需要在项目中引入一下依赖:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>27.0.1-jre</version>
</dependency>
public static void MyBloomFilterSysConfig {
@Autowired
OrderMapper orderMapper
// 1.创建布隆过滤器 第二个参数为预期数据量10000000,第三个参数为错误率0.00001
BloomFilter<CharSequence> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.forName("utf-8")),10000000, 0.00001);
// 2.获取所有的订单,并将订单的id放进布隆过滤器里面
List<Order> orderList = orderMapper.findAll()
for (Order order;orderList ) {
Long id = order.getId();
bloomFilter.put("" + id);
}
}
// 判断订单id是否在布隆过滤器中存在
bloomFilter.mightContain("" + id)
缓存击穿
key
非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,瞬间对数据库的访问压力增大。
-
该数据没有人查询过 ,第一次就大并发的访问。(冷门数据) -
添加到了缓存,reids有设置数据失效的时间 ,这条数据刚好失效,大并发访问(热点数据)
当用户出现大并发访问的时候,在查询缓存的时候和查询数据库的过程加锁,只能第一个进来的请求进行执行,当第一个请求把该数据放进缓存中,接下来的访问就会直接集中缓存,防止了缓存击穿。
load
数据后再释放锁。若其它线程获取锁失败,则等待一段时间后重试。这里要注意,分布式环境中要使用分布式锁,单机的话用普通的锁(
synchronized
、
Lock
)就够了。
// 获取库存数量
public String getProduceNum(String key) {
try {
synchronized (this) { //加锁
// 缓存中取数据,并存入缓存中
int num= Integer.parseInt(redisTemplate.opsForValue().get(key));
if (num> 0) {
//没查一次库存-1
redisTemplate.opsForValue().set(key, (num- 1) + "");
System.out.println("剩余的库存为num:" + (num- 1));
} else {
System.out.println("库存为0");
}
}
} catch (NumberFormatException e) {
e.printStackTrace();
} finally {
}
return "OK";
}
public String getProduceNum(String key) {
// 获取分布式锁
RLock lock = redissonClient.getLock(key);
try {
// 获取库存数
int num= Integer.parseInt(redisTemplate.opsForValue().get(key));
// 上锁
lock.lock();
if (num> 0) {
//减少库存,并存入缓存中
redisTemplate.opsForValue().set(key, (num - 1) + "");
System.out.println("剩余库存为num:" + (num- 1));
} else {
System.out.println("库存已经为0");
}
} catch (NumberFormatException e) {
e.printStackTrace();
} finally {
//解锁
lock.unlock();
}
return "OK";
}
缓存雪崩
-
reids宕机 -
大部分数据失效
当某一时间点,key大量失效,造成的缓存雪崩的原理图如下:
对于缓存雪崩的解决方案有以下两种:
-
搭建高可用的集群,防止单机的redis宕机。 -
设置不同的过期时间,防止同一时间内大量的key失效。
针对业务系统,永远都是具体情况具体分析,没有最好,只有最合适。于缓存其它问题,缓存满了和数据丢失等问题,我们后面继续深入的学习。最后也提一下三个词LRU、RDB、AOF,通常我们采用LRU策略处理溢出,Redis的RDB和AOF持久化策略来保证一定情况下的数据安全。
有道无术,术可成;有术无道,止于术
好文章,我在看❤️