缓存中间件的选型
在选择缓存中间件时,通常需要考虑业务需求、性能、可靠性和成本等因素。目前市场上比较流行的缓存中间件包括Memcached、MongoDB和Redis。
-
Memcached是一个高性能的分布式内存对象缓存系统,主要用于动态Web应用以减轻数据库负载。它通过将经常访问的数据存储在内存中,以提高数据访问速度。然而,Memcached不支持数据的持久化,如果需要重启或者宕机,可能会导致数据丢失。
-
MongoDB则是一个介于关系数据库和非关系数据库之间的文档型数据库,具有高度的可扩展性和丰富的查询语言。它的缓存机制主要是通过将热数据存储在内存中来提高查询速度。但是MongoDB的主要功能还是数据存储,其缓存机制相对较弱。
-
Redis是一个开源的内存数据结构存储系统,支持多种数据结构如字符串、列表、集合、散列和有序集合等。它具有高速读写、支持数据持久化和发布/订阅等功能。并且,通过采用适当的数据淘汰策略,可以保证在内存使用达到上限时仍能正常运行。
考虑到目前redis比memcached更流行,下面主要介绍redis。
缓存何时存储数据
- 先尝试从缓存中读取数据。
- 若缓存中没有数据或者数据过期,再从数据库中读取数据保存到缓存中。
- 最终把缓存数据返回给调用方。
这种逻辑唯一麻烦的地方是,当用户发来大量的并发请求时,它们会发现缓存中没有数据,那么所有请求会同时挤在第2)步,此时如果这些请求全部从数据库读取数据,就会让数据库崩溃。
数据库的崩溃可以分为3种情况。
1)单一数据过期或者不存在,这种情况称为缓存击穿。
解决方案:第一个线程如果发现Key不存在,就先给Key加锁,再从数据库读取数据保存到缓存中,最后释放锁。如果其他线程正在读取同一个Key值,那么必须等到锁释放后才行。关于锁的问题前面已经讲过,此处不再赘述。
2)数据大面积过期或者Redis宕机,这种情况称为缓存雪崩。解决方案:设置缓存的过期时间为随机分布或设置永不过期即可。3)一个恶意请求获取的Key 不在数据库中,这种情况称为缓存穿透。
比如正常的商品ID是从100000到1000000(10万到100万之间的数值),那么恶意请求就可能会故意请求2000000以上的数据。这种情况如果不做处理,恶意请求每次进来时,肯定会发现缓存中没有值,那么每次都会查询数据库,虽然最终也没在数据库中找到商品,但是无疑给数据库增加了负担。这里给出两种解决办法。
①在业务逻辑中直接校验,在数据库不被访问的前提下过滤掉不存在的Key。
②针对恶意请求的Key存放一个空值在缓存中,防止恶意请求骚扰数据库。
最后说一下缓存预热。
上面这些逻辑都是在确保查询数据的请求已经过来后如何适当地处理,如果缓存数据找不到,再去数据库查询,最终是要占用服务器额外资源的。那么最理想的就是在用户请求过来之前把数据都缓存到Redis中。这就是缓存预热。其具体做法就是在深夜无人访问或访问量小的时候,将预热的数据保存到缓存中,这样流量大的时候,用户查询就无须再从数据库读取数据了,将大大减)数据读取压力。
缓存更新策略
缓存更新是redis为了节约内存而设计出来的一个东西,主要是因为内存数据宝贵,当我们向redis插入太多数据,此时就可能会导致缓存中的数据过多,所以redis会对部分数据进行更新,或者把他叫为淘汰更合适。
-
内存淘汰:redis自动进行,当redis内存达到咱们设定的max-memery的时候,会自动触发淘汰机制,淘汰掉一些不重要的数据(可以自己设置策略方式)
-
超时剔除:当我们给redis设置了过期时间ttl之后,redis会将超时的数据进行删除,方便咱们继续使用缓存
-
主动更新:我们可以手动调用方法把缓存删掉,通常用于解决缓存和数据库不一致问题
如何更新缓存
更新缓存的步骤比较简单,更新数据库和更新缓存,注意的是,这两步需要考虑比较多的问题:
- 先更新数据库还是先更新缓存?更新缓存时先删除还是直接更新?
- 假设第一步成功了,第二步失败了怎么办?
- 假设两个线程同时更新同一个数据,A线程先完成第一步,B线程先完成第二步怎么办?
先更新缓存,再更新数据库
1、线程1先更新缓存成功,但是网络原因写数据库失败,就会导致缓存是最新数据,而数据库的数据为旧数据,那缓存就是脏数据。
2、线程2读取缓存中数据,而这个数据数据库中却不存在,数据库都不存在的数据。
另外没有异常的情况也会存在问题:
1、线程A、线程B 同时更新一条数据。
2、更新缓存的顺序是先 A 后 B。
3、更新数据库的顺序是先 B 后 A。

先更新数据库,再更新缓存
1、线程1先更新数据库成功,但是由于网络卡顿更新缓存失败,从而导致缓存中的数据为旧数据。
2、线程2从缓存中读取数据,缓存中的数据为旧数据,从而导致数据库与缓存数据不一致。
另外没有异常的情况也会存在问题:
1、线程A、线程B 同时更新一条数据。
2、更新数据库的顺序是先 A 后 B。
3、更新缓存的顺序是先 B 后 A。

先删除缓存,再更新数据库
1、线程A先删除缓存。
2、线程B读取缓存,发现缓存未命中,于是从数据库读取数据。
3、线程A接着更新数据库。
4、线程B将从数据库读取到的数据同步到缓存中。

针对这种场景,可以采用“延迟双删”的策略。既然读请求会把旧值写回到缓存中,因此可以在数据库写请求处理完之后,等到差不多的时间再次删除缓存。

“延迟双删”的方案关键在于时间N的判断,如果太短,线程A第二次删除缓存早于线程B将脏数据写回到缓存的时间,那么相当于做了无用功;如果太长,则在延迟双删成功前,新请求从缓存中读到的是脏数据。
先更新数据库,再删除缓存

在更新数据库与删除缓存之间,可能会被其它线程从缓存中读到旧值。在先更新数据库后删除缓存这个场景下,不一致窗口仅仅是 T2 到 T3 的时间,内网状态下通常不过 1ms,在大部分业务场景下我们都可以忽略不计。因为大部分情况下一个用户的请求很难能再 1ms 内快速发起第二次。
真实场景下,还是会有一个情况存在不一致的可能性,这个场景是读线程发现缓存未命中,于是读写并发时,读线程将数据库读到的旧值更新到缓存中。并发情况如下:

1276

被折叠的 条评论
为什么被折叠?



