缓存层场景实战读缓存,如何更新缓存+缓存的高可用设计+监控

如何更新缓存

更新缓存的步骤特别简单,共两步:更新数据库和更新缓存。但这简单的两步中需要考虑很多问题。

1)先更新数据库还是先更新缓存?更新缓存时先删除还是直接更新?

2)假设第一步成功了,第二步失败了怎么办?

3)假设两个线程同时更新同一个数据,A线程先完成第一步,B线程先完成第二步怎么办?

其中,第1个问题就存在5种组合方案,下面逐一进行介绍(以上3个问题因为紧密关联,无法单独考虑,下面就一起说明)。

组合1:先更新缓存,再更新数据库

对于这个组合,会遇到这种情况:假设第二步更新数据库失败了,要求回滚缓存的更新,这时该怎么办呢?Redis不支持事务回滚,除非采用手工回滚的方式,先保存原有数据,然后再将缓存更新回原来的数据,这种解决方案有些缺陷。

这里简单举个例子。

1)原来缓存中的值是a,两个线程同时更新库存。

2)线程A将缓存中的值更新成b,且保存了原来的值a,然后更新数据库。

3)线程B将缓存中的值更新成c,且保存了原来的值b,然后更新数据库。

4)线程A更新数据库时失败了,它必须回滚,那现在缓存中的值更新成什么呢?理论上应该更新成c,因为数据库中的值是c,但是,线程A里面无从获得c这个值。

如果在线程A更新缓存与数据库的整个过程中,先把缓存及数据库都锁上,确保别的线程不能更新,是否可行?当然是可行的。但是其他线程能不能读取?

假设线程A更新数据库失败回滚缓存时,线程C也加入进来,它需要先读取缓存中的值,这时又返回什么值?

看到这个场景,是不是有点儿熟悉?不错,这就是典型的事务隔离级别场景。所以就不推荐这个组合,因为此处只是需要使用一下缓存,而这个组合就要考虑事务隔离级别的一些逻辑,成本太大。接着考虑别的组合。

组合2:先删除缓存,再更新数据库

使用这种方案,即使更新数据库失败了也不需要回滚缓存。这种做法虽然巧妙规避了失败回滚的问题,却引出了两个更大的问题。

1)假设线程A先删除缓存,再更新数据库。在线程A完成更新数据库之前,后执行的线程B反而超前完成了操作,读取Key发现没有数据后,将数据库中的旧值存放到了缓存中。线程A在线程B都完成后再更新数据库,这样就会出现缓存(旧值)与数据库的值(新值)不一致的问题。

2)为了解决一致性问题,可以让线程A给Key加锁,因为写操作特别耗时,这种处理方法会导致大量的读请求卡在锁中。以上描述的是典型的高可用和一致性难以两全的问题,如果再加上分区容错 就 是 CAP ( 一 致 性 Consistency 、 可 用 性 Availability 、 分 区 容 错 性Partition Tolerance)了,这里不展开讨论,接下来继续讨论另外3种组合。

组合3:先更新数据库,再更新缓存

对于组合3,同样需要考虑两个问题。

1)假设第一步(更新数据库)成功,第二步(更新缓存)失败了怎么办?

因为缓存不是主流程,数据库才是,所以不会因为更新缓存失败而回滚第一步对数据库的更新。此时一般采取的做法是重试机制,但重试机制如果存在延时还是会出现数据库与缓存不一致的情况,不好处理。

2)假设两个线程同时更新同一个数据,线程A先完成了第一步,线程B先完成了第二步怎么办?线程A把值更新成a,线程B把值更新成b,此时数据库中的最新值是b,因为线程A先完成了第一步,后完成第二步,所以缓存中的最新值是a,数据库与缓存的值还是不一致,这个逻辑还是有问题的。

因此,也不建议采用这个组合。

组合4:先更新数据库,再删除缓存

针对组合4,先看看它能不能解决组合3的第二个问题。

假设两个线程同时更新同一个数据,线程A先完成第一步,线程B先完成第二步怎么办?

线程A把值更新成a,线程B把值更新成b,此时数据库中的最新值是b,因为线程A先完成了第一步,所以第二步谁先完成已经不重要了,因为都是直接删除缓存数据。这个问题解决了。

那么,它能解决组合3的第一个问题吗?假设第一步成功,第二步失败了怎么办?

这种情况的出现概率与组合3相比明显低不少,因为删除比更新容易多了。虽然这个组合方案不完美,但出现一致性问题的概率较低。

除了组合3会碰到的问题,组合4还会碰到别的问题吗?是的。假设线程A要更新数据,先完成第一步更新数据库,在线程A删除缓存之前,线程B要访问缓存,那么取得的就是旧数据。这是一个小小的缺陷。那么,以上问题有办法解决吗?

组合5:先删除缓存,更新数据库,再删除缓存

还有一个方案,就是先删除缓存,再更新数据库,再删除缓存。这个方案其实和先更新数据库,再删除缓存差不多,因为还是会出现类似的问题:假设线程A要更新数据库,先删除了缓存,这一瞬间线程C要读缓存,先把数据迁移到缓存;然后线程A完成了更新数据库的操作,这一瞬间线程B也要访问缓存,此时它访问到的就是线程C放到缓存里面的旧数据。

