面对缓存,有哪些问题需要思考? -pg电子游戏网站

1顶
0踩

面对缓存,有哪些问题需要思考?

2017-09-27 16:18 by 副主编 jihong10102006 评论(2) 有25632人浏览
引用
作者|邱家榆
编辑|雨多田光

缓存可以说是无处不在,比如 pc 电脑中的内存、cpu 中的二级缓存、http 协议中的缓存控制、cdn 加速技术都是使用了缓存的思想来解决性能问题。

缓存是用于解决高并发场景下系统的性能及稳定性问题的银弹。

本文主要是讨论我们经常使用的分布式缓存 redis 在开发过程中的相关思考。

一、 如何将业务逻辑与缓存之间进行解耦?

大部分情况,大家都是把缓存操作和业务逻辑之间的代码交织在一起的,比如(代码一):

从上面的代码可以看出以下几个问题:
  • 缓存操作非常繁琐,产生非常多的重复代码;
  • 缓存操作与业务逻辑耦合度非常高,不利于后期的维护;
  • 当业务数据为 null 时,无法确定是否已经缓存,会造成缓存无法命中;
  • 开发阶段,为了排查问题,经常需要来回开关缓存功能,使用上面的代码是无法做到很方便地开关缓存功能;
  • 当业务越来越复杂,使用缓存的地方越来越多时,很难定位哪些数据要进行主动删除;
  • 如果不想用 redis,换用别的缓存技术的话,那是多么痛苦的一件事。
因为高耦合带来的问题还很多,就不一一列举了。接下来以笔者开源的一个缓存管理框架 为例,看看我的设计是如何帮助我们来解决上述问题的。

借鉴 spring cache 的思想使用 aop annotation 等技术实现缓存与业务逻辑的解耦。我们先用 autoloadcache 来重构上面的代码(代码二),进行对比,再进行分析。

autoloadcache 在 aop 拦截到请求后,大概的流程如下:

1 . 获取到拦截方法的 @cache 注解,并生成缓存 key;

2 . 通过缓存 key,去缓存中获取数据;

3 . 如果缓存命中,执行如下流程:
  • 如果需要自动加载,则把相关信息保存到自动加载队列中;
  • 否则判断缓存是否即将过期,如果即将过期,则会发起异步刷新;
  • 最后把数据返回给用户。
4 . 如果缓存没有命中,执行如下流程:
  • 选举出一个 leader 回到数据源中去加载数据,加载到数据后通知其它请求从内存中获取数据(拿来主义机制);
  • leader 负责把数据写入缓存;如果需要自动加载,则把相关信息保存到自动加载队列中;
  • 最后把数据返回给用户。
这里提到的异步刷新、自动加载、拿来主义机制,我们会在后面再说明。

二、 对缓存进行“包装”

上面代码一的例子中,当从数据源获取的数据为 null 时,缓存就没有意义了,请求会回到数据源去获取数据。当请求量非常大的话,会造成数据源负载过高而宕机。

所以对于 null 的数据,需要做特殊处理,比如使用特殊字符串进行替换。而在 autoloadcache 中使用了一个包装器对所有缓存数据进行包装(代码三):

在这上面的代码中,除了封装缓存数据外,还封装了数据加载时间和缓存时长,通过这两项数据,很容易判断缓存是否即将过期或者已经过期。

三、 如何提升缓存 key 生成表达式性能?

使用 annotation 解决缓存与业务之间的耦合后,我们最主要的工作就是如何来设计缓存 key 了,缓存 key 设计的粒度越小,缓存的复用性也就越好。

上面例子中我们是使用 spring el 表达式来生成缓存 key,有些人估计会担心 spring el 表达式的性能不好,或者不想用 spring 的情况该怎么办?

框架中为了满足这些需求,支持扩展表达式解析器:继承 com.jarvis.cache.script. abstractscriptparser 后就可以任你扩展。

框架现在除了支持 spring el 表达式外,还支持 ognl、javascript 表达式。对于性能要求非常高的人,可以使用 ognl,它的性能非常接近原生代码。

四、 如何解决缓存 key 冲突问题?

在实际情况中,可能有多个模块共用一个 redis 服务器或是一个 redis 集群的情况,那么有可能造成缓存 key 冲突了。

为了解决这个问题 autoloadcache,增加了 namespace。如果设置了 namespace 就会在每个缓存 key 最前面增加 namespace(代码四):

五、 压缩缓存数据及提升序列化与反序列化性能

我们希望缓存数据包越小越好,能减少内存占用,以及减轻带宽压力;同时也要考虑序列化与反序列化的性能。

