redis缓存与数据库的一致性一种解决方案 - Go语言中文社区

redis缓存与数据库的一致性一种解决方案


以抢购活动中商品的库存为例,大家可以想一下在淘宝上买东西,当把商品放到购物车里面的时候,并没有真正的扣减库存;因为可能很多用户都有加购物车的习惯,但真正购买人数其实远没有这么多,当我们点击购买,到达支付页时才会真正的判断库存是否足够,满足条件时,扣减库存生成待支付订单。

通常情况下如果用户的并发量不高时,代码中可以在库存操作上使用redis 分布式锁或mysql for update的方式,来防止超卖;但一旦并发量大起来将有大量的用户阻塞在这一过程中,导致请求超时,进而整个系统奔溃,且这种方式对数据库库存字段所在表的查找修改操作极其频繁,数据库的压力也会很高,大量占用数据库系统请求线程资源,有可能发生其他正常的数据请求超时,从而引发数据库宕机,系统不可用。

下面我给出一种高并发量情况下商品售卖过程中库存扣减的处理过程,用于说明在高并发情况下保证redis和数据库数据一致性的解决方案,大家有不同的意见,可以留言,一起探讨:

用户购买时扣减库存,那么对应的商户后台,当用户创建商品时一定会给出库存,我们在创建库存值时直接将库存放到redis中一份(无时间限制),这两个操作需要放在同一个事务中,从而保证库存初始化创建成功,则redis中肯定也有该商品的库存值;当商户之后想编辑现有库存时,则需要 new - old,然后利用redis lua脚本以incrby的方式更新至缓存之中,lua中需要判断更新后的redis缓存是否会小于0(new<old),当小于0时提示用户,重新设定库存值,当然他们也要在一个事务中执行。

而用户端的操作,由于redis是单线程执行,依赖lua脚本天然具有线程安全性,且redis操作速度很快,可以提供良好的用户体验。lua脚本比较缓存中库存与用户购买数量的大小,当库存剩余数量大于购买数时,利用incrby 进行数量更新,同时使用消息中间件如kafka发送消息异步执行数据库中库存的扣减操作;之后便可以进行订单额创建过程。

如果在一定时间段内订单没有被支付成功,那么订单关闭此时需要将订单中扣减的库存数再还回去。

但是使用kafka异步执行数据库中库存数量修改会出现一个问题,就是当消息队列中有扣减库存消息未执行完成时,redis和库中的缓存数量是不一致的,比如redis中库存数为10,消息队列中有3条扣减库存的消息未执行,每条消息扣减1个库存量,数据库中库存为13,此时如果用户从后台修改了剩余库存为0,new-old = 0-13=-13,使用lua incrby更新redis中数量前进行校验会发现10-13<0,此时redis更新失败,由于数据库更新和redis操作在同一个事务中,则用户操作不成功,需要给用户一个明确的提示,告知部分库存已进入待支付状态,稍后重试;且更新库存时应该使用乐观锁,如update table set 库存 = a - 2 where 库存 = a 或 分布式锁 或 for update的方式; 用于避免商家在异步扣减库存的时候修改库存,导致库存数据不正确。

上面这种数据库和redis数据保障的是最终一致性,并不是实时的强一致性,从而保证服务的高可用

 

------------------------------------------------------------------------------------------------------------

上面这种是以redis为基准去更新数据库,但大部分情况下是使用数据库为准来更新redis,允许redis和mysql数据一致性有一定的数据延迟,即满足最终一致性。如果对数据有强一致性要求最好不好把数据放在redis之中。

Cache-Aside pattern 先更新数据库再删除缓存

查询:如果命中缓存则直接返回,否则查询数据库,数据库中有相应的数据则将数据更新至缓存之中,然后返回,如果数据库中没用则直接返回

更新:更新DB后删除缓存

这种方案有自身的问题,在高并发,mysql集群的情况下,可能会出现,数据不一致的情况,如下图

本来线程3在线程2之后更新DB,但是由于线程3查询完DB之后发生网络抖动,造成将数据重写放入缓存的时间往后延长了很多,网上有人给出解决方案有延迟双删,但网络抖动的时间不能确定,可能到双删之后才把数据更新至缓存,此时DB和redis的数据依旧是不一致的,并且这种方式对业务代码的入侵程度很高,每一个更新操作都需要加一段延迟双删的逻辑;还有人使用内存队列,按照某种方式将同一个数据的redis删除和set操作映射到同一个队列中,但这种方式开发成本有点大,需要自己维护队列大小和数量

下面我给出的实现方式是使用canal+mq,监听mysql bin_log,将数据变化推送至mq,通过数据变化来更新redis(不是删除了),利用mq例如kafka的同一个topic下的同一partition消息处理是有序的来保证操作的顺序执行,同时对redis中数据添加过期时间,保证最终一致性,过程如下图所示

不过如果系统可以容忍mysql和redis有一段时间数据不一致,支持最终一致性,其实最简单的办法就是为缓存设置一个过期时间,等到缓存过期后由下次查询操作进行更新

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/swl1993831/article/details/113728556
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2021-12-11 20:44:51
  • 阅读 ( 1641 )
  • 分类:数据库

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