分布式缓存—缓存与数据库强一致场景下的方案

1. 概述

缓存与数据库的强一致性,也称线性一致性,核心要求是:数据库中的值发生变更,缓存数据要实现同步复制,并且一旦操作完成,随后任意客户端的查询都必须返回这一新值。以下图为例,一旦写入b完成,必须保证读到;而写入过程中,认为值的跳变可能发生在某一瞬间,因此读到a或b都是可能的。数据库与缓存作为一个整体,在向外提供服务的过程中,无论数据是否变更过,都时刻保持数据一致,因为它内部的数据仿佛只有一份,即使并发访问不同节点。

2. 场景

秒杀是一个比较典型的强一致场景,一般秒杀系统的库存同时保持在数据库与缓存中,如果查询缓存有数据,直接可以走秒杀流程,将数据库中的库存数量进行扣减,同时将最新的数据更新到缓存,使缓存中数据与数据库中数据保持强一致,这里只是拿秒杀的场景来举例,类似秒杀的场景有很多,像抢门票系统、12306抢火车票等,资源比较少用户比较多,需要在特定时间内进行抢购的业务场景。真实秒杀场景的设计,是在缓存中扣库存,不会直接在数据库中进行扣库存,因为数据库的性能远远比缓存差,所以本篇也只是拿类似秒杀这样的场景,来阐述强一致下的设计思想与相关实现。

3. 方案

分布式系统里面,有个众所周知的理论,就是CAP理论,CAP即:

​Consistency(一致性) Availability(可用性) Partition tolerance(分区容忍性) 这三个性质对应了分布式系统的三个指标。 而CAP理论说的就是一个分布式系统,不可能同时做到这三点。如果默认是分区的,那么只能选择P的情况下,出现了两种选择组合,AP与CP,AP保证可用性则牺牲了一致性,CP保证了一致性则牺牲了可用性,所以我们在讲缓存与数据库强一致的同时,不可避免牺牲了系统可用性的指标,所以看到12306网站这种体验不好,总是抢不到票,或者在一直提示排队中这种情况,就是系统可用性不佳的表现,因为火车站的票源是个稀缺资源,而且在各个站点之间查到的数量又是动态的,在这种强一致性下的业务场景,可用性必然会出现问题。这里不深入讨论12306网站具体是如何实现的,只是拿该场景做个引入。

假设现有一般抢购系统,某些商品搞促销活动,库存也就1000,抢完为止,在开枪时间未到来前,页面显示初始库存,在抢购过程中,只要刷新页面库存还有,按钮就不会置灰,还可以接着点击抢购,直到页面显示库存为0,活动结束。

这是个比较典型的读多写少场景,大量请求来集中访问,少部分请求能真正完成下单,我们很容易想到做读写分离,将商品的库存提前从数据库预加载到缓存,用户读的时候,从缓存读取数量,只要能看到数量,也就可以直接下单,至于用户能否抢到,得看用户运气了,让真正下单成功的用户去走后续付款操作。注意,这里对于某个用户下单成功后,后台要做的操作是先扣数据库库存数量,随后实时同步更新库存到缓存中。如果这一步更新不及时,很有可能数据库与缓存库存不一致,导致缓存中的数量比实际数据库库存还多,最终缓存库存减为零,而数据库已经是负数,结果导致超卖。

3.1 数据库与缓存双写+读取操作异步串行化

当库存发生变化后,更新数据库,同时更新缓存,如果在读并发高的情况下,更新数据库与更新缓存的时间间隔中,被读操作打断,那么读到的将是缓存中旧的库存,数据库已经是新库存,此时会出现不一致;

​为了解决这种问题,应先更新数据库后,立即删除缓存

更新数据的时候,根据数据的唯一标识,将操作路由之后,发送到一个jvm内部的内存队列中,同时删除缓存。 读取数据的时候,那么将重新读取数据,并更新缓存的操作,根据唯一标识路由之后,也发送同一个jvm内部的队列中。

​一个队列对应一个工作线程,每个工作线程串行拿到对应的操作,然后一条一条的执行,这样的话,一个数据变更的操作先执行,删除缓存。如果一个读请求过来,读到了空的缓存,就从数据库将更新后的值加载到缓存。如果并发高的情况下,会出现多个读操作并发的读数据库并加载缓存,可以先将缓存更新的请求发送到队列中,此时会在队列中积压,然后同步等待缓存更新完成。

这里有一个优化点,一个队列中,其实多个更新缓存请求串在一起是没意义的,因此可以做过滤,如果发现队列中已经有一个更新缓存的请求了,那么就不用再放个更新请求操作进去了,直接等待前面的更新操作请求完成即可;

如果请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回; 如果请求等待的时间超过一定时长,那么直接尝试从数据库中读取数据,并写入缓存。

C++后台开发高级架构师系统学习地址:C/C++Linux服务器开发高级架构师/C++后台开发架构师​

以下C++后台开发学习资料,面试题,教学视频,C++后台开发学习路线图,免费分享有需要的可以自行添加:学习资料群720209036 自取

​实现代码如下: step1: 注册监听器,初始化工作线程池和内存队列 在SpringBoot的启动类中注册如下监听器类InitListener

@EnableAutoConfiguration
@SpringBootApplication
@ComponentScan
@MapperScan("com.roncoo.eshop.inventory.mapper")
public class Application {

    
    /**
     * 注册监听器
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
使用分布式缓存时,可以采取以下方法来保证数据的读写一致性: 1. 缓存更新策略:在进行写操作时,需要及时更新缓存中的数据。可以通过以下几种策略来实现: - Cache-Aside(旁路缓存):在写操作时,先更新数据库,然后再手动更新或使缓存失效,下次读取时从数据库中获取最新数据并放入缓存。 - Write-Through(直写缓存):在写操作时,先更新数据库,然后再直接更新缓存中的数据。 - Write-Back(回写缓存):在写操作时,先更新缓存中的数据,然后异步或定期批量将更新的数据写回数据库。 2. 缓存失效策略:为了保证数据的一致性,需要在写操作时及时使缓存失效,以便下次读取时从数据库中获取最新数据。可以根据业务需求和数据更新频率设置合适的缓存失效时间或手动使缓存失效。 3. 分布式锁:在某些场景下,如果多个客户端同时进行写操作,可能会导致数据不一致。可以使用分布式锁来保证只有一个客户端能够进行写操作,从而保证数据的一致性。常见的分布式锁实现包括基于数据库的悲观锁、基于缓存的乐观锁、分布式锁服务(如ZooKeeper、Redisson)等。 4. 缓存预热:在应用启动时,可以预先加载一部分热点数据到缓存中,以提高访问性能并减少对数据库的压力。这样可以保证初始时缓存中的数据与数据库的数据保持一致。 5. 监控和报警:通过监控缓存的命中率、失效率、更新频率等指标,可以及时发现和解决缓存读写一致性的问题。同时设置合适的报警机制,以便快速响应并解决潜在的问题。 需要根据具体业务场景和系统需求选择适合的缓存策略和技术,并综合考虑性能、一致性和可用性等方面的权衡。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值