前言
随着我们业务量的增长,系统面对的压力也陡然上升,大量的读写请求到数据库往往会伴随着各式各样的问题,可能仅仅是一条慢SQL,就有可能拖垮整个系统服务。通常这个时候,我们除了做数据库的读写分离架构,还会对数据库进行分库分表。但是可能有些一成不变或者极少时间触发变更的数据,像类目、类目属性等,大量的针对类目维度的读数据库也会给数据库带来各种压力,通常会以NoSql数据库与关系型数据库互相搭配的方式,以用来更好的服务与我们的业务发展。
为什么要使用缓存
伴随着业务量的增长,数据量、并发量也会翻个很多倍,这个时候数据库的IO也逐渐成了系统的性能瓶颈,这个时候我们可以利用缓存中间件,来提升访问速度降低请求响应时间,以便提升整体系统性能。
说到缓存,大家可能都会想到memcached、redis、tair等,日常使用中可能大部分场景都是简单的读取缓存,有数据就返回,没有数据就查数据库,然后再写入缓存中。但是真正的缓存读写策略是比较复杂的,不仅要考虑各种场景,还需针对业务的各种因素权衡利弊的,比如“缓存与数据库”以谁为准、数据更新时是“淘汰缓存”还是“更新缓存”、缓存和数据库的操作时序如何保障等。
缓存的读写策略
缓存读写策略中有三种,分别是旁路缓存模式策略、读写穿透策略、异步缓存写入策略,日常开发中最常用的就是旁路缓存模式策略,它常常用于读请求比写请求多的场景下,具体的策略定义如下:
旁路缓存策略(Cache Aside Pattern):
- 读:先读取缓存,读取到就直接返回;没有读取到缓存,就读取数据库,然后拿出数据后放入缓存,同时返回响应。
- 写:先更新数据库,然后再删除缓存
读写穿透策略(Read/Write Through Pattern):
- 先:先读取缓存,读取到就直接返回;没有读取到缓存,就读取数据库,然后拿出数据后放入缓存,同时返回响应。
- 写:与旁路缓存策略相反,先看缓存中是否存在该数据,存在则先更新缓存,再更新数据库,也就是同时更新数据库跟缓存;不存在直接更新数据库。
异步缓存写入策略(Write Behind Pattern):
- 它跟读写穿透策略类似,但是它们的不同点就是:读写穿透是同步更新DB和cache,而异步缓存只更新缓存,不直接更新数据库,而是通过异步批量的方式来更新数据库。
关于这三种读写策略详细的解析,本文暂不展开,且听下回书讲解。
Cache Aside Pattern
上文说到旁路缓存策略是日常使用最多的一种策略,我们通过一个案例分析来看看是如何使用的。
假设现在数据库有个订单,字段order_id为订单号,字段price为订单价格。缓存中存储了以订单号为key的缓存(20221126:100),如下所示:
需求:由于可能订单算错了价格,需要将order_id为20221126的订单的价格改成300,我们该如何去做?
1、先更新数据库,在更新缓存
首先说明,这种读写策略下可能会存在数据不一致的情况的,假如A请求是将order_id=20221126的价格改为300,请求B是将order_id=20221126的价格改为100,那么最终情况应该是order_id=20221126的价格为100,且数据库与缓存中的数据一致,那么如果没有并发等控制,那么如下图所示:
因为更新数据库和更新缓存是两个非原子性的操作,而且没有并发控制等策略,所以请求A/B执行的顺序是不能保证的,如果你是因为系统并发量小而采取这种操作,那么是有问题的,这种情况发生的概率还是很高的,它不一定是并发导致,还有可能因为查询接口的耗时或者网络波动导致操作并发,最终导致数据的不一致。
2、先更新数据库,在删除缓存
其实解决上面那样的问题也很简单,我们不采取缓存更新的方案,而是删除缓存。而读取数据的时候,如果缓存没命中,就去查数据库,然后再回填到缓存中,如下所示:
这种解决方案就是上文的旁路缓存策略,它是以数据库的数据为基准的,而缓存是按需才加载,一般被分为读策略和写策略。此示例只是作为数据不一致的推导。
其实像Cache Aside这种缓存策略,也是有缺点的即也会出现数据不一致的情况,同时操作数据库以及缓存,任何一个操作失败都会造成数据不一致。假如设置了数据库,但是操作缓存失败了,没有清除掉,就会导致数据不一致。
而且这种方案也有可能出现脏数据可能,请求A读取缓存,此时缓存刚好失效,而更新操作执行速度比读操作执行快,读线程最后更新缓存,但是是个老数据,虽然更新操作最后会删除缓存,但是在这中间,脏数据就会产生。
Cache Aside策略有什么缺陷
最大的缺陷就是当我们写入操作很频繁的时候,缓存中的数据就会被频繁的删除掉,会直接导致缓存命中率下降,但是如果我们业务中又必须要很高的缓存命中率怎么办呢?
- 在更新数据库记录的时候也更新缓存,我们在代码写更新缓存前加上分布式锁,每次运行一个线程更新缓存,防止并发问题,这种做法就是会对写入性能带了一定影响,毕竟加了锁。
- 第二种方案,同样也是更新数据库的时候更新缓存,但是这次我们把缓存设置一个过期时间,一般很短,即使根据业务需求计算,即使出现了数据不一致的情况,也是会很快就过期了,这种情况是可以接受的。
还有就是首次的请求的数据一定不在缓存的问题,这种解决方式也简单,我们可以在系统加载的时候将热点数据提前写入缓存中。