关于缓存设计:缓存的设计方式

问题状况:当有大量的申请到外部零碎时,若每一个申请都须要咱们操作数据库,例如查问操作,那么对于那种数据根本不怎么变动的数据来说,每一次都去数据库外面查问,是很耗费咱们的性能 尤其是对于在海量数据中进行数据操作的时候,如果都是从 DB 中进行加载,那这是在挑战用户的耐性 简略来看,例如咱们要去小区外面理解一个人在不在家,当没有通信工具的前提下,咱们每一次都要通过小区们的保安,而后再到具体的单元楼,最终到了这家门口,最终才晓得在不在家 如果咱们换了一个比拟优良的保安,他晓得以后小区外面的特定的家外面是否有人,那这个时候,如果咱们间接去问小区保安,天然就无需跑冤枉路了,天然就进步了效率 此处简略的就能够将优良保安看做是一个缓存,咱们每一次去拜访,就会先去拜访缓存 , 这样就能极大的进步拜访效率和零碎性能 能够看出,有一个优良的保安相当重要 缓存的根本设计形式是什么样的设计缓存天然也是为了解决零碎是的低效问题,让零碎能够高性能,高并发 例如咱们间接拜访单机的数据库如mysql 也就是上千级别的 qps,如果是拜访 缓存的时候,就能达到上万,上十几万,这差距不是一点半点,是一个质的飞越 缓存的设计实际上就是 DB 和 缓存操作程序以及谁来操作的事件,大体分为如下 4 种模式 Cache Aside<!----> Read Through<!----> Write Through<!----> Write Behind Caching上述四种模式, Cache Aside 用的形式是最常应用的,咱们后续细说 后续三种模式的含意是 Read Through 是在查问操作的时候更新缓存,若缓存生效了,则是由缓存服务器本人将数据加载到缓存中Write Through 是在更新数据库的时候,如果命中了缓存,则先更新缓存,再由缓存服务器本人去更新数据,<!----> 如果是没有命中缓存,那么就间接更新数据库Write Behind Caching 通过名字咱们晓得,是在写到缓存操作之后才做些操作,实际上这种模式只更新缓存,不会更新数据库,缓存服务器会以异步的形式将数据批量更新到数据库中 很显著,这种,模式速度天然会更快,可这种模式对于保障数据库和缓存数据一致性问题,是个硬伤,且还会存在丢数据的状况,比方,咱们的缓存服务器挂掉了 Cache Aside 读写缓存模式是怎么玩的Cache Aside 读写模式缓存又是如何去解决的呢,一起来看看 Cache Aside 模式读取数据的逻辑是这个样子的: 读取数据时 先读取缓存中的数据,如果缓存中有数据,则间接返回<!----> 若缓存中没有数据,则去读数据库中的数据,并将数据同步到缓存中 写入数据时 写入数据库,写入胜利时,将缓存的数据删除掉 认真的同学可能会思考并提出这样的问题,如果我一个查问操作,当初缓存中无数据,此时会去数据库中查问,在这个过程中,另外有一个写入数据库的操作,且操作结束后,删除了缓存,这个时候,第一个操作实际上从数据库拿到的还是之前的老数据,并且会将数据放到缓存中,那么此时的数据实际上是一个老数据,也能够了解是在脏数据 这个点其实咱们就无需放心了,大佬们曾经论证过这种状况呈现的概率极低 因为咱们的写表操作是要锁表的,且咱们晓得数据库写入操作比读取操作要慢,也就是说,当同时有一个读取和写入 DB 的操作时,天然是写入的操作是要后返回后果的,此处不要杠啥读写数据量不统一的状况,咱们做比照,天然是在同等条件下比拟咯 从图中咱们晓得,同等条件下,先进行查问 DB 的操作,过程中,来了一个写入 DB 操作,天然是 查问操作先返回,写入操作再返回后果 ...