autoloadcache 为了满足不同用户的需要,已经实现了基于 jdk、hessian、jacksonjson、fastjson、jacksonmsgpack 等技术序列化及反序列工具。也可以通过实现 com.jarvis.cache.serializer.iserializer 接口自行扩展。

jdk 自带的序列化与反序列化工具产生的数据包非常大,而且性能也非常差,不建议大家使用;jacksonjson 和 fastjson 是基于 json 的,所有用到缓存的函数的参数及返回值都必须是具体类型的,不能是不确定类型的(不能是 object, list等),另外有些数据转成 json 时其一些属性是会被忽略,存在这种情况时,也不能使用 json;而 hessian 则是非常不错的选择,非常成熟和稳定性。阿里的 dubbo 和 hsf 两个 rpc 框架都是使用了 hessian 进行序列化和返序列化。

六、 如何减少回源并发数?

当缓存未命中时,都需要回到数据源去取数据,如果这时有多个并发来请求相同一个数据(即相同缓存 key 请求),都回到数据源加载数据,并写缓存,造成资源极大的浪费,也可能造成数据源负载过高而无法服务。

autoloadcache 使用 拿来主义机制 和 自动加载机制 来解决这个问题:

拿来主义机制

拿来主交机制,指的是当有多个用户请求同一个数据时,会选举出一个 leader 去数据源加载数据,其它用户则等待其拿到的数据。并由 leader 将数据写入缓存。

自动加载机制

自动加载机制,将用户请求及缓存时间等信息放到一个队列中,后台使用线程池定期扫这个队列,发现缓存即将过期,则去数据源加载最新的数据放到缓存中,达到将数据长驻内存的效果。从而将这些数据的请求,全部引向了缓存,而不会回到数据源去获取数据。这非常适合用于缓存使用非常频繁的数据,以及非常耗时的数据。

为了防止自动加载队列过大,设置了容量限制;同时会将超过一定时间没有用户请求的数据从自动加载队列中移除,把服务器资源释放出来,给真正需要的请求。

往缓存里写数据的性能相比读的性能差非常多,通过上面两种机制,可以减少写缓存的并发,提升缓存服务能力。

七、 异步刷新

autoloadcache 从缓存中获取到数据后,借助上面提到的 cachewrapper,能很方便地判断缓存是否即将过期, 如果即将过期,则会把发起异步刷新请求。

使用异步刷新的目的是提前将数据缓存起来,避免缓存失效后,大量请求穿透到数据源。

八、 多种缓存操作

大部分情况下,我们都是对缓存进行读与写操作,可有时,我们只需要从缓存中读取数据,或者只写数据,那么可以通过 @cache 的 optype 指定缓存操作类型。现支持以下几种操作类型:
  • read_write:读写缓存操,如果缓存中有数据,则使用缓存中的数据,如果缓存中没有数据,则加载数据,并写入缓存。默认是 read_write;
  • write:从数据源中加载最新的数据,并写入缓存。对数据源和缓存数据进行同步;
  • read_only: 只从缓存中读取,并不会去数据源加载数据。用于异地读写缓存的场景;
  • load:只从数据源加载数据,不读取缓存中的数据,也不写入缓存。
另外在 @cache 中只能静态指写缓存操作类型,如果想在运行时调整操作类型,需要通过 cachehelper.setcacheoptype() 方法来进行调整。

九、 批量删除缓存

很多时候,数据查询条件是比较复杂的,我们无法获取或还原要删除的缓存 key。

autoloadcache 为了解决这个问题,使用 redis 的 hash 表来管理这部分的缓存。把需要批量删除的缓存放在同一个 hash 表中,如果需要需要批量删除这些缓存时,直接把这个 hash 表删除即可。这时只要设计合理粒度的缓存 key 即可。

通过 @cache 的 hfield 设置 hash 表的 key。

我们举个商品评论的场景(代码五):

如果添加评论时,我们只需要主动删除前 3 页的评论(代码六):

十、  双写不一致问题

在代码二中使用 updateuser 方法更新用户信息时, 同时会主动删除缓存中的数据。 如果在事务还没提交之前又有一个请求去加载用户数据,这时就会把数据库中旧数据缓存起来,在下次主动删除缓存或缓存过期之前的这一段时间内,缓存中的数据与数据库中的数据是不一致的。

autoloadcache 框架为了解决这个问题,引入了一个新的注解:@cachedeletetransactional (代码七):

