go-zero微服务实战系列(六、缓存一致性保证)

简介: go-zero微服务实战系列(六、缓存一致性保证)

只要我们使用缓存,就必然会面对缓存和数据库间的一致性问题。如果缓存中的数据和数据库的数据不一致,那么业务应用从缓存中读取的数据就不是最新的数据,对业务的影响可想而知。比如我们把商品的库存数据存在缓存中,如果缓存中库存数据不对,那么可能就会影响下单操作,这是业务上很难接受的。本篇文章我们来一起聊一聊缓存的一致性问题。

如何解决缓存不一致

先删缓存再更新数据库

假设线程A删除缓存后,还没来得及更新数据库,这时候线程B开始读数据,线程B发现缓存缺失就只能去读数据库,等到线程B从数据库中读取完数据回塞缓存后,线程A才开始更新数据库,此时,缓存中的数据是旧值,而数据库中是最新值,两者已经不一致了。

这种场景的解决方案是在线程A更新完数据库的值后,可以让它sleep一小段时间,再进行一次缓存删除操作,之所以要加上sleep的一段时间,就是为了让线程B能够先从数据库读取出数据然后再把缓存miss的数据回塞到缓存,然后线程A再进行删除。所以线程A的sleep时间就需要大于线程B读取数据再写入缓存的时间。这个时间是多少呢?这个是需要我们在业务中加入打点监控来统计的,根据这个统计值来估算该时间。这样一来,其他线程读取数据时,会发现缓存缺失,就会从数据库中读取最新的值。我们把这种模型叫做 "延时双删"。

先更新数据库再删除缓存

如果线程A更新了数据库中的值,但还没来得及删除缓存中的值,线程B这时候开始读取数据,此时,线程B查询缓存时,命中了缓存,就会直接使用缓存中的值,该值为旧值。不过在这种场景下,如果并发请求量不高的话,其实基本上不会有线程读到旧值,而且线程A更新完数据库后,删除缓存是非常快的操作,所以,这种情况总体对业务影响较小。一般在生产环境中,也推荐大家采用该模式。

重试机制

可以把要删除的缓存值或者要更新的数据库的值放到消息队列中,当应用没能够成功地删除缓存或者是更新数据库的值的时候,可以从消息队列中消费这些值,这里消费消息队列的服务叫job,然后再次进行删除或者更新,起到一个兜底补偿的作用,以此来保证最终的一致性。

如果能够成功地删除或更新,就需要把这些值从消息队列中去除,以免重复操作,此时,我们也可以保证数据库和缓存数据的一致了,否则的话,我们还需要再次进行重试,如果重试超过一定次数还是失败,这时候一般都需要记录错误日志或者发送告警通知。

并发读写

首先第一步线程A读取缓存,这时候缓存没有命中,由于使用的是cache aside这种模式,所以接下来第二步线程A会去读数据库,这个时候线程B更新数据库,更新完数据库后通过set cache更新了缓存,最后第五步线程A把从数据库读到的值通过set cache也更新了缓存,但是这时候线程A中的数据已经是脏数据了,由于第四步和第五步都是设置缓存,导致写入的值相互覆盖,并且操作的顺序具有不确定性,从而导致了缓存不一致情况的发生。

怎么解决这个问题呢?其实非常地简单,我们只需要把第五步的set cache操作替换成add cache即可,add cache即setnx操作,只有缓存不存在的时候才会成功写入,相当于加了优先级,即更新数据库后的更新缓存优先级更高,而读数据库后回塞缓存的优先级较低,从而保证写操作的最新数据不会被读操作的回塞数据覆盖。

结束语

本篇文章说明了在使用缓存时最常遇见的一个问题,也就是缓存和数据库不一致的问题,针对这个问题我们列举了一些可能导致不一致的场景以及对应场景的解决方案,特别地,对于job异步补偿的场景我们可以使用set操作来强行覆盖缓存,保证缓存的更新为最新的数据,而对于读数据库回塞缓存的操作我们一般使用add来更新缓存。

希望本篇文章对你有所帮助,谢谢。

每周一、周四更新

代码仓库: https://github.com/zhoushuguang/lebron

项目地址

https://github.com/zeromicro/go-zero

