具体场景是这样的,生产环境的缓存采用oscache,配置了永久缓存,最大缓存数是30000。缓存集群采用Jgroup组播,数据变更(主要是update)成功后由该同步节点通知到其他所有节点,有20多个节点。缓存客户端包括业务部件和数据同步部件,业务部件查询数据,数据同步部件变更数据。问题的起因是数据变更请求大规模并发,单节点请求总并发tps达到700。数据同步先删后插,会删除原缓存键值,再去同步数据库,再插入缓存。业务查询因缓存被删掉失效,导致命中率降低而直接查库。查库时没有加锁,存在多个节点重复到数据库查同一个键。这时数据库就扛不住了,数据库cpu飙升到90%。数据库性能的大幅下降又反过来影响接口的响应时延,很多接口都被殃及池鱼。
当时的规避方案是修改oscache缓存配置,不再实时更新,而是改为定时刷新。降低缓存变更频率,提高缓存命中率,降低对数据库的压力,提高接口响应速度。这一措施操作后生产环境的数据库cpu下去了,接口调用正常了,但牺牲了变更数据后在一定时间内的一致性,因为这时查到的缓存还是老数据。
后续优化,增加redis作为二级永久缓存,全量同步数据。把oscache作为一级缓存,不使用集群,只当作本地缓存使用,缓存失效时间半个小时。请求过来先到oscache查,查到就响应,查不到接着查redis,还查不到最后查数据库。查数据库时加锁,锁住后先查一次redis,再查库,查到后更新到一二级缓存。加锁避免了并发时可能重复对同一个键查库,保证相同键只要查库一次,其他的并发查都从redis缓存取。增删改先操作数据库,再同步到一二级缓存。缓存集群从oscache组播改为客户端分片加redis主从,使用哨兵监控和故障恢复,避免原组播无法跨网段的扩容瓶颈。redis的内存容量比本地内存大,目前生产环境配置是8G,超过该内存使用LRU自动移除键值,性能相对稳定,支持分布式,扩容可能麻烦一点,需要重新分片,但目前够用。多了一层缓存可以有力保护数据库免受重复查询的骚扰。
抽取数据同步能力,沉淀为能力部件,新增两个节点专门只接受数据变更请求。其他节点剥离数据变更业务,只存在查的情况。这样从架构就实现了读写分离。