使用 @cachedeletetransactional 注解后,autoloadcache 会先使用 threadlocal 缓存要删除缓存 key,等事务提交后再去执行缓存删除操作。其实不能说是“解决不一致问题”,而是 缓解 而已。

缓存数据双写不一致的问题是很难解决的,即使我们只用数据库(单写的情况)也会存在数据不一致的情况(当从数据库中取数据时,同时又被更新了),我们只能是减少不一致情况的发生。对于一些比较重要的数据,我们不能直接使用缓存中的数据进行计算并回写的数据库中,比如扣库存,需要对数据增加版本信息,并通过乐观锁等技术来避免数据不一致问题。

十一、 与 spring cache 的比较

autoloadcache 的思想其实是源自 spring cache,都是使用 aop annotation ,将缓存与业务逻辑进行解耦。区别在于:

1 . autoloadcache 的 aop 不限于 spring 中的 aop 技术,即可以脱离 spring 生态使用,比如成功案例

2 . spring cache 不支持命名空间;

3 . spring cache 没有自动加载、异步刷新、拿来主义机制;

4 . spring cache 使用 name 和 key 的来管理缓存(即通过 name 和 key 就可以操作具体缓存了),而 autoloadcache 使用的是 namespace key hfield 来管理缓存,同时每个缓存都可以指定缓存时间(expire)。也就是说 spring cache 比较适合用来管理 ehcache 的缓存,而 autoloadcache 更加适合管理 redis,memcache,尤其是 redis,hfield 相关的功能都是针对它们进行开发的(因为 memcache 不支持 hash 表,所以没办法使用 hfield 相关的功能)。

5 . spring cache 不能针对每个缓存 key,进行设置缓存过期时间。而在缓存管理应用中,不同的缓存其缓存时间要尽量设置为不同的。如果都相同的,那缓存同时失效的可能性会比较大些,这样穿透到数据库的可能性也就更大了,对系统的稳定性是没有好处的;

6 . spring cache 最大的缺点就是无法使用 spring el 表达式来动态生成 cache name,而且 cache name 是的必须在 spring 配置时指定几个,非常不方便使用。尤其想在 redis 中想精确清除一批缓存,是无法实现的,可能会误删除我们不希望被删除的缓存;

7 . spring cache 只能基于 spring 中的 aop 及 spring el 表达式来使用,而 autoloadcache 可以根据使用者的实际情况进行扩展;

8 . autoloadcache 中使用 @cachedeletetransactional 来减少双写不一致问题,而 spring cache 没有相应的pg电子游戏网站的解决方案;

最后欢迎大家对 autoloadcache 开源项目 star 和 fork 进行支持。
  • 大小: 60.3 kb
  • 大小: 48.9 kb
  • 大小: 36.2 kb
  • 大小: 41.9 kb
  • 大小: 52.2 kb
  • 大小: 49.7 kb
  • 大小: 30.3 kb
来自:
1
0
评论 共 2 条 请登录后发表评论
2 楼 2017-10-12 16:10
文档好像没人写,一些细节地方,比如“在读写模式下,不能使用获取retval作为缓存key。”,只在讨论区一个关闭的帖子里有答案,而在线文档是这样写的例子:

@cache(expire=600, key="'user.getuserbyid' #args[0]", excache={@excache(expire=600, key="'user.getuserbyname' #retval.name")})
public user getuserbyid(long id){... ...}

让我很困惑。
1 楼 2017-10-11 14:22
初入社会,真的要学习学习缓存技术。。。

发表评论

您还没有登录,请您登录后再发表评论

