缓存总结

1.使用场景

不经常变更的数据存到缓存中,减少db压力

2.缓存分类

1.分布式缓存redis/memcached
2.本地缓存cachetools/flask自带g
3.读操作缓存: 业务系统 读操作 > 写操作,减少db查询操作
4.写操作缓存: 业务系统 写操作 > 读操作,减少db写入操作

3.相关问题

3.1缓存穿透

如果查询的key一直不存在, 并发量大的情况下,造成db压力增大

解决:

  可以将这个不存在的key预先设定一个值, 比如&&
  
  首次查询如果缓存和db中均没有,设置该key对应的值为&&,这个失效时间设置短一些
  再次查询如果查到key对应的value是&&, 则直接拦截	

3.2缓存并发

高并发情况下, 缓存失效后, 可能出现多个进程同时查询db, 同时设置缓存的情况 造成db压力过大,缓存频繁更新的问题

解决:

对缓存查询加竞争锁,但是存在部分请求锁等待的问题

3.3缓存失效

高并发的情况下,存在某一时间缓存同时失效 解决:

缓存失效时间分散开,比如可以在原有失效时间的基础上增加一个随机值
失效时间可以分析用户行为,根据场景进行区分

3.4 缓存更新策略

1.LRU/LFU/FIFO算法剔除

使用场景: 剔除算法通常用于缓存使用量超过了预设的最大值时候,如何对现有的数据进行剔除。例如Redis使用maxmemory-policy这个配置作为内 存最大值后对于数据的剔除策略

一致性:

要清理哪些数据是由具体算法决定,开发人员只能决定使用哪种算法,所以数据的一致性是最差的

维护成本:

通常只需要配置最大maxmemory和对应的策略即可
2.超时剔除

使用场景:超时剔除通过给缓存数据设置过期时间,让其在过期时间后 自动删除 如果业务可以容忍一段时间内,缓存层数据和存储层数据不一致,那么可以为其设置过期时间。在数据过期 后,再从真实数据源获取数据,重新放到缓存并设置过期时间 如涉及交易方面的业务不可用

一致性:

一段时间窗口内(取决于过期时间长短)存在一致性问题,即 缓存数据和真实数据源的数据不一致

维护成本:

维护成本不是很高,只需设置expire过期时间即可,当然前 提是应用方允许这段时间可能发生的数据不一致
3.主动更新

使用场景: 应用方对于数据的一致性要求高,需要在真实数据更新后, 立即更新缓存数据。例如可以利用消息系统或者其他方式通知缓存更新

一致性:

一致性最高,但如果主动更新发生了问题,那么这条数据很可 能很长时间不会更新,所以建议结合超时剔除一起使用效果会更好

维护成本:

维护成本会比较高,开发者需要自己来完成更新,并保证更 新操作的正确性。

建议

低一致性业务建议配置最大内存和淘汰策略的方式使用
高一致性业务可以结合使用超时剔除和主动更新,这样即使主动更新 出了问题,也能保证数据过期时间后删除脏数据

根据我们自身的业务场景,API层面一致性要求不是很高,遂采用最大内存和超时剔除的方案

4.超时剔出策略
1.先更新数据库,再更新缓存

并发情况下,存在脏数据写入缓存

1.请求A更新db
2.请求B更新db
3.请求A更新cache
4.请求B更新cache

问题

可能由于处理,A最后才更新cache,导致cache中写入脏数据
如果缓存不设置expiret时间, 则一直存在脏数据的情况
2.先删除缓存,再更新数据库
1.请求A进行写操作,先删除cache
2.请求B查询缓存,不存在
3.请求B去数据库查询,得到旧值
4.请求B将旧值写入缓存
5.请求A将新值写入数据库

问题

如果缓存不设置expire时间, 则一直存在脏数据的情况

解决

延时双删 > A在更新db后再次删除cache
存在双删仍然失败的情况,则脏数据仍存在
3.先更新数据库,再删除缓存
1.缓存刚好失效
2.请求A查询数据库,得到一个旧值
3.请求B更新数据库
4.请求B删除cache
5.请求A将旧值写入cache

发生条件: 步骤3比步骤2耗时短,导致步骤4先于步骤5发生,但是步骤3是写操作,比较耗时, 所以以上概率很小 解决:

1.缓存加入过期时间
2.请求B更新db和删除cache之间,sleep, 延时删除

4.删除缓存失败重试机制

方案1

删除缓存的时,程序增加重试机制,如果失败,则扔到消息队列中 另一个任务定期读取消息队列,再次尝试删除

方案2

删除缓存的任务,交给订阅binlog程序处理,业务层面只需要更新数据库即可

5.缓存加载问题

如果缓存挂掉或者db字段有更新

解决:

项目加载或者db有变动的情况下,热加载db数据更新到缓存,减少db压力

6.缓存粒度控制

从MySQL获取用户信息

1
select * from user where id={id}

将用户信息缓存到Redis中: 缓存全部列:

1
set user:{id} 'select * from user where id={id}'

缓存部分重要列

1
set user:{id} 'select {importantColumn1}, {important Column2} ... {importantColumnN} from user where id={id}'

通用性:

缓存全部数据比部分数据更加通用,但从实际经验看,很长时 间内应用只需要几个重要的属性

空间占用:

缓存全部数据要比部分数据占用更多的空间,可能存在以下 问题

1.全部数据会造成内存的浪费
2.全部数据可能每次传输产生的网络流量会比较大,耗时相对较大,在 极端情况下会阻塞网络
3.全部数据的序列化和反序列化的CPU开销更大

代码维护:

    全部数据的优势更加明显,而部分数据一旦要加新字段需要
    修改业务代码,而且修改后通常还需要刷新缓存数据

参考: http://blog.jobbole.com/113992/ http://blog.didispace.com/chengchao-huancun-zuijiazhaoshi/ 《redis开发与运维》