目录
打赏
0
0
0
0
19
分享
相关文章
|
11天前
|
微服务——MongoDB实战演练——需求分析
本文档《5-MongoDB实战演练》聚焦于某头条文章评论业务的需求分析与功能实现。基于MongoDB,需完成以下功能:1)提供基本的增删改查API;2)支持通过文章ID查询相关评论;3)实现评论点赞功能。结合实际业务场景,演示MongoDB在数据存储与操作中的应用,附带示意图帮助理解业务结构。
17 2
微服务——MongoDB实战演练——需求分析
微服务——MongoDB实战演练——文章评论的基本增删改查
本节介绍了文章评论的基本增删改查功能实现。首先,在`cn.itcast.article.dao`包下创建数据访问接口`CommentRepository`,继承`MongoRepository`以支持MongoDB操作。接着,在`cn.itcast.article.service`包下创建业务逻辑类`CommentService`,通过注入`CommentRepository`实现保存、更新、删除及查询评论的功能。最后,新建Junit测试类`CommentServiceTest`,对保存和查询功能进行测试,并展示测试结果截图,验证功能的正确性。
25 2
|
11天前
|
微服务——MongoDB实战演练——文章评论实体类的编写
本节主要介绍文章评论实体类的编写,创建了包`cn.itcast.article.po`用于存放实体类。具体实现中,`Comment`类通过`@Document`注解映射到MongoDB的`comment`集合,包含主键、内容、发布时间、用户ID、昵称等属性,并通过`@Indexed`和`@CompoundIndex`注解添加单字段及复合索引,以提升查询效率。同时提供了Mongo命令示例,便于理解和操作。
23 2
微服务——MongoDB实战演练——MongoTemplate实现评论点赞
本节介绍如何使用MongoTemplate实现评论点赞功能。传统方法通过查询整个文档并更新所有字段,效率较低。为优化性能,采用MongoTemplate对特定字段直接操作。代码中展示了如何利用`Query`和`Update`对象构建更新逻辑,通过`update.inc("likenum")`实现点赞数递增。测试用例验证了功能的正确性,确保点赞数成功加1。
19 0
微服务——MongoDB实战演练——根据上级ID查询文章评论的分页列表
本节介绍如何根据上级ID查询文章评论的分页列表,主要包括以下内容:(1)在CommentRepository中新增`findByParentid`方法,用于按父ID查询子评论分页列表;(2)在CommentService中新增`findCommentListPageByParentid`方法,封装分页逻辑;(3)提供JUnit测试用例,验证功能正确性;(4)使用Compass插入测试数据并执行测试,展示查询结果。通过这些步骤,实现对评论的高效分页查询。
21 0
微服务——MongoDB实战演练——文章微服务模块搭建
本节介绍文章微服务模块的搭建过程,主要包括以下步骤:(1)创建项目工程 *article*,并在 *pom.xml* 中引入依赖;(2)配置 *application.yml* 文件;(3)创建启动类 *cn.itcast.article.ArticleApplication*;(4)启动项目,确保控制台无错误提示。通过以上步骤,完成文章微服务模块的基础构建与验证。
19 0
探索Go语言在微服务架构中的优势
在微服务架构的浪潮中,Go语言以其简洁、高效和并发处理能力脱颖而出。本文将深入探讨Go语言在构建微服务时的性能优势,包括其在内存管理、网络编程、并发模型以及工具链支持方面的特点。通过对比其他流行语言,我们将揭示Go语言如何成为微服务架构中的一股清流。
179 53
【环境】Rocky8使用gvm配置Go多版本管理的微服务开发环境(go-zero)
通过本文的介绍,我们详细讲解了如何在Rocky8上使用gvm来管理多个Go版本,并配置go-zero框架的开发环境。通过gvm的灵活管理,开发者可以轻松切换不同的Go版本,以适应不同项目的需求。同时,go-zero框架的使用进一步提升了微服务开发的效率和质量。希望本文能帮助开发者构建高效的Go语言开发环境,提高项目开发的灵活性和稳定性。
112 63
微服务——MongoDB实战演练——表结构分析
本文档来源于数据库articledb,展示了一张图片资源。图片宽度为1207像素,高度607像素,采用内联显示方式。内容涉及图像处理与样式设定,适用于文档或网页设计中多媒体元素的布局参考。图片来源为cdn.nlark.com,支持webp格式并附带水印处理。
16 1
微服务——MongoDB实战演练——表结构分析
微服务——MongoDB实战演练——技术选型
本节主要介绍技术选型中的两个重要工具:**mongodb-driver** 和 **SpringDataMongoDB**。其中,mongodb-driver 是 MongoDB 官方提供的 Java 驱动包,用于连接和操作 MongoDB 数据库,功能类似 JDBC 驱动。通过官方示例可快速上手。而 SpringDataMongoDB 是 Spring 生态的一员,封装了 mongodb-driver,提供了更简洁的 API,方便开发者在 Spring 环境中操作 MongoDB。两者各有优势,可根据实际需求选择合适的技术方案。
25 2