`
huorongbj
  • 浏览: 19761 次
文章分类
社区版块
存档分类
最新评论

分布式系统关注点——先写DB还是「缓存」?

阅读更多

如果第二次看到我的文章,欢迎文末扫码订阅我的个人公众号(跨界架构师)哟~  

本文长度为4209字,建议阅读12分钟。

坚持原创,每一篇都是用心之作~

 

 

在前一篇《360°全方位解读「缓存」》中,我们聊了运用缓存的三种思路,以及在一个完整的系统中可以设立缓存的几个位置,并且分享了关于浏览器缓存、CDN缓存、网关(代理)缓存的一些使用经验。

 

这次Z哥将深入到实际场景中,来看一下「进程内缓存」、「进程外缓存」运用时的一些最佳实践。由于篇幅原因,这次先聊三个问题。

 

首当其冲的就是“先写DB还是缓存?”。我想,只要你开始运用缓存,这会是你第一个要好好思考的问题,否则在前方等待你的就是灾难。。。

 

 

先写DB还是缓存?

一个程序可以没有缓存,但是一定要有数据库。这是大家的普遍观点,所以数据库的重要性在你的潜意识里总是被放在了第一位。

 

先DB再缓存

如果不细想的话你可能会觉得,数据库操作失败了,自然缓存也不用操作了;数据库操作成功了,再操作缓存,没毛病。

 

但是数据库操作成功,缓存操作的失败的情况该怎么解?(主要在用到redis,memcached这种进程外缓存的时候,由于网络因素,失败的可能性大增)

 

办法也是有的,在操作数据库的时候带一个事务,如果缓存操作失败则事务回滚。大致的代码意思如下:

begin trans
    var isDbSuccess = write db;
    if(isDbSuccess){
        var isCacheSuccess = write cache;
        if(isCacheSuccess){
            return success;
        }
        else{
            rollback db;
            return fail;
        }
    }
    else{
        return fail;
    }
    catch(Exception ex){
        rollback db;
    }
end trans

 

 

如此一来就万无一失了吗?并不是。除了由于事务的引入,增加了数据库的压力之外,在极端场景下可能会出现rollback db失败的情况。是不是很头疼?

 

解决这个问题的方式就是write cache的时候做delete操作,而不是set操作。如此一来,用多一次cache miss的代价来换rollback db失败的问题。

 

 

就像图上所示,哪怕rollback失败了,通过一次cache miss重新从db中载入旧值。

 

题外话:其实这种做法有一种专业的叫法——Cache Aside Pattern。为了便于记忆,你可以和分布式系统的CAP定理同时记忆,叫「缓存的CAP模式」。

 

 

是不是看上去妥了?可以开始潇洒了?

 

▲图片来源于网络,版权归原作者所有

 

如果你的数据库没有做高可用的话,的确可以妥了。但是如果数据库做了高可用,就会涉及到主从数据库的数据同步,这就有新问题了

 

题外话:所以大家不要过度追求技术的酷炫,可能会得不偿失,自找麻烦。

 

 

什么问题呢?就是如果在数据还未同步到「从库」的时候,由于cache miss去「从库」取到了未同步前的旧值。

 

 

 

解决它的第一个方式很简单,也很粗暴。就是定时去「从库」读数据,发现数据和缓存不一样了就set到缓存里去。

 

 

但是这个方式有点“治标不治本”。不断的从数据库定时读取,对资源的消耗大不说,这个间隔频率也不好定义一个比较合适的统一标准,太短吧,会导致重复读取的次数加大,太长吧,又会导致缓存和数据库不一致的时间变长。

 

所以这个方案仅适用于项目中只有2、3处需要做这种处理的场景,并且还不能是数据会频繁修改的情况。因为在数据修改频次较高的场景,甚至可能还会出现这个定时机制所消耗的资源反而大于主程序的情况。

 

 

一般情况下,另一种更普适性的方案是采用接下去聊的这种更底层的方式进行,就是“哪里有问题处理哪里”,当「从库」完成同步的时候再额外做一次delete cache或者set cache的操作

 

 

如此,虽说也没有100%解决短暂的数据不一致问题,但是已经将脏数据所存在的时长降到了最低(最终由主从同步的耗时决定),并且大大减少了无谓的资源消耗。

 

 

可能你会说,“不行,这么一点时间也不能忍”怎么办?办法是有,但是会增加「主库」的压力。就是在产生数据库写入动作后的一小段时间内强制读「主库」来加载缓存

 

怎么实现呢?先得依赖一个共享存储,可以借助数据库或者也可以是我们现在正在聊的分布式缓存。

 

然后,你在事务提交之后往共享存储中临时存一个{ key = dbname + tablename + id,value = null,expire = 3s }这样的数据,并且再做一次delete cache的操作。

 

begin trans
    var isDbSuccess = write db;
    if(isDbSuccess){        
        var isCacheSuccess = delete cache;
        if(isCacheSuccess){
            return success;
        }
        else{
            rollback db;
            return fail;
        }
    }
    else{
        return fail;
    }
    catch(Exception ex){
        rollback db;
    }
end trans
​
//在这里做这个临时存储,{key,value,expire}。
delete cache;

 

如此一来,当「读数据」的时候发生cache miss,先判断是否存在这个临时数据,只要在3秒内就会强制走「主库」取数据。

 

 

可以看到,不同的方案各有利弊,需要根据具体的场景仔细权衡。

 

 

先缓存再DB

你工作中的大部分场景对数据准确性肯定是低容忍的,所以一般不建议选择「先缓存再DB」的方案,因为内存是易失性的。一旦遇到操作缓存成功,操作DB失败的情况,问题就来了。

 

 

在这个时候最新的数据只有缓存里有,怎么办?单独起个线程不断的重试往数据库写?这个方案在一定程度上可行,但不适合用于对数据准确性有高要求的场景,因为缓存一旦挂了,数据就丢了!

 

题外话:哪怕选择了这个方案,重试线程应确保只有1个,否则会存在“ABBA”的「并发写」问题。

 

 

可能你会说用delete cache不就没问题了?

 

可以是可以,但是要有个前提条件,访问缓存的程序不会产生并发。因为只要你的程序是多线程运行的,一旦出现并发就有可能出现「读」的线程由于cache miss从数据库取的时候,「写」的线程还没将数据写到数据库的情况。

 

 

所以,哪怕用delete cache的方式,要么带lock多客户端情况下还得上分布式锁),要么必然出现数据不一致

 

 

值得注意的是,如果数据库同样做了高可用,哪怕带了lock,也还需要考虑和上面提到的「先DB再缓存」中一样的由于主从同步的时间差可能会产生的问题。

 

当然了,「先缓存再DB」也不是一文不值。当对写入速度有极致要求,而对数据准确性没那么高要求的场景下就非常好使,其实就是前一篇(《360°全方位解读「缓存」》)提到的「延迟写」机制。

 

 

小结一下,相比缓存来说,数据库的「高可用」一般会在系统发展的后期才会引入,所以在没有引入数据库「高可用」的情况下,Z哥建议你使用「先DB再缓存」的方式,并且缓存操作用delete而不是set,这样基本就可以高枕无忧了。

 

但是如果数据库做了「高可用」,那么团队必然也形成一定规模了,这个时候就老老实实的做数据库变更记录(binlog)的订阅吧。

 

 

到这里可能有的小伙伴要问了,“如果上了分布式缓存,还需要本地缓存吗?”。

 

 

本地缓存还要不要?

在解答这个问题之前我们先来思考一个问题,一个分布式系统最重要的价值是什么?

 

是「无限扩展」,只要堆硬件就能应对业务增长。要达到这点的背后需要满足一个特性,就是程序要「无状态」。那么既想引入缓存来加速,又要达到「无状态」,靠的就是分布式缓存。

 

所以,能用分布式缓存解决的问题就尽量不要引入本地缓存。否则引入分布式缓存的作用就小了很多。

 

 

但是在少数场景下,本地缓存还是可以发挥其价值的,但是我们需要仔细识别出来。主要是三个场景:

  1. 不经常变更的数据。(比如一天甚至好几天更新一次的那种)

  2. 需要支撑非常高的并发。(比如秒杀)

  3. 对数据准确性能容忍的场景。(比如浏览量,评论数等)

 

不过,我还是建议你,除了第二种场景,否则还是尽量不要引入本地缓存。原因我们下面来说说。

 

 

其实这个原因的根本问题就是在引入了本地缓存后,本地缓存(进程内缓存)、分布式缓存(进程外缓存)、数据库这三者之间的数据一致性该怎么进行呢?

 

 

本地缓存、分布式缓存、db之间的数据一致性

如果是个单点应用程序的话,很简单,将本地缓存的操作放在最后就好了。

 

可能你会说本地缓存修改失败怎么办?比如重复key啊什么的异常。那你可以反思一下为这种数据为什么可以成功的写进数据库。。。

 

 

但是,本地缓存带来的一个巨大问题就是:虽然一个节点没问题,但是多个本地缓存节点之间的数据如何同步?

 

解决这个问题的方式中有两种和之前我们聊过的Session问题(《做了「负载均衡」就可以随便加机器了吗?》)是类似的。要么是由接收修改的节点通知其它节点变更(通过rpc或者mq皆可),要么借助一致性hash让同一个来源的请求固定落到一个节点上。后者可以让不同节点上的本地缓存数据都不重复,从源头上避免了这个问题。

 

但是这两个方案走的都是极端,前者变更成本太高,比如需要通知上千个节点的话,这个成本难以接受。而后者的话对资源的消耗太高,而且还容易出现压力分摊不均匀的问题。所以,一般系统规模小的时候可以考虑前者,而规模越大越会选择后者

 

还有一种相对中庸一些的,以降低数据的准确性来换成本的方案。就是设置缓存定时过期或者定时往下游的分布式缓存拉取最新数据。这和前面「先DB再缓存」中提到的定时机制是一样的逻辑,胜在简单,缺点就是会存在更长时间的数据不一致。

 

 

小结一下,本地缓存的数据一致性解决方案,能彻底解决的是借助一致性hash的方案,但是成本比较高。所以,如非必要还是慎重决定要不要做本地缓存。

 

 

总结

好了,我们一起总结一下。

 

这次呢,Z哥先花了大量的篇幅和你讨论「先写DB还是缓存」的问题,并且带你层层深入,通过一点一点的演进来阐述不同的解决方案。

 

然后与你讨论了「本地缓存」的意义以及如何在「分布式缓存」和「数据库」的基础上做好数据一致性,这其中主要是多个本地缓存节点之间的数据同步问题。

 

希望对你有所启发。

 

 

这次的缓存实践是一个非常好的例子,从中我们可以看到一件事情的精细化所带来的复杂度需要更加的精细化去解决,但是又会带来新的复杂度。所以作为技术人的你,需要无时无刻考虑该怎么权衡,而不是人云亦云

 

 

 


 

相关文章:

 


 

作者:Zachary

出处:https://www.cnblogs.com/Zachary-Fan/p/dborcachefirst.html

 

 

如果你喜欢这篇文章,可以关注下我的个人公众号哦。

 

 

 

▶关于作者:张帆(Zachary,个人微信号:Zachary-ZF)。坚持用心打磨每一篇高质量原创。欢迎扫描下方的二维码~。

定期发表原创内容:架构设计丨分布式系统丨产品丨运营丨一些思考。

 

如果你是初级程序员,想提升但不知道如何下手。又或者做程序员多年,陷入了一些瓶颈想拓宽一下视野。欢迎关注我的公众号「跨界架构师」,回复「技术」,送你一份我长期收集和整理的思维导图。

如果你是运营,面对不断变化的市场束手无策。又或者想了解主流的运营策略,以丰富自己的“仓库”。欢迎关注我的公众号「跨界架构师」,回复「运营」,送你一份我长期收集和整理的思维导图。

 

 

分享到:
评论

相关推荐

    Visual Basic 与分布式监控系统——RS-232485串行通信

    ### Visual Basic 与分布式监控系统——RS-232/485串行通信 #### 知识点一:串行通信概念与分类 串行通信是指数据在一条信号线上,按位(bit)顺序传输的通信方式。与并行通信相比,串行通信的数据传输速率较低,...

    分布式缓存 原理 架构及Go语言实现-高清-完整目录

    分布式缓存作为一种在多节点之间共享和分布数据的存储方式,是现代大型分布式系统中不可或缺的一个组件。它能够有效降低数据库的读写压力,加速数据访问速度,提高系统的响应性能。在分布式缓存的实现方式中,基于...

    学习笔记:300分钟吃透分布式缓存.docx

    "学习笔记:300分钟吃透分布式缓存" 缓存是指用于加速数据交换的存储介质,可以是硬件也可以是软件。缓存存在的意义就是通过开辟一个新的数据交换缓冲区,来解决原始数据获取代价太大的问题,让数据得到更快的访问...

    DB31_T 1081-2018 天然气分布式能源系统项目服务规范.pdf

    DB31_T 1081-2018 天然气分布式能源系统项目服务规范.pdf

    重建图标缓存——Delphi工具

    在Windows操作系统中,图标缓存是一个系统组件,用于存储桌面图标、文件夹和应用程序图标的图像,以便快速显示。然而,由于系统更新、程序错误或意外情况,图标缓存可能会损坏,导致图标显示异常,如空白、错误的...

    探讨分布式系统之路

    - **避免单点故障**:为提高系统的稳定性和可靠性,分布式系统通过多节点部署确保任意一个节点出现问题时,整个系统仍能正常运行。 - **系统可管理性**:随着系统的复杂度增加,分布式架构使得系统更加模块化,便于...

    Memcached分布式缓存

    Memcached是一款高性能、分布式内存对象缓存系统,旨在通过减轻数据库负担来加速动态网络应用的速度。它通过在内存中缓存数据和对象来减少读取数据库的次数,从而提高网站或应用的响应速度。 **1.2 Memcached的特征...

    分布式采集系统在某工程中的应用.pdf

    本文主要介绍了分布式采集系统在某工程中的具体应用,并针对DB4000型DAU(数据采集单元)以及振弦式仪器进行了详细的阐述。 首先,分布式采集系统可以分为集中式、分布式和混合式三类。集中式系统适合于测点数量较...

    pg12主从高可用+timescaledb分布式节点

    接下来,我们关注TimescaleDB,这是一个针对PostgreSQL的开源时序数据库扩展,专为高效处理时间序列数据而设计。在大数据场景下,对于物联网(IoT)、监控系统、金融交易等需要记录大量时间戳数据的应用,TimescaleDB...

    HTML5本地存储——IndexedDB

    IndexedDB的应用场景广泛,比如离线应用、数据缓存、复杂表单的持久化等。它在现代Web应用中特别有用,尤其是在需要处理大量数据并提供高性能搜索功能时。例如,你可以用它来存储用户设置、游戏进度,或者在新闻阅读...

    一致的数据访问技术——ADOOLE DB

    ### 一致的数据访问技术——ADO/OLE DB #### 一、一致数据访问技术概述 随着信息技术的快速发展,数据集成的需求日益增长。为了更好地管理和访问来自不同来源的数据,Microsoft提出了**一致数据访问技术**...

    POLARDB专场:POLARDB云数据库分布式存储引擎揭秘(鸣嵩).pdf

    POLARDB云数据库分布式存储引擎揭秘PolarStore: POLARDB是阿里云推出的一款高性能、低延迟、容错性强的云数据库分布式存储引擎,它的全称为PolarStore。PolarStore通过数据平面与控制平面分离的设计,实现高性能...

    cpp-分布式缓存服务器memcachedb

    Memcached是一款高性能、分布式内存对象缓存系统,用于减少数据库负载,提高网站性能。它的工作原理是将数据存储在内存中,通过键值对的形式提供快速访问。由于数据只存在于内存中,因此在服务器重启后,数据会丢失...

    集中式还是分布式——“账务类”数据库架构的选型.pdf

    在当前信息技术领域,数据库架构的选择对于构建高效能、高稳定性的账务类系统起着决定性作用。近年来,随着大数据时代的来临,分布式数据库因为其天然的分布式特性和弹性扩展能力,成为业界热捧的技术方向之一。然而...

    分布式系统CAP

    ### 分布式系统CAP定理解析 #### 一、引言 在当今信息化时代,随着互联网技术的迅猛发展,分布式系统已经成为解决大规模数据处理和高并发访问问题的关键技术之一。在构建分布式系统的过程中,如何平衡数据一致性...

    05、缓存&分布式锁1

    在IT领域,缓存和分布式锁是提升系统性能和稳定性的重要技术手段。本文将深入探讨这两方面的内容,特别是在Spring框架中的应用。 首先,缓存的主要目的是为了提高数据访问速度,减轻数据库的压力。当某些数据访问...

    安徽医学高等专科学校——大学管理系统_db_升级后

    "安徽医学高等专科学校——大学管理系统_db_升级后" 这个标题表明我们关注的是一个针对安徽医学高等专科学校的大学管理系统的数据库(db)升级后的状态。大学管理系统通常涵盖学生信息管理、课程管理、教务管理、...

    AT45DB041D中文说明和参考程序

    附加了另外三个高手写的参考程序(是AT45DB161D的,不过AT45DB系列D版本的芯片操作基本雷同。这些参考程序都来自于OURDEV网站的高手)。个人感觉gxlujd写的那个比较好,哈哈! 感谢STM32群里面“倮倮”的提醒,有两...

Global site tag (gtag.js) - Google Analytics