相关推荐

  • 对于这个问题,简单的说就是把缓存透了但数据库没透。还不明白?那么这一小节我来带大家通过一个形象的例子来讲解一下。 我们知道缓存层都会设置数据过期时间,如果不设置过期时间的话,随着查询的越来越多缓存就会...

  • 关于redis的知识总结了一个脑图分享给大家 1、在项目中缓存是如何使用的?为什么要用缓存?缓存使用不当会造成什么后果? (1)面试官心理分析 ...这就是看看你对缓存这个东西背后有没有思考,如果你...

  • 这就是看看你对缓存这个东西背后有没有思考,如果你就是傻乎乎的瞎用,没法给面试官一个合理的解答,那面试官对你印象肯定不太好,觉得你平时思考太少,就知道干活儿。项目中缓存是如何使用的?这个,需要结合自己...

  • 只要我们使用redis缓存,就必然会面对缓存和数据库间的一致性保证问题,这也算是redis缓存应用中的“必答题”了。最重要的是,如果数据不一致,那么业务应用从缓存中读取的数据就不是最新数据,这会导致严重的错误。...

  • 缓存应用和数据库在更新时经常会出现不一致的问题,采用哪种策略,值得去思考。 从理论上来说,给缓存设置过期时间,是保证最终一致性的pg电子游戏网站的解决方案。这种方案下,我们可以对存入缓存的数据设置过期时间,所有的写操作...

  • 分布式寻址都有哪些算法?了 解一致性 hash 算法吗? 面试官心理分析 在前几年,redis 如果要搞几个节点,每个节点存储一部分的数据,得借助一些中间件来实现,比如说有codis,或者 twemproxy,都有。有一些 redis...

  • 缓存常见问题及pg电子游戏网站的解决方案 在上一篇文章性能设计之...从上面的描述可知,问题只要出在大量key同时过期,那我们可以翻过来思考一下,如果我们能避免key不在同一时间过期,那雪崩问题不就解决了么。通常的做法有以下两...

  • 缓存的作用,主要是解决查询数据速度慢的性能问题。比如经过代码优化,数据库优化之后,页面查询依然存在性能瓶颈问题,为了提高用户的体验,提升页面的响应时间,那就采用其它方案来解决吧,数据库采...

  • 文章目录 每日一句正能量 前言 一、目标达成情况总结 二、工作和学习成果总结 三、下半年规划总结 四、个人想法 后记 附录 每日一句正能量 做一个向日葵族,面对阳光,不自艾自怜,每天活出最灿烂的自己。...

  • 缓存与数据库双写一致问题 操作缓存 更新策略1-先更新数据库,后更新缓存 更新策略2-先删除缓存,在更新数据库 更新策略3-先更新数据库,再删除缓存 并发竞争key 缓存穿透 缓存穿透,...

  • 这一讲,我们主要围绕数据库读取操作频繁的问题深入探讨。 业务场景(架构经历四) 在我曾经负责的一个电商系统中,存放了 50000 多条商品数据,每次用户浏览商品详情页时,需要先从数据库中读取数据,再进行数据...

  • 毕竟实际使用起来远没有那么简单,本文中只是介绍了最基础的使用,实际中的并发问题、事务的回滚问题都需要考虑,还需要思考什么数据适合放在一级缓存、什么数据适合放在二级缓存等等的其他问题。那么,这次的分享就...

  • http缓存

  • 现实情况下我们面对的是一个不可靠的网络、有一定概率宕机的设备,这两个因素都会导致partition,因而分布式系统实现中 p 是一个必须项,而不是可选项。 高可用、数据一致性是很多系统设计的目标,但是分区又是不可...

  • 如果第二次看到我的文章,欢迎文末扫...在前一篇《360°全方位解读「缓存」》中,我们聊了运用缓存的三种思路,以及在一个完整的系统中可以设立缓存的几个位置,并且分享了关于浏览器缓存、cdn缓存、网关(代理)缓...

  • 但有一些问题需要考虑。 如何尽量使各存储节点的负载相对均衡? 怎样保证新加入的节点,不会因短期负载压力过大而崩塌? 如果需要数据迁移,那如何使其对业务层透明? 1)如何尽量使各存储节点的负载相对均衡? 首先要...

  • 一线大厂redis高并发缓存架构实战与性能优化。

  • 缓存提升性能就会有数据更新的延迟,就无法使数据库和缓存数据保持强一致,所以上树的各种优化方案,都是以保证弱一致性,最终一致性为前提的。

  • 缓存可以极大的减轻db的访问压力,当然缓存涉及到分布式要考虑的问题也很多,主要有:更新模式、失效机制、淘汰策略、常见问题(缓存穿透、缓存击穿、缓存雪崩)等。 缓存解决的问题: 提升访问性能,redis、...

  • python库是一组预先编写的代码模块,旨在帮助开发者实现特定的编程任务,无需从零开始编写代码。这些库可以包括各种功能,如数学运算、文件操作、数据分析和网络编程等。python社区提供了大量的第三方库,如numpy、pandas和requests,极大地丰富了python的应用领域,从数据科学到web开发。python库的丰富性是python成为最受欢迎的编程语言之一的关键原因之一。这些库不仅为初学者提供了快速入门的途径,而且为经验丰富的开发者提供了强大的工具,以高效率、高质量地完成复杂任务。例如,matplotlib和seaborn库在数据可视化领域内非常受欢迎,它们提供了广泛的工具和技术,可以创建高度定制化的图表和图形,帮助数据科学家和分析师在数据探索和结果展示中更有效地传达信息。

global site tag (gtag.js) - google analytics
网站地图