August 24, 2023 · 1 min · jiezi

关于缓存设计:玩转Java并发工具精通JUC成为并发多面手构建高性能缓存

引言《玩转Java并发工具、精通JUC、成为并发多面手》构建高性能缓存这部分的集体笔记。本节为单纯的实战,次要是把之前学习并发编程的知识点串起来。 挺有意思的一个demo,能够疾速理解到一些并发编程的时候须要留神的一些问题。目录整个高性能构建的梳理思路如下: 应用最简略的HashMap剖析HashMap实现的问题 高并发拜访反复计算性能问题复用性能较差的问题装璜模式形象计算业务 解决复用性能较差的问题应用Future改写计算实现接口 避免业务反复计算如何解决高并发拜访反复计算性能问题减少泛型缓存过期和减少随机性 为什么须要缓存过期? 避免缓存净化为什么要减少随机性? 避免同一个时刻大量缓存过期减少零碎压力整体测试代码一个简略的小demo,能够间接拷贝上面的包所属的各个版本代码到本人的我的项目浏览即可:https://gitee.com/lazyTimes/interview/tree/master/src/main/java/com/zxd/interview/mycache 一、构建步骤1. 应用最简略的HashMap最根底的版本实现非常简单,这是咱们通常会想到的利用缓存实现计划,这里应用了Lombok的@Slf4j注解进行日志打印。 整个逻辑非常简单,首先通过计算方法匹配缓存,如果有就取缓存内容,否则就调用计算方法而后把后果缓存到HashMap当中。 /** * 初版高速缓存实现 * 1. 应用简略的HashMap * 2. 在高速缓存中进行计算 * * 裸露问题: * 1. 复用性能查 * 2. HashMap线程不平安,有可能反复判断 */ @Slf4j public class MyCacheVersion1 { private final Map<String, Integer> cache = new HashMap<>(); /** * 依据参数计算结果,此参数对于同一个参数会计算同样的后果 * 1. 如果缓存存在后果,间接返回 * 2. 如果缓存不存在,则须要计算增加到map之后才返回 * @param userId * @return */ public Integer compute(String userId) throws InterruptedException { if(cache.containsKey(userId)){ log.info("cached => {}", userId); return cache.get(userId); } log.info("doCompute => {}", userId); Integer result = doCompute(userId); //不存在缓存就退出 cache.put(userId, result); return result; } private Integer doCompute(String userId) throws InterruptedException { TimeUnit.SECONDS.sleep(5); return Integer.parseInt(userId); } }初版存在较多的问题,比较显著的问题是compute这个办法在多线程的环境是不平安的,咱们能够编写测试程序验证。 ...

June 22, 2023 · 12 min · jiezi

关于缓存设计:缓存穿透缓存击穿缓存雪崩

