性能设计之缓存

缓存

在系统中最消耗性能的地方就是对数据库的访问了,一般来说,增、删、改操作不会出现什么性能问题,除非索引太多,并且数据量有十分庞大的情况下,这三个操作才会导致性能问题。一般可以限制单表索引的数量来提升性能,比如单表的索引数量不能超过5个。

绝大多数情况下,性能问题都出在查询上,select操作提供了非常丰富的语法,这些语法包括函数,子查询,like子句,where子句等,这些查询都是非常消耗性能的。大部分应用都是读多写少的应用,所以查询慢的问题会被放大了,导致慢查询成为了系统性能的瓶颈,这是就需要用到缓存来提高系统的性能。

缓存为什么能提供系统性能?

缓存通过减少系统对数据库的访问量来提高系统性能。试想一下,如果有100个请求同时请求同一个数据,没有加缓存之前,需要访问100次数据库,而加了缓存之后,可能只需访问1次数据库,剩下的99个请求的数据从缓存中取,大大的较少了数据库的访问量,虽然同样需要访问100次,但数据库的读取性能和缓存的读取性能不在一个级别上,所以对系统性能提升显著。为什么说可能需要访问1次数据库呢,这个和过期有关,后面会讲。

更新模式

既然知道了缓存对系统性能提升显著,那下面先来了解一下缓存如何更新吧。

Cache Aside(推荐)

这应该是最常用的更新模式了,这种模式大致流程如下:

  1. 读取

    • 如果缓存中没有,则再从数据库中读取数据,得到数据之后,放入缓存。
    • 如果缓存中有,取到后直接返回。
  2. 更新

    ​ 先更新数据库里的数据,成功后,让缓存失效。

为什么是让缓存失效而不是更新缓存呢?

主要是因为两个并发写操作导致脏数据。试想一下,有两个线程A和B,分别要将资源A的值修改为1和2,线程A先到达数据库把数据更新为1,但还没更新缓存,由于时间片用完了,此时线程B获得了CPU,并在这期间把数据库资源A的值和缓存的值都更新为2,线程B结束后,线程A重新获得CPU,执行更新缓存,把资源A的值改为1,线程A结束。此时,数据库中A的值为2,而缓存中A的值为1,数据不一致。所以让缓存失效就不会有这个问题,保证缓存中的数据和数据库的保持一致。

那是不是Cache Aside模式就不会有并发问题了呢?

不是的。 比如,一个读操作,没有命中缓存,就去数据的读取数据(A=1),此时一个写操作,更新数据库数据(A=2)并让缓存失效,此时读操作把读取到的数据(A=1)写到缓存中,导致脏数据。

这种情况理论上会出现,但现实情况中出现的几率极低。要这种情况出现必须在一个读操作发生时,有一个并发写操作,并且既要读操作要于写操作写入前读取,又要后于写操作写入缓存。满足这种条件的概率并不大。

基于出现上面所描述的问题,目前有两种比较合理的解决方案:

  1. 通过2PC这种保证数据的一致性(复杂);
  2. 通过降低并发时脏数据的概率,并设置合理的过期时间(简单,但存在一定时间内的错误率,一般可以接受)。

Read/Write Through

在这种模式下,对于应用程序来说,所有的读写请求都是直接和缓存打交道,关于数据库的数据完全由缓存服务来更新(更新同步为同步操作)。

这种模式下流程就相当简单了,完全就是对缓存的读写。

缺点:这种模式对缓存服务有强依赖性,要求缓存具备高可用性。所以应有没有上一种普遍。

Write Behind Caching

其实这个模式就是Read/Write Through的一个变种,区别就在于前者是异步更新,后者是同步更新。

既然是异步更新数据库,他的相应速度比Read/Write Through还要高,并且还能合并对同一个数据的多次操作。这个有点像MySQL的buffer pool的刷盘操作。

缺点:异步就代表数据不是强一致性的,还存在数据丢失的风险,实现逻辑也较为复杂。

设计思路

无状态的服务

在分布式系统中,无状态的服务有利于横向扩展,所以缓存也应该独立于业务服务在外,设计成一个独立的服务,使业务服务变成无状态的。很多公司都选择是用Redis来搭建他们的缓存系统,取决于其高速的读写性能。

命中率

一个缓存服务的好坏主要看命中率,一般来说,命中率保存在80%以上已经算很高了,但我们不能为了提高命中率而把数据库的全部数据的写到缓存了,这个是不符合缓存的设计理念,而且需要极大的内存空间。**通常来说,应该只有小部分热点数据写到缓存。**缓存是通过牺牲强一致性来换取性能的,并不是所有的业务的适合使用缓存。

有效时间

缓存数据的有效时间不易过短,不易过长,不易过于集中。

  • 过短,会增加数据库访问的次数。

  • 过长容易不使用的数据一直停留在缓存中,浪费空间,并且一旦产生脏数据,过程的有效时间会导致脏数据迟迟无法失效,进而导致影响更多的业务。

  • 过于集中,会导致缓存雪崩。

淘汰策略

当内存不足时,缓存系统就要按照淘汰策略,把不适合留在缓存的数据淘汰掉,腾出位置给新数据。下面就以Redis为例,给出了淘汰策略:

  • noeviction: 不删除策略, 达到最大内存限制时, 如果需要更多内存, 直接返回错误信息(有极少数会例外, 如 DEL )。
  • allkeys-lru: 所有key通用,优先删除最近最少使用(less recently used ,LRU) 的 key。(推荐)
  • volatile-lru: 只限于设置了 expire 的部分,优先删除最近最少使用(less recently used ,LRU) 的 key。
  • allkeys-random: 所有key通用,随机删除一部分 key。
  • volatile-random: 只限于设置了 expire 的部分,随机删除一部分 key。
  • volatile-ttl: 只限于设置了 expire 的部分,优先删除剩余时间(time to live,TTL) 短的key。

可根据项目实际情况进行选择。

小结

缓存是为了加速数据的访问,在数据库之上的一直机制,并非所有业务都适合使用缓存,要根据具体情况选择更新策略和淘汰策略。

发布了6 篇原创文章 · 获赞 25 · 访问量 2502

猜你喜欢

转载自blog.csdn.net/qq_36011946/article/details/104164031