缓存不仅加快了IO,还可减少原始数据的计算工作。
缓存系统一般设计简单,功能单一,所以Redis吞吐量能是MySQL几倍~几十倍,对于互联网读多写少的高并发场景已不可或缺。
虽然简单好用,但是如果姿势不对,就会造成不必要的损失。
不要把Redis当数据库
很多人不仅把 redis 当缓存,更是把Redis当做数据库使用。很多因为Redis中数据消失导致业务逻辑错误,并且因为没有保留原始数据,业务都无法恢复。
虽然Redis有持久化功能,但这并不能以为它可作为高性能的KV数据库。
Redis处理请求很快,但那是因为在内存中,无法保存超过内存大小的数据。
因此,再将Redis用作缓存时,需要注意:
- 客户端角度看,缓存数据的特点一定是有原始数据来源,且允许丢失,即使设置的缓存时间是1min,在30s时缓存数据因为某种原因消失了,也要能接受。当数据丢失后,需要从原始数据重新加载数据,永远不要认为缓存是绝对可靠的,也不要认为缓存不会删除没有过期的数据。
- Redis服务端来看,缓存可以保存的数据量一定小于原始数据
应该限制Redis内存使用量,即maxmemory参数
根据数据特点,明确Redis数据淘汰策略
这些策略是Key范围+Key选择算法的搭配组合,范围有allkeys和volatile两种,算法有LRU、TTL和LFU三种。
如何选择合适的驱逐算法
算法角度
Redis 4.0以后推出的LFU比LRU更“实用”。试想一下,如果一个Key访问频率是1天一次,但正好在1秒前刚访问过,那么LRU可能不会选择优先淘汰这个Key,反而可能会淘汰一个5秒访问一次但最近2秒没有访问过的Key,而LFU算法不会有这个问题。而TTL会比较“头脑简单”一点,优先删除即将过期的Key,但有可能这个Key正在被大量访问。
Key范围角度
allkeys可确保即使Key没有TTL也能回收,如果使用的时候客户端总是“忘记”设置缓存的过期时间,那么可以考虑使用这系列算法。
而volatile会更稳妥,万一客户端把Redis当做了长效缓存,只是启动时候初始化一次缓存,那么一旦删除了此类没有TTL的数据,可能就会导致客户端出错。
缓存数据同步策略
实际开发中,如果修改了原始数据,考虑到缓存数据更新的及时性,我们可能会采用主动更新缓存的策略:
先更新缓存,再更新数据库
不可行。数据库设计复杂,压力集中,数据库因为超时等原因更新操作失败的可能性较大,此外还会涉及事务,很可能因为数据库更新失败,导致缓存和数据库的数据不一致。
先更新数据库,再更新缓存
不可行:
- 如果线程A、B先后完成数据库更新,但更新缓存时却是B和A的顺序,那很可能会把旧数据更新到缓存中引起数据不一致
- 我们不确定缓存中的数据是否会被访问,不一定要把所有数据都更新到缓存
先删除缓存,再更新数据库,访问时候按需加载数据到缓存
不可行。并发情况下,很可能删除缓存后还没来得及更新DB,就有另一个线程先读取旧值到缓存中去。并发量高时,概率还很大。
先更新DB,再删缓存,访问时按需加载数据至缓存
最好。虽然在极端情况下,这种策略也可能出现数据不一致,但概率很低,基本可以忽略。
一个极端的例子,更新数据的时间节点恰好是缓存失效了:
- 【查询线程A】,先读取到了DB的旧值
- 随后【更新线程B】操作DB完成更新,并删除缓存后
- 【查询线程A】再把旧值加入缓存
更新DB后,删除缓存的操作可能失败,若失败则考虑把任务加入延迟队列进行延迟重试,确保数据可以删除,缓存可以及时更新。因为删除操作是幂等的,所以即使重复删,问题也不大,这也是删除比更新缓存好的一个原因。
所以对于缓存更新,推荐缓存中的数据不由数据更新操作主动触发,统一在需要使用的时候按需加载,数据更新后及时删除缓存中的数据即可。
并且要尽量设置合适的缓存过期时间,这样即便真的发生不一致,也可以在缓存过期后数据得到及时同步。
使用缓存系统的时候,要监控缓存系统的内存使用量、命中率、对象平均过期时间等重要指标,以便评估系统的有效性,并及时发现问题。