如何通过缓存来提升系统性能?

共 2805字,需浏览 6分钟

 ·

2021-07-27 21:04

微信搜索逆锋起笔关注后回复编程pdf
领取编程大佬们所推荐的 23 种编程资料!


来源:blog.csdn.net/qq_36011946/article/details/104164031

缓存

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

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

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

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

更新模式

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

Cache Aside(推荐)

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

读取

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

更新

  • 先更新数据库里的数据,成功后,让缓存失效。
为什么是让缓存失效而不是更新缓存呢?

主要是因为两个并发写操作导致脏数据。试想一下,有两个线程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)写到缓存中,导致脏数据。

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

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

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

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。

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

小结

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

逆锋起笔是一个专注于程序员圈子的技术平台,你可以收获最新技术动态最新内测资格BAT等大厂大佬的经验增长自身学习资料职业路线赚钱思维,微信搜索逆锋起笔关注!

编写高性能 Java 代码的最佳实践!

SQL 性能优化梳理

编写高性能 Java 代码的最佳实践

一款微信小程序商城项目(附源码)

再见 收费的 XShell,我改用国产工具!

点赞是最大的支持 

浏览 16
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报