1、背景在高并发环境下,用户长时间对服务器进行操作,可能会呈现缓存穿透、缓存击穿、缓存雪崩等问题。 2、缓存穿透2.1 什么是缓存穿透?高并发前提下,用户拜访数据库和缓存都不存在的数据称之为缓存穿透。缓存之所以存在,除了进步程序运行效率,还有就是爱护数据库。如果有大量的申请拜访数据库跟缓存都没有的数据,那么此时缓存便会生效,这大量的申请必然会间接拜访数据库。 2.2 缓存穿透的解决方案①禁用IP,限度IP的应用(因为拜访不存在的数据基本上属于歹意拜访)②限流,每秒最多拜访3次③应用布隆过滤器 2.3 布隆过滤器2.3.1 介绍布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器能够用于检索一个元素是否在一个汇合中。它的长处是空间效率和查问工夫都比个别的算法要好的多,毛病是有肯定的误识别率和删除艰难。 2.3.2 原理①首先对数据进行预加载。将数据进行hash计算得出的hash值进行特定的函数运算出数组的索引,而后在该坐标地位用二进制向量1示意。如此一来坐标为0的数据肯定不存在,所以能够禁止拜访,而坐标为1可能存在,能够放行拜访。②当一个申请过去了,通过hash加特定的函数算法而后查看数组的二进制的值来判断是否容许通过。③因为是采纳二进制向量存储,所以节俭大量内存空间。 2.3.4 hash碰撞问题如果坐标值是0,那就肯定不存在,然而坐标是1也不代表肯定存在,因为两个不同的key计算的后果是可能是雷同的坐标值,那么如何解决这个问题呢? 2.3.4.1 解决hash碰撞问题要解决hash碰撞的问题,那就要升高hash碰撞的概率。①通过增长数据的个数,来升高坐标值雷同的概率。②通过采纳多种hash函数来计算坐标值,因为不同的hash函数计算出的坐标值后果会有不同,只有正在存在的数据无论通过哪种hash函数计算,坐标值都是向量1,凡是有一个后果不是1,就能够判定这个是不存在的数据。 3、缓存击穿3.1 什么是缓存击穿?某个热点数据在缓存中忽然生效。导致大量的用户间接拜访数据库。导致并发压力过高造成异样的景象。 3.2 缓存击穿的解决方案之所以存在缓存击穿次要是因为数据被设置了超时工夫,当超时工夫一到便会被删除,而此时刚好处于高并发。①尽可能将热点数据的超时工夫,设定的长一点。②设定多级缓存,超时工夫采纳随机算法。只有不同时生效便能够无效阻止这种景象。 4、缓存雪崩4.1 什么是缓存雪崩?在缓存服务器中,因为大量的缓存数据生效,导致用户拜访的命中率过低。导致间接拜访数据库。fluashAll命令和数据大量数据同时超时可能导致缓存雪崩景象。 4.2 缓存雪崩的解决方案①设定超时工夫时,应该采纳随机算法②设定多级缓存

December 15, 2020 · 1 min · jiezi

这3个关键指标直接决定了缓存的命中率

