1.Redis缓存场景问题
1. 缓存和数据库不一致
Redis最常见的使用场景就是缓存一些高频访问的数据来缓解数据库压力,那么当数据库更新时,缓存如何更新呢?最常见的做法就是进行编写代码进行更新,当然直接更新缓存是不推荐的,因为会有无效的写操作,一般是采用删除缓存的方式。
那么当更新数据库时就面临两种选择:
- 先删除缓存,再更新数据库
- 先更新数据库,在删除缓存
以上两种方式在普通场景下都能正确更新缓存,而如果在并发场景下就可能会出现问题。
先删除缓存,再更新数据库
- 假设有两个线程,线程1进行数据修改,线程2进行数据查询
- 线程1先将缓存给删除,那么此时此时线程2就会来查数据发现缓存未命中就会去查数据库
- 此时线程2查到的是一份旧数据,接着线程1去修改数据库
- 最后线程2将查到的旧值写到了缓存中
此时就会出现缓存和数据不一致的情况
先更新数据,再删除缓存
- 同样有两个线程,线程1进行数据修改,线程2进行修改数据
- 线程2先查询缓存,缓存没有命中就会去查数据库
- 接着线程1去更新数据库,更新完数据库后将缓存删除
- 最后线程2将旧数据写入缓存
两中方法都会导致缓存和数据库不一致的问题,那么该采用哪种?
- 注意第二种方式出现缓存不一致的概率是极低的,因为要满足好几个条件
- 首先缓存中没有数据,一般缓存中是有数据的除非复杂场景下缓存失效了
- 第二满足两个线程并行的执行,一个去查一个去修改
- 第三更新数据库和删除缓存在写入旧数据到缓存的线程之前执行,而数据库的写操作是需要加锁的,加锁就需要时间的开销,所以更新数据库和删除缓存执行的时间是要比查数据库更新缓存的执行时间要长
综上所诉,这种情况发送的概率是极低的,并且我们可以设置缓存超时时间来进一步避免。
所以先更新数据,再删除缓存这种做法更推荐,当然要配合事务进行操作保证两个步骤同时成功。
2. 缓存穿透
缓存穿透指的是查询的数据在缓存和数据库中都不存在,这些缓存就永远不会生效,这些请求就又会到数据库,如果请求量比较大的话就又会给数据库增加压力。
解决缓存穿透有两种实现方案:
-
缓存空对象
- 优点:实现简单
- 缺点:额外内存消耗,可能照成短期的数据不一致问题
因为如果有大量请求查询的数据都不存在的时候都会存一个空对象,就会导致一定的内存消耗,不过可以通过设置一个较短的超时时间来解决。
还有一种情况就是如果查询的数据不存在在Redis里存了一个空对象,而此时恰好存入了这么一个数据,就会导致后面查询的时候查不到这个数据。
-
布隆过滤器
布隆过滤器是一种数据结构,他可以判断一个数据可能存在或者一定不存在,用一个比特位来表示一个数据,由于哈希冲突的原因,就可能存在误判。
就是在查询Redis之前去查询布隆过滤器,如果在布隆过滤器查不到直接返回,如果查得到再去Redis里查询,不过这种方式也是存在一定的穿透风险的。
- 优点:内存占用较少,没有多余key
- 缺点:实现复杂,存在误判的可能
当然避免缓存穿透还可以进一步做一些操作:
- 在控制层做好数据的校验
- 增加id复杂性,加强用户权限校验
- 做好热点参数的限流等
3. 缓存雪崩
缓存雪崩是指同一时段大量的缓存Key同时失效或者Redis服务直接宕机,导致大量的请求又达到了服务器上。
解决方案:
- 给不同的Key的TTL添加随机值
- 利用Redis集群提高服务的可用性
- 给缓存业务添加降级限流策略
- 给业务添加多级缓存
4. 缓存击穿
缓存击穿问题也就做热点Key问题,就是一个被高并发访问并且重新生成缓存比较复杂的Key失效了,假设缓存失效了生成一个缓存需要几百毫秒,接着大量的请求一瞬间打到了数据,给数据库造成了巨大的压力,
解决办法:
-
使用互斥锁
- 当在缓存中没有查询到对应数据时,此时先加锁之后再去数据库查询数据再写入缓存,写入缓存之后再释放锁
- 当其它线程查询缓存前,尝试获取锁失败,就让其休眠一会再重新获取锁,此时对应缓存已经建立就可以立即获取到数据并返回
- 优点:没有额外内存消耗、可以保证数据一致性、实现简单
- 缺点:可能会导致大量线程等待影响性能,可能会有死锁风险
-
使用逻辑过期
- 在存储缓存数据时给数据添加逻辑过期时间
- 当有线程来查询数据发现缓存已经过期,就会获取到锁之后开启一个新的线程去查询数据并重置缓存过期时间,最后释放锁
- 如果此时有其它线程来查询数据,发现锁被占用就直接返回旧数据
- 优点:线程无需等待,性能较好
- 缺点:不保证一致性、有额外的内存消耗、实现较为复杂