序言:
我们经常在项目中用到Redis去做缓存,为什么要用Redis去做缓存,会产生哪些常见问题呢?拿Redis做缓存无非就是一个字快!缓存他指的是将数据库中的数据同步到内存中,客户端获取数据直接从内存中获取,由于内存读写速度大于磁盘,而使用缓存能减少磁盘读取,大大提高查询性能。
Redis是什么:
Redis是一个高性能的C语言编写的非关系型数据库,可以对关系型数据库起到补充作用,同时支持持久化,可以将数据同步保存到数据库
Redis为什么快:
1. 数据结构简单
2. 直接在内存中读写数据
3. 采用多路IO复用模型,减少网络IO的时间消耗
4.单线程避免了线程切换和上下文切换产生的消耗
Redis常见问题(缓存击穿,穿透,雪崩)与解决方法:
缓存击穿:
击穿指的是缓存中没有,但是数据库有数据,由于种种原因缓存过期了,Redis 这层流量防护屏障被击穿了,很多并发请求直奔数据库导致数据库压力过大可能会压垮数据库把服务搞死。画个大致图:
解决方法:
1.过期时间 + 随机值
对于热点数据,我们不设置过期时间,这样就可以把请求都放在缓存中处理,充分把 Redis 高吞吐量性能利用起来。
或者过期时间再加一个随机值。
设计缓存的过期时间时,使用公式:过期时间=baes 时间+随机时间。
即相同业务数据写缓存时,在基础过期时间之上,再加一个随机的过期时间,让数据在未来一段时间内慢慢过期,避免瞬时全部过期,对数据库造成过大压力。
2.加互斥锁,只能允许一个线程去访问数据库,然后其他线程获取锁失败就睡眠等待从缓存里面去拿:
public Object getHotData(String id) {
String hotData= redis.get(id); //redisTemplate 我这里简写
// 缓存为空,过期了
if (hotData== null) {
// 互斥锁,只有一个请求可以成功
if (redis(lockName)) {
try
// 从数据库取出数据
hotData= getFromDB(id);
// 写到 Redis
redis.set(id, hotData, 60 * 60 * 24);
} catch (Exception e) {
log.error(e);
} finally {
// 确保最后删除,释放锁
redis.del(lockName);
return hotData;
}
} else {
// 否则睡眠200ms,接着获取锁
Thread.sleep(200);
return getHotData(id);
}
}
}
缓存穿透:
穿透指的是客户端频繁请求一个缓存和数据库中都没有的数据,导致数据库压力大
解决方法:
1. 缓存空值:当请求的数据不存在 Redis 也不存在数据库的时候,设置一个缺省值(比如:None)。当后续再次进行查询则直接返回空值或者缺省值。
2. 布隆过滤器:在数据写入数据库的同时将这个 ID 同步到到布隆过滤器中,当请求的 id 不存在布隆过滤器中则说明该请求查询的数据一定没有在数据库中保存,就不要去数据库查询了。(BloomFilter是缓存全量的key,全量key‘的数量尽量不要太多)
缓存雪崩:
雪崩指的是大量key过期或者失效,导致大量并发打到数据库,出现该原因主要有两种:
1. 大量热点数据同时过期,导致大量请求需要查询数据库并写到缓存;
2. Redis 故障宕机,缓存系统崩了。
解决方案
过期时间添加随机值
要避免给大量的数据设置一样的过期时间,过期时间 = baes 时间+ 随机时间(较小的随机数,比如随机增加 1~5 分钟)。
这样一来,就不会导致同一时刻热点数据全部失效,同时过期时间差别也不会太大,既保证了相近时间失效,又能满足需求。
还有一个方案就是设置接口限流,当访问的不是核心数据的时候,在查询的方法上加上接口限流保护。比如设置 10000 req/s。
如果访问的是核心数据接口,缓存不存在允许从数据库中查询并设置到缓存中。
这样的话,只有部分请求会发送到数据库,减少了压力。
限流,就是指,我们在业务系统的请求入口前端控制每秒进入系统的请求数,避免过多的请求被发送到数据库。