当前位置: 首页 > 科技观察

不同业务场景如何选择缓存读写策略?

时间:2023-03-17 10:01:08 科技观察

大家可能觉得缓存的读写很简单,只需要先读取缓存,缓存不命中就从数据库中查询,查询到则返回缓存即可。其实针对不同的业务场景,缓存的读写策略也是不同的。我们在选择策略的时候,还需要考虑很多因素,比如是否有可能将脏数据写入缓存,策略的读写性能如何,缓存命中率是否下降等等在。下面我就以标准的“缓存+数据库”场景为例,带大家分析一下经典的缓存读写策略及其适用场景。这样,你就可以在日常工作中根据不同的场景选择不同的阅读和写作策略。CacheAside(旁路缓存)策略让我们考虑最简单的业务场景。比如你的电商系统中有一个用户表。表中只有两个字段,ID和age。在缓存中,我们以ID为key存储用户的年龄信息。那么当我们要将用户ID1的年龄从19岁改成20岁时,我们该怎么办呢?你可能会产生这样的想法:先更新数据库中ID为1的记录,再更新缓存中Key为1的数据。这种思路会造成缓存和数据库的数据不一致。例如,A请求将数据库中ID为1的用户年龄从19岁改为20岁,同时请求B也开始更新ID为1的用户数据,将数据库中记录的年龄改为21岁,然后更改缓存中的年龄。年龄为21岁的用户。紧接着,A请求开始更新缓存数据,这会将缓存中的年龄更改为20。此时,用户在数据库中的年龄为21,但用户的年龄在缓存是20。为什么会出现这个问题?因为更改数据库和更改缓存是两个独立的操作,并且我们对这些操作没有任何并发??控制。那么当两个线程并发更新的时候,就会因为写入顺序的不同导致数据不一致。另外,直接更新缓存还有一个问题就是更新丢失。还是以我们的电子商务系统为例。如果电子商务系统中的账户表有ID、账户名、金额三个字段,此时缓存中保存的不仅仅是金额信息,而是完整的账户信息。更新缓存中的账户金额时,需要从缓存中查询完整的账户数据,修改金额后将金额写入缓存。这个过程中也会出现并发问题。比如原来的amount是20,A请求从缓存中读取数据,将amount加1,变为21。在写入缓存之前,另一个请求B也读取了缓存数据后,将金额加1,修改为21,两个请求同时将金额写回缓存。此时缓存中的数量是21,但是我们实际期望数量增加2,也是一个比较大的数量。问题。那么我们如何解决这个问题呢?其实我们在更新数据的时候可以不更新缓存,而是删除缓存中的数据。在读取数据的时候,我们发现缓存中没有数据,然后从数据库中读取数据,更新到缓存中。这种策略是我们最常用的缓存策略,即CacheAside策略(也称为旁路缓存策略)。该策略中的数据是基于数据库中的数据,缓存中的数据是按需加载的。分为读策略和写策略。读取策略的步骤是:从缓存中读取数据;如果缓存命中,直接返回数据;如果缓存没有命中,则从数据库中查询数据;查询完数据后,将数据写入缓存,返回给用户。编写策略的步骤是:更新数据库中的记录;删除缓存记录。你可能会问,在写策略中,是否可以先删除缓存,再更新数据库?答案是否定的,因为缓存的数据也可能存在不一致的情况。我将以用户表为例进行说明。假设一个用户的年龄是20岁,请求A想要更新用户的年龄为21岁,那么就会删除缓存中的内容。这时另一个请求B想要读取用户的年龄。它查询缓存发现未命中后,会从数据库中读取年龄20写入缓存,然后请求A继续改数据库,将用户的年龄更新为21,这就创建了缓存和数据库之间的不一致。那么是不是像CacheAside策略一样,先更新数据库,再删除缓存呢?事实上,它在理论上仍然存在缺陷。如果缓存中不存在某个用户数据,则请求A读取数据并从数据库中查询年龄为20,另一个请求B在数据未写入缓存时更新该数据。它将数据库中的年龄更新为21并清除缓存。此时请求A将从年龄为20的数据库中读取的数据写入缓存,导致缓存与数据库数据不一致。不过,这种问题出现的概率并不高。原因是缓存的写入通常比数据库的写入要快得多,所以在实践中,请求B很难在请求A更新之前更新数据库并清除缓存。缓存情况。一旦请求A在请求B清除缓存之前更新了缓存,下一次请求会重新从数据库中加载数据,因为缓存是空的,所以不会出现这种不一致的情况。CacheAside策略是我们日常开发中使用频率最高的缓存策略,但是我们在使用的时候也要学会根据情况进行更改。比如当一个新用户注册的时候,按照这个更新策略,你要先写入数据库,然后清缓存(当然缓存里面没有数据让你清)。但是当我注册用户后立即读取用户信息,数据库主从分离时,会出现主从延迟导致无法读取用户信息的情况。解决这个问题的方法是将新数据插入数据库后写入缓存,这样后续的读请求就会从缓存中读取数据。并且由于是新注册的用户,不会并发更新用户信息。CacheAside最大的问题是,当写入频繁时,缓存中的数据会被频繁清理,这会对缓存命中率造成一定的影响。如果你的业务对缓存命中率有严格的要求,可以考虑两种方案:一种方式是在更新数据的同时更新缓存,只需要在更新缓存之前加一个分布式锁即可,因为这种方式同时通过只允许一个线程去更新缓存,不会有并发问题。当然,这样做会对写入的性能产生一定的影响;另一种方式是在更新数据的时候更新缓存,但是给缓存加一个更短的过期时间,这样即使缓存不一致,缓存的数据也会很快过期,对业务的影响是可以接受的。当然,除了这种策略,还有其他几种计算机领域的经典缓存策略,它们也有各自适用的使用场景。Read/WriteThrough(读透/写透)策略该策略的核心原理是用户只与缓存打交道,缓存与数据库通信写入或读取数据。这就像你向上级报告,然后直接上级报告他的上级。您不能超出该级别进行报告。WriteThrough的策略是这样的:首先检查缓存中是否已经存在要写入的数据,如果已经存在,则更新缓存中的数据,缓存组件会同步更新到数据库中,如果存在缓存不存在,我们称这种情况为“WriteMiss(写入失败)”。一般来说,我们可以选择两种“WriteMiss”方式:一种是“WriteAllocate(写入分配)”,也就是写入缓存中相应的位置,然后缓存组件会同步更新到数据库中;另一种是“No-writeallocate(不通过写入分配)”,该方法不写入缓存,而是直接更新到数据库。在WriteThrough策略中,我们一般会选择“No-writeallocate”方式,因为无论使用哪种“WriteMiss”方式,我们都需要同步更新数据到数据库,而“No-writeallocate”方式与“WriteAllocate”相比,还减少了一个缓存的写入,可以提高写入的性能。ReadThrough策略更简单。它的步骤是:首先查询缓存中的数据是否存在,存在则直接返回。如果不存在,缓存组件负责从数据库同步加载数据。下面是ReadThrough/WriteThrough策略示意图:ReadThrough/WriteThrough策略的特点是缓存节点而不是用户与数据库进行交互。在我们的开发过程中,它比CacheAside策略更不常用,因为我们经常使用Memcached和Redis都没有提供写入数据库或自动加载数据库数据的功能。而我们在使用本地缓存的时候可以考虑使用这种策略。比如上一节提到的本地缓存GuavaCache中的LoadingCache,就有ReadThrough策略的影子。我们看到WriteThrough策略中写数据库是同步的,这对性能会有比较大的影响,因为相对于writecache,同步写数据库的延迟要高很多。那么我们可以异步更新数据库吗?这就是我们接下来要提到的“WriteBack”策略。WriteBack(回写)策略该策略的核心思想是在写入数据时只写入缓存,并将缓存块标记为“脏”。脏块中的数据只有在再次使用时才会写入后端存储。需要注意的是,在“WriteMiss”的情况下,我们使用的是“WriteAllocate”的方式,即在写入后端存储的同时写入缓存,这样我们只分配更新的就够了缓存而不是后端存储。我把Writebackstrategy的示意图放下面:如果使用WriteBack策略,读策略也变了。我们在读取缓存的时候,如果发现缓存命中,就直接返回缓存的数据。如果缓存未命中,则查找可用的缓存块。如果缓存块“脏”,则将缓存块中之前的数据写入后端存储,并将后端存储中的数据加载到缓存块中,如果不脏,则缓存组件将加载数据中的数据后端存入缓存,最后我们设置缓存不脏,返回数据。你找到了吗?事实上,这种策略并不能应用到我们常见的数据库和缓存场景中。它是计算机体系结构中的一种设计。比如我们往磁盘写数据的时候就用到了这个策略。无论是操作系统层面的PageCache,还是日志的异步刷新,或者是将消息队列中的消息异步写入磁盘,大部分都是采用这种策略。因为这种策略的性能优势是毋庸置疑的,它避免了直接写入磁盘带来的随机写入问题。毕竟随机I/O写内存和写磁盘的延迟相差几个数量级。但是因为缓存一般使用内存,而内存是非持久化的,一旦缓存机器断电,原缓存中的脏块数据就会丢失。所以你会发现系统断电后,之前写入的一些文件会丢失,因为PageCache还没来得及刷盘。当然,在某些场景下你仍然可以使用这个策略。在使用的时候,给大家一个实用的建议:当你向低速设备写入数据时,可以将数据暂时存放在内存中一段时间??,甚至可以做一些统计汇总,然后周期性的进行刷新到低速设备。比如你在统计你的接口响应时间的时候,你需要把每个请求的响应时间打印到日志中,然后监控系统收集日志然后统计。但是如果每次请求都打印日志无疑会增加磁盘I/O,那么最好将响应时间暂存一段时间,简单统计一下平均耗时后,每次请求的次数——消费间隔等,然后定期,批量打印到日志。小结本文主要带大家了解缓存的几种策略以及每种策略的使用场景。需要大家掌握的一点是:CacheAside是我们在使用分布式缓存时最常用的策略,实际工作中可以直接使用。Read/WriteThrough和WriteBack策略需要缓存组件的支持,更适合你在实现本地缓存组件时使用;WriteBack策略是计算机体系结构中的一种策略,但是写策略中的只写缓存、异步写进入后端存储的策略应用场景很多。