使用缓存会带来的问题:
1. 缓存数据跟数据库数据不一致
2. 缓存穿透
3. 缓存雪崩
4. 缓存击穿
问题1: 缓存数据跟数据库数据不一致
解决方法:
- 内存淘汰(依赖Redis自身的配置,当趋紧给Redis配置的最大内存时,过期旧数据)
- 超时剔除(设置TTL)
- 主动更新(当DB有操作时,更新Redis)
对于低一致性的需求,可以采用前两种方式。第三种解决方式维护成本高,对代码侵入性强。第三种解决思路的3种实现:
Cache Aside Pattern; Read/Write Through Pattern (将Redis和数据库整合成一个服务,调用者无需知道Redis和数据库是怎样同步的); Write Behind Caching Pattern (调用者只操作缓存,另一个异步的线程及时监控redis然后更新到数据库).
对于第一种实现,又有3个问题
- 更新DB的数据时,删掉还是更新缓存中的此条数据?删掉吧。
- 如何保证原子性?事务/分布式事务
- 先后顺序?先数据库后Redis吧。
问题2: 缓存穿透
当获取一条Redis和数据库都不存在的数据时,会直接访问到数据库,当这种获取不存在的数据一直请求时,会造成一直访问到数据库。有时可能会发生恶意攻击,伪造随机ID一直访问,造成一直访问数据库有无这条数据。
解决方法:1. 缓存空对象 redis.set(key, null) 这时可以设置一个较短的TTL 2. 布隆过滤器 3. 控制访问权限 4. 设置复杂ID,例如雪花ID
问题3: 缓存雪崩
有大量的key同时失效,或Redis服务宕机,导致数据库接到大量请求。
解决方法:
- 给不同的key的TTL添加随机值,避免同时失效。
- 哨兵机制,提高Redis服务高可用性,主从。
- Redis挂了->在业务上添加降级限流策略。
- 多级缓存。
问题4: 缓存击穿 (热点key)
被高并发访问,并且缓存重建业务复杂的key突然失效了。比如某家店铺有促销,大量流量访问到这家店铺,如果它的缓存失效,则数据库造成非常大压力。当失效后,第一个访问到该店铺的线程会重建缓存,假设建设缓存需要200ms,那么在这200ms内,其余的线程也纷纷来尝试创建缓存。
解决方法:
互斥锁。缓存未命中 -> 互斥锁 -> 读数据库 -> 写缓存 ->释放锁. 这里的互斥锁可以用redis的setnx 来做,释放锁就是del咯.
逻辑过期。不设置key的过期时间,默认一直有效。同时往对象里塞一个expire字段代表过期时间,如果发现读到的值过期了,那么利用新开的线程更新缓存,其他线程先读旧的值。同时可以伴随缓存预热,即提前把热点key放入缓存中。
在塞expire字段的时候如何能更少的侵入业务代码呢?可以新建一个class,只有expire字段,然后让业务类去继承。也可以新建一个class,除了有一个expire字段外,新加一个Object类型的data字段,然后把需要缓存的类塞进来。
封装缓存工具
压测工具JMeter
用 brew install jmeter 下载JMeter
用 brew search jmeter 查看是否下载成功
用 jmeter 启动JMeter