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

相关文章
|
11天前
|
Dubbo Java 应用服务中间件
微服务框架Dubbo环境部署实战
微服务框架Dubbo环境部署的实战指南,涵盖了Dubbo的概述、服务部署、以及Dubbo web管理页面的部署,旨在指导读者如何搭建和使用Dubbo框架。
63 17
微服务框架Dubbo环境部署实战
|
8天前
|
安全 大数据 Go
深入探索Go语言并发编程:Goroutines与Channels的实战应用
在当今高性能、高并发的应用需求下,Go语言以其独特的并发模型——Goroutines和Channels,成为了众多开发者眼中的璀璨明星。本文不仅阐述了Goroutines作为轻量级线程的优势,还深入剖析了Channels作为Goroutines间通信的桥梁,如何优雅地解决并发编程中的复杂问题。通过实战案例,我们将展示如何利用这些特性构建高效、可扩展的并发系统,同时探讨并发编程中常见的陷阱与最佳实践,为读者打开Go语言并发编程的广阔视野。
|
8天前
|
自然语言处理 Java 网络架构
解锁跨平台微服务新纪元:Micronaut与Kotlin联袂打造的多语言兼容服务——代码、教程、实战一次打包奉送!
【9月更文挑战第6天】Micronaut是一款轻量级、高性能的Java框架,适用于微服务开发。它支持Java、Groovy和Kotlin等多种语言,提供灵活的多语言开发环境。本文通过创建一个简单的多语言兼容服务,展示如何使用Micronaut及其注解驱动特性实现REST接口,并引入国际化支持。无论是个人项目还是企业应用,Micronaut都能提供高效、一致的开发体验,成为跨平台开发的利器。通过简单的配置和代码编写,即可实现多语言支持,展现其强大的跨平台优势。
22 2
|
9天前
|
运维 监控 持续交付
深入浅出:微服务架构的设计与实战
微服务,一个在软件开发领域如雷贯耳的名词,它代表着一种现代软件架构的风格。本文将通过浅显易懂的语言,带领读者从零开始了解微服务的概念、设计原则及其在实际项目中的运用。我们将一起探讨如何将一个庞大的单体应用拆分为灵活、独立、可扩展的微服务,并分享一些实践中的经验和技巧。无论你是初学者还是有一定经验的开发者,这篇文章都将为你提供新的视角和深入的理解。
38 3
|
10天前
|
消息中间件 缓存 NoSQL
奇怪的缓存一致性问题
本文记录了缓存一致性问题的排查过程和解决方案,同时带读者朋友们一起回顾下相关的八股文。
|
12天前
|
缓存 安全 Java
如何利用Go语言提升微服务架构的性能
在当今的软件开发中,微服务架构逐渐成为主流选择,它通过将应用程序拆分为多个小服务来提升灵活性和可维护性。然而,如何确保这些微服务高效且稳定地运行是一个关键问题。Go语言,以其高效的并发处理能力和简洁的语法,成为解决这一问题的理想工具。本文将探讨如何通过Go语言优化微服务架构的性能,包括高效的并发编程、内存管理技巧以及如何利用Go生态系统中的工具来提升服务的响应速度和资源利用率。
|
11天前
|
缓存 NoSQL 关系型数据库
MySQL与Redis缓存一致性的实现与挑战
在现代软件开发中,MySQL作为关系型数据库管理系统,广泛应用于数据存储;而Redis则以其高性能的内存数据结构存储特性,常被用作缓存层来提升数据访问速度。然而,当MySQL与Redis结合使用时,确保两者之间的数据一致性成为了一个重要且复杂的挑战。本文将从技术角度分享MySQL与Redis缓存一致性的实现方法及其面临的挑战。
34 2
|
18天前
|
消息中间件 缓存 Kafka
go-zero微服务实战系列(八、如何处理每秒上万次的下单请求)
go-zero微服务实战系列(八、如何处理每秒上万次的下单请求)
|
16天前
|
缓存 NoSQL Java
惊!Spring Boot遇上Redis,竟开启了一场缓存实战的革命!
【8月更文挑战第29天】在互联网时代,数据的高速读写至关重要。Spring Boot凭借简洁高效的特点广受开发者喜爱,而Redis作为高性能内存数据库,在缓存和消息队列领域表现出色。本文通过电商平台商品推荐系统的实战案例,详细介绍如何在Spring Boot项目中整合Redis,提升系统响应速度和用户体验。
41 0
|
16天前
|
存储 缓存 数据库
微服务+多级缓存:性能起飞的秘籍
【8月更文挑战第29天】在当今快速迭代的软件开发领域,高性能与可扩展性是企业应用不可或缺的两大支柱。微服务架构与多级缓存策略的完美结合,正是这一追求下的璀璨明珠。今天,我们将深入探讨这一组合如何助力系统性能“起飞”,并在实际工作学习中成为技术升级的关键推手。
34 0