缓存是架构设计中一个重要的手段,它的技术比较简单,同时对性能提升又有特别显著的效果,在很多地方都会用到。使用缓存需要注意3个关键因素,它们决定了缓存的有效性、缓存的使用效率、缓存实现的效果: 1、缓存键集合大小 2、缓存空间的大小 3、缓存的使用寿命 阅读本篇文章将使用5分钟的时间,帮你提升缓存命中率。 0.什么是缓存的命中率? 缓存的主要特点是一次写入多次读出,通过这种手段减少对数据库的使用,尽快从缓存中读取数据,提高性能。所以缓存是否有效,主要就是看它一次写进去的缓存能不能够多次去读出来响应业务的请求,这个指标就叫做缓存的命中率。缓存命中率怎么算呢?查询得到正确缓存结果去比上总的查询次数,得到的指标就是缓存命中率,比如说十次查询九次都能够得到缓存的正确结果,他的命中率就是90%。 影响缓存命中率的主要因素有三个,分别是缓存键集合的大小、内存空间大小和缓存的寿命。 1.缓存键的集合大小。 缓存中的每个对象都是通过缓存键进行识别的。比如我们拿到key、value结构,key是字符串abc,value是字符串hello,abc就是其中的一个缓存键。键是缓存中唯一的识别符,定位一个对象的唯一方式就是对缓存键进行精确的匹配。 比如我们想缓存每个商品的在线商品信息,就需要使用商品ID作为缓存键。换句话说,缓存键空间是你的应用能够生成的所有键的数量。从统计数字上看,应用生成的唯一键越多,重用的机会越小。比如说根据IP地址缓存天气数据,可能需要40多亿个键。但是如果基于国家缓存天气数据,那么只需要几百个缓存键就够了,全世界也不过就几百个国家。 所以要尽可能减少缓存键的数量,键的数量越少,缓存的效率越高。设计缓存的时候要关注缓存键是如何进行设计的,它的整个的集合范围,限定在一个既能够高效使用,又可以减少它的数量,这个时候缓存的性能是最好的。 2.缓存可用空间的内存大小 缓存可以使用的内存空间决定了缓存对象平均大小和缓存对象的数量。因为缓存通常是存储在内存中的,缓存对象可用的内存空间相对来说比较昂贵,而且受到严格限制。 如果想缓存更多的对象,就需要先删除老的对象,再添加新的对象。而这些老的对象被删除掉,就会影响到缓存的命中率。所以物理上缓存的空间越大,缓存的对象越多,缓存的命中率也就越高。 3.缓存对象的生存时间 缓存对象的生存时间称为TTL。 对象缓存的时间越长,被重用的可能性就越高。使缓存失效的方法有两种: 1)超时失效 超时失效是在构建缓存,也就是在写缓存的时候,每个缓存对象都设置一个超时时间,在超时之前访问缓存就会返回缓存的数据,而一旦超时缓存就失效了,这时候再访问缓存,就会返回空。 2)实时清除 而实时清除是说,当有缓存对象更新的时候,直接通知缓存将已经被更新了的数据进行清除。清除了以后,应用程序下一次访问这个缓存对象键的时候,因为缓存已经清除了,不得不到数据库中去查找读取,这个时候就会得到最新的数据。因为更新总是更新在数据库里的。 还有一种虽然时间上还没有失效,但是新的对象要写入缓存,而内存空间不够了,这个时候就需要将一些老的缓存对象清理掉,为新的缓存对象腾出空间。 内存空间清除主要使用的算法是LRU算法,LRU算法就是最近最久未用算法。清除的时候,去清除那些最近最久没有被访问过的对象,这个算法使用链表结构实现的。所有的缓存对象都放在同一个链表上。当一个对象被访问的时候,就把这个对象移到整个链表的头部。当需要通过LRU算法清除那些最近最久未用对象的时候,只需要从队列的尾部进行查找,越是在队列尾部的越是最近最久没有被访问过的,优先清除的,腾出的内存空间让新对象加入进来。 以上3个条件即为决定缓存命中率的关键要素,熟练掌握后,会对缓存有更深的理解。 以上内容摘取自拉勾《阿里前辈的架构经》 第02讲(上):分布式缓存 点击查看更多 主讲人:李智慧,前阿里巴巴技术专家,《大型网站技术架构》作者 加拉勾职场导师微信:lagouandy,可不定时参与简历1v1诊断抽奖活动,更有拉勾官方技术交流社群等你加入

May 30, 2019 · 1 min · jiezi

Java并发10-ReadWriteLock快速实现一个完备的缓存