不过组合5出现类似问题的概率更低,因为要刚好有3个线程配合才会出现问题(比先更新数据库,再删除缓存的方案多了一个需要配合的线程)。

但是相比于组合4,组合5规避了第二步删除缓存失败的问题——组合5是先删除缓存,再更新数据库,假设它的第三步“再删除缓存”失败了,也没关系,因为缓存已经删除了。

其实没有一个组合是完美的,它们都有读到脏数据(这里指旧数据)的可能性,只不过概率不同。根据以上分析,组合5相对来说是比较好的选择。

不过这个组合也有一些问题要考虑,具体如下。

1)删除缓存数据后变相出现缓存击穿,此时该怎么办?此问题在前面已经给出了方案。

2)删除缓存失败如何重试?这个重试可以做得复杂一点,也可以做得简单一点。简单一点就是使用try…catch…,假设删除缓存失败了,在catch里面重试一次即可;复杂一点就是使用一个异步线程不断重试,甚至用到MQ。不过这里没有必要大动干戈。而且异步重试的延时大,会带来更多的读脏数据的可能性。所以仅仅同步重试一次就可以了。

3)不可避免的脏数据问题。虽然这个问题在组合5中出现的概率已经大大降低了,但是还是有的。关于这一点就需要与业务沟通,毕竟这种情况比较少见,可以根据实际业务情况判断是否需要解决这个瑕疵。

Tips任何一个方案都不是完美的,但如果剩下1%的问题需要花好几倍的代价去解决,从技术上来讲得不偿失,这就要求架构师去说服业务方,去平衡技术的成本和收益。

前面花了较长的篇幅来讨论更新缓存的逻辑,接下来详细讨论缓存的高可用设计。

缓存的高可用设计

关于缓存高可用设计的问题,其实可以单独用一章来讲,但是考虑到Redis的用法介绍偏理论,本书主要讲场景,这里就不讲详细的用法了,只讲要点。

设计高可用方案时,需要考虑5个要点。

1)负载均衡:是否可以通过加节点的方式来水平分担读请求压力。

2)分片:是否可以通过划分到不同节点的方式来水平分担写压力。

3)数据冗余:一个节点的数据如果失效,其他节点的数据是否可以直接承担失效节点的职责。

4)Failover:任何节点失效后,集群的职责是否可以重新分配以保障集群正常工作。

5)一致性保证:在数据冗余、Failover、分片机制的数据转移过程中,如果某个地方出了问题,能否保证所有的节点数据或节点与数据库之间数据的一致性(依靠Redis本身是不行的)。

如果对缓存高可用有需求,可以使用Redis的Cluster模式,以上5个要点它都会涉及。关于Cluster的配置方法,可以参考Redis官方文档或其他相关教程。

缓存的监控

缓存上线以后,还需要定时查看其使用情况,再判断业务逻辑是否需要优化,也就是所谓的缓存监控。

在查看缓存使用情况时,一般会监控缓存命中率、内存利用率、慢日志、延迟、客户端连接数等数据。当然,随着问题的深入还可增加其他的指标,这里就不详细说明了。

当时公司采用的是一套自研的管理工具,这套管理工具里包含了监控功能。目前也有很多开源的监控工具,如RedisLive、Redis-monitor。至于最终使用哪种监控工具,则需要根据实际情况而定。

小结

以上方案可以顺利解决读数据请求压垮数据库的问题,目前互联网架构也基本是采取这个方案。

分布式缓存系统上线后,商品详情页的大部分数据存到了Redis中,并且一些数据的读取改为异步请求,优化效果非常明显:打开详情页基本只需要1秒;而后台监控这个详情页的API(从缓存中取数据的那个API),平均响应时长变为10毫秒以内;监控数据中,从首页到搜索再到详情页的平均时长变成了4秒左右,这个改善幅度还是很大的。

IT部门在当时一次例会中讨论最近的工作亮点,有人问道:“咱们要不要提一下这个缓存方案?它对业务的帮助很大。”另一个同事就说:“可是缓存这个技术很普通,万一被问到,这个方案有什么创新的地方,我们要怎么回答?”然后大家都沉默了。

但是后来还是把这个方案放到了部门汇报内容里,只写了一句话:“利用缓存技术和异步加载技术将商品详情页的平均响应时间从十几秒缩短到1秒。”这在会议上并没有引起什么反响,只是后来有一次聚餐时,CEO也在场,他说过这样的话:“其实我们不要老是追求新技术,能帮到业务的技术就是好技术。”CTO就接话说:“比如上次你们把商品详情页的打开时间大幅度缩短了,这就是好事,这种好事以后多做。”

接下来说说不足吧。

这个方案主要针对读数据请求量大的情况,或者读数据响应时间很长的情况,而不能应对写数据请求量大的场景。也就是说写请求多时,数据库还是会支撑不住。针对这个问题,下一章会给出对应的解决方案。

本文给大家讲解的内容是缓存层场景实战,读缓存,如何更新缓存+缓存的高可用设计+缓存的监控

  1. 下篇文章给大家讲解的内容是缓存层场景实战,写缓存,业务场景:如何以最小代价解决短期高频写请求
  2. 觉得文章不错的朋友可以转发此文关注小编;
  3. 感谢大家的支持
举报
评论 0