高频面试题:如何保证缓存与数据库的双写一致性

前言

    为了解决高并发的流量问题,通常我们都会添加缓存这一层,来扛住大量的读请求。虽然缓存能够帮数据库分担大量的读请求,但是也伴随着一个问题就是缓存中的数据怎么跟数据库中的数据保持一致,又是一个新问题


数据实时性等级

    这里我们需要保证缓存和数据库的数据一致性,也可以根据数据所要求的实时性等级去评估,并不是所有的数据我们都需要保证强一致性,这里根据数据要求实时性不同大致分为2个等级:强一致性数据,弱一致性数据

        强一致性数据:比如一些支付数据,金额数据,涉及到钱的数据对实时性要求就比较高,必须要保证数据的一致性

        弱一致性数据:像一些用户基础信息,具有读多写少的特征,没必要保证数据的强一致性,可以保证数据的最终一致性

    下面来讨论一下如何去保证缓存和数据库双写时数据的一致性


解决方案

    缓存的经典用法:请求进来,先走缓存,缓存存在,直接返回,缓存没有,查数据库,再将数据库的值放到缓存中,供后续的读请求使用

    上面讲述的是读请求好理解,如果碰到的是写请求,先删除缓存,还是先更新数据库,再更新缓存是一个问题

    那么针对不同的场景,可以概括为以下几个策略:

        1. 先更新数据库,后更新缓存

        2. 先更新数据库,后删除缓存

        3. 先更新缓存,后更新数据库

        4. 先删除缓存,后更新数据库


    先更新数据库,后更新缓存

    这种方案比较严重的问题有2点:如果更新数据库成功,而更新缓存失败,则会导致缓存中一直存在旧数据。还有一个问题就是如果读请求不多,便会导致每次写请求都更新一下缓存,频繁的更新缓存,这里可以采用删除缓存的方式,而不是更新缓存的思想,让后续有读请求去做到更新缓存,有点类似lazy的思想

    这种方案被采用的比较少,因为存在频繁的更新缓存,性能低下


    先更新数据库,后删除缓存

    跟第一种都是先更新数据库,再去操作缓存,比第一种方案的好处就是不是去更新缓存,而是直接删除,避免频繁的更新操作缓存

    问题还是存在旧数据的读取,缓存操作的失败,出现数据不一致问题,先操作数据库的话,再去操作缓存,都有可能存在缓存不一致的问题

    解决问题1的思路可以有:

        可以借助可靠消息一致性的特性来完成缓存的删除

    我们可以通过订阅binlog日志,再借助mq的消息队列达到成功删除缓存的效果,如果中间删除失败,则消息还会一直重发,直到消息被成功消费


    先更新缓存,后更新数据库

    后面两个方案都是先操作缓存,再去操作数据库。

    先更新缓存,虽然能够保证后续的请求读到的不是旧数据,但是更新数据库的时候,如果数据库更新失败,那么就会导致缓存中一直存在无效的数据,所造成的影响不比读到旧数据小。还有一个问题就是采用的是更新缓存,而不是删除缓存,就会导致在读请求很少的情况下,会频繁的操作缓存,性能低下


    先删除缓存,后更新数据库

    先删除缓存虽然能解决第三种方案的频繁更新缓存,缓存中保存的无效数据。但是加入了一个新问题就是更新数据库时如果事务还未提交,这时有读请求过来,还是会读到旧数据。如果需要解决这种问题可以采用内部队列去解决,将同一个数据的读写请求放到一个队列中,看是否具有写请求,让读请求进行阻塞

    思路可以有:

        更新数据的时候,根据数据的唯一标识,放到JVM的一个内部队列中,后续有读请求的时候,发现队列中存在正在更新数据的线程,那么也将该读请求放到内部队列中,等待之前的写请求更新完成,再来执行读请求。这样的话虽然能解决读到旧数据问题,但是会引发以下的系列问题:

                这样会引起读请求长时阻塞,如果队列中写请求过多的话

                读请求的并发量过高

                需要将同一数据的请求路由到同一实例中


总结

    上面四种方案总结下来,每种方案都有自己的缺点,但是对系统影响比较小的还是先删除缓存,再更新数据库方案。如果对于数据要求有很强的一致性,干脆还不如不要走缓存,读写都走数据库,这样也就不用保证缓存和数据库双写的一致性了。当然对于那种技术方案是没有最好的,只有最适合我们的

posted @ 2020-09-26 17:21  半分、  阅读(1751)  评论(0编辑  收藏  举报