大家知道了Java中使用管程同步原语,理论上可以解决所有的并发问题。那 Java SDK 并发包里为什么还有很多其他的工具类呢?原因很简单:分场景优化性能,提升易用性 今天我们就介绍一种非常普遍的并发场景:读多写少场景。实际工作中,为了优化性能,我们经常会使用缓存,例如缓存元数据、缓存基础数据等,这就是一种典型的读多写少应用场景。缓存之所以能提升性能,一个重要的条件就是缓存的数据一定是读多写少的. 针对读多写少这种并发场景,Java SDK 并发包提供了读写锁——ReadWriteLock,非常容易使用,并且性能很好。 什么是读写锁读写锁,并不是 Java 语言特有的,而是一个广为使用的通用技术,所有的读写锁都遵守以下三条基本原则: 允许多个线程同时读共享变量;只允许一个线程写共享变量;如果一个写线程正在执行写操作,此时禁止读线程读共享变量。读写锁与互斥锁的一个重要区别就是读写锁允许多个线程同时读共享变量,而互斥锁是不允许的,这是读写锁在读多写少场景下性能优于互斥锁的关键。但读写锁的写操作是互斥的,当一个线程在写共享变量的时候,是不允许其他线程执行读操作和写操作的。 快速实现一个缓存在下面的代码中,我们声明了一个 Cache<K, V> 类,其中类型参数 K 代表缓存里 key 的类型,V 代表缓存里 value 的类型。缓存的数据保存在 Cache 类内部的 HashMap 里面,HashMap 不是线程安全的,这里我们使用读写锁 ReadWriteLock 来保证其线程安全。ReadWriteLock 是一个接口,它的实现类是 ReentrantReadWriteLock,通过名字你应该就能判断出来,它是支持可重入的。下面我们通过 rwl 创建了一把读锁和一把写锁。 Cache 这个工具类,我们提供了两个方法,一个是读缓存方法 get(),另一个是写缓存方法 put()。读缓存需要用到读锁,读锁的使用和前面我们介绍的 Lock 的使用是相同的,都是 try{}finally{}这个编程范式。写缓存则需要用到写锁,写锁的使用和读锁是类似的。 class Cache<K,V> { final Map<K, V> m = new HashMap<>(); final ReadWriteLock rwl = new ReentrantReadWriteLock(); // 读锁 final Lock r = rwl.readLock(); // 写锁 final Lock w = rwl.writeLock(); // 读缓存 V get(K key) { r.lock(); try { return m.get(key); } finally { r.unlock(); } } // 写缓存 V put(String key, Data v) { w.lock(); try { return m.put(key, v); } finally { w.unlock(); } }}实现缓存的按需加载设计封装缓存类时,我们需要在当应用查询缓存,并且数据不在缓存里的时候,触发加载源头相关数据进缓存的操作,这也是我们需要实现的最基本的功能。下面看下利用 ReadWriteLock 来实现缓存的按需加载。 ...

May 13, 2019 · 2 min · jiezi

系统的讲解-PHP-缓存技术

概述缓存已经成了项目中是必不可少的一部分,它是提高性能最好的方式,例如减少网络I/O、减少磁盘I/O 等,使项目加载速度变的更快。 缓存可以是CPU缓存、内存缓存、硬盘缓存,不同的缓存查询速度也不一样(CPU缓存 > 内存缓存 > 硬盘缓存)。 接下来,给大家逐一进行介绍。 浏览器缓存浏览器将请求过的页面存储在客户端缓存中,当访问者再次访问这个页面时,浏览器就可以直接从客户端缓存中读取数据,减少了对服务器的访问,加快了网页的加载速度。 强缓存用户发送的请求,直接从客户端缓存中获取,不请求服务器。 根据 Expires 和 Cache-Control 判断是否命中强缓存。 代码如下: header('Expires: '. gmdate('D, d M Y H:i:s', time() + 3600). ' GMT');header("Cache-Control: max-age=3600"); //有效期3600秒Cache-Control 还可以设置以下参数: public:可以被所有的用户缓存(终端用户的浏览器/CDN服务器)private:只能被终端用户的浏览器缓存no-cache:不使用本地缓存no-store:禁止缓存数据协商缓存用户发送的请求,发送给服务器,由服务器判定是否使用客户端缓存。 代码如下: $last_modify = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);if (time() - $last_modify < 3600) { header('Last-Modified: '. gmdate('D, d M Y H:i:s', $last_modify).' GMT'); header('HTTP/1.1 304'); //Not Modified exit;}header('Last-Modified: '. gmdate('D, d M Y H:i:s').' GMT');用户操作行为对缓存的影响操作行为ExpiresLast-Modified地址栏回车有效有效页面跳转有效有效新开窗口有效有效前进/后退有效有效F5刷新无效有效Ctrl+F5刷新无效无效文件缓存数据文件缓存将更新频率低,读取频率高的数据,缓存成文件。 比如,项目中多个地方用到城市数据做三级联动,我们就可以将城市数据缓存成一个文件(city_data.json),JS 可以直接读取这个文件,无需请求后端服务器。 全站静态化CMS(内容管理系统),也许大家都比较熟悉,比如早期的 DEDE、PHPCMS,后台都可以设置静态化HTML,用户在访问网站的时候读取的都是静态HTML,不用请求后端的数据库,也不用Ajax请求数据接口,加快了网站的加载速度。 ...

May 10, 2019 · 2 min · jiezi

分布式系统关注点18缓存穿透和缓存雪崩到底啥区别

如果第二次看到我的文章,欢迎文末扫码订阅我个人的公众号(跨界架构师)哟~ 本文长度为2805字,建议阅读8分钟。坚持原创,每一篇都是用心之作~ 有句话说得好,欲要使其毁灭,先要使其疯狂。当你沉浸在缓存所带来的系统tps飙升的喜悦中时,使你系统毁灭的种子也已经埋在其中。 而且,你所承载的tps越高,它所带来的毁灭性更大。 在前两篇《360°全方位解读「缓存」》和《先写DB还是「缓存」?》中,我们已经对缓存有了一定的认识,并且知道了关于缓存相关的「一致性」问题的最佳实践。 这次,我们就来聊聊隐藏在缓存中的毁灭性种子是什么? 我们从前一篇文章《先写DB还是「缓存」?》中多次提到的「cache miss」说起。 缓存雪崩在前一篇文章《先写DB还是「缓存」?》中,我们多次提到了「cache miss」这个词,利用「cache miss」来更好的保障DB和缓存之间的数据一致性。 然而,任何事物都是有两面性的,「cache miss」在提供便利的同时,也带来了一个潜在风险。 这个风险就是「缓存雪崩」。 在图中的第二步,大量的请求并发进入,这里的一次「cache miss」就有可能导致产生「缓存雪崩」。 不过,虽然「cache miss」会产生「缓存雪崩」,但「缓存雪崩」并不仅仅产生于「cache miss」。 雪崩一词源于「雪崩效应」,是指像「多米勒骨牌」这样的级联反应。前面没顶住,导致影响后面,如此蔓延。(关于对应雪崩的方式参考之前的文章,文末放链接) 所以「缓存雪崩」的根本问题是:缓存由于某些原因未起到预期的缓冲效果,导致请求全部流转到数据库,造成数据库压力过重。 因此,流量激增、高并发下的缓存过期、甚至缓存系统宕机都有可能产生「缓存雪崩」问题。 怎么解决这个问题呢?宕机可以通过做高可用来解决(可以参考之前的文章,文末放链接)。而在“流量激增”、“高并发下的缓存过期”这两种场景下,也有两种方式可以来解决。 加锁排队通过加锁或者排队机制来限制读数据库写缓存的线程数量。比如,下面的伪代码就是对某个key只允许一个线程进入的效果。 key = "aaa";var cacheValue = cache.read(key);if (cacheValue != null) { return cacheValue;}else { lock(key) { cacheValue = cache.read(key); if (cacheValue != null) { return cacheValue; } else { cacheValue = db.read(key); cache.set(key,cacheValue); } } return cacheValue;} 这个比较好理解,就不废话了。 ...

April 25, 2019 · 1 min · jiezi

分布式系统关注点——先写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)的订阅吧。到这里可能有的小伙伴要问了,“如果上了分布式缓存,还需要本地缓存吗?”。本地缓存还要不要?在解答这个问题之前我们先来思考一个问题,一个分布式系统最重要的价值是什么?是「无限扩展」,只要堆硬件就能应对业务增长。要达到这点的背后需要满足一个特性,就是程序要「无状态」。那么既想引入缓存来加速,又要达到「无状态」,靠的就是分布式缓存。所以,能用分布式缓存解决的问题就尽量不要引入本地缓存。否则引入分布式缓存的作用就小了很多。但是在少数场景下,本地缓存还是可以发挥其价值的,但是我们需要仔细识别出来。主要是三个场景:不经常变更的数据。(比如一天甚至好几天更新一次的那种)需要支撑非常高的并发。(比如秒杀)对数据准确性能容忍的场景。(比如浏览量,评论数等)不过,我还是建议你,除了第二种场景,否则还是尽量不要引入本地缓存。原因我们下面来说说。其实这个原因的根本问题就是在引入了本地缓存后,本地缓存(进程内缓存)、分布式缓存(进程外缓存)、数据库这三者之间的数据一致性该怎么进行呢?本地缓存、分布式缓存、db之间的数据一致性如果是个单点应用程序的话,很简单,将本地缓存的操作放在最后就好了。可能你会说本地缓存修改失败怎么办?比如重复key啊什么的异常。那你可以反思一下为这种数据为什么可以成功的写进数据库。。。但是,本地缓存带来的一个巨大问题就是:虽然一个节点没问题,但是多个本地缓存节点之间的数据如何同步?解决这个问题的方式中有两种和之前我们聊过的Session问题(《做了「负载均衡」就可以随便加机器了吗?》)是类似的。要么是由接收修改的节点通知其它节点变更(通过rpc或者mq皆可),要么借助一致性hash让同一个来源的请求固定落到一个节点上。后者可以让不同节点上的本地缓存数据都不重复,从源头上避免了这个问题。但是这两个方案走的都是极端,前者变更成本太高,比如需要通知上千个节点的话,这个成本难以接受。而后者的话对资源的消耗太高,而且还容易出现压力分摊不均匀的问题。所以,一般系统规模小的时候可以考虑前者,而规模越大越会选择后者。还有一种相对中庸一些的,以降低数据的准确性来换成本的方案。就是设置缓存定时过期或者定时往下游的分布式缓存拉取最新数据。这和前面「先DB再缓存」中提到的定时机制是一样的逻辑,胜在简单,缺点就是会存在更长时间的数据不一致。小结一下,本地缓存的数据一致性解决方案,能彻底解决的是借助一致性hash的方案,但是成本比较高。所以,如非必要还是慎重决定要不要做本地缓存。总结好了,我们一起总结一下。这次呢,Z哥先花了大量的篇幅和你讨论「先写DB还是缓存」的问题,并且带你层层深入,通过一点一点的演进来阐述不同的解决方案。然后与你讨论了「本地缓存」的意义以及如何在「分布式缓存」和「数据库」的基础上做好数据一致性,这其中主要是多个本地缓存节点之间的数据同步问题。希望对你有所启发。这次的缓存实践是一个非常好的例子,从中我们可以看到一件事情的精细化所带来的复杂度需要更加的精细化去解决,但是又会带来新的复杂度。所以作为技术人的你,需要无时无刻考虑该怎么权衡,而不是人云亦云。相关文章:分布式系统关注点——360°全方位解读「缓存」分布式系统关注点——做了「负载均衡」就可以随便加机器了吗?作者:Zachary出处:https://www.cnblogs.com/Zacha…如果你喜欢这篇文章,可以点一下文末的「赞」。这样可以给我一点反馈。: )谢谢你的举手之劳。▶关于作者:张帆(Zachary,个人微信号:Zachary-ZF)。坚持用心打磨每一篇高质量原创。欢迎扫描下方的二维码~。定期发表原创内容:架构设计丨分布式系统丨产品丨运营丨一些思考。如果你是初级程序员,想提升但不知道如何下手。又或者做程序员多年,陷入了一些瓶颈想拓宽一下视野。欢迎关注我的公众号「跨界架构师」,回复「技术」,送你一份我长期收集和整理的思维导图。如果你是运营,面对不断变化的市场束手无策。又或者想了解主流的运营策略,以丰富自己的“仓库”。欢迎关注我的公众号「跨界架构师」,回复「运营」,送你一份我长期收集和整理的思维导图。 ...

April 11, 2019 · 1 min · jiezi