Redis和MySQL缓存一致性问题思考

项目中是如何保证Redis和MySQL的数据一致性呢?

方案一:先更新数据库,再更新缓存。

方案二:先删除库存,再更新数据库

方案三:先删除缓存,再更新数据库(设置等待时间),再删除缓存【延迟双删】

方案四:最终一致性方案(删除缓存 重试机制)

方案一:如果先更新数据库,再更新缓存。当用户A进行更新数据库操作时,B来读取该物品的数据,先是会在Redis读取数据,然后当它读取到数据以后就会返回。

那么,如果此时A还没有在B之前将数据更新到Redis的话,就会造成数据不一致性。

方案二:同理,你用户A想要更改10变成9.当你删除了Redis库存,还没有写入到MySQL数据库的时候,用户B来读取数据,发现Redis没有数据,他就会到数据库读取,读取了10,然后返回。此时A才把数据写入到MySQL里面。造成数据不一致性。

方案三:当用户A想要修改10变为9.先删除了缓存,再更新数据库的数据,更新完以后,再执行一次删除缓存的操作。为什么呢,

假如用户A刚删除完Redis数据,还没有更新数据库数据,用户B来到,读取了Redis发现没有数据以后,来到了MySQL读取数据,读取到了10,然后返回,写入到了Redis里面。此时,A用户完成了数据库更新操作。并且等待一定的时间,也是为了防止有用户B正在修改着Redis[如果没有延迟时间,假如A将Redis再删除了一次,然后用户B将数据10写入到了Redis,依旧会数据不一致],然后等待时间过去后,再重新删除一次Redis。这样的话,下一次用户C来读取数据,发现Redis没有,他还是会到MySQL读取数据,此时读取到的就是正确的数据了。

但是方案三有一个缺点就是:因为它是有延迟删除的,所以在高并发的场景下,就会很浪费时间。

方案四:我们不追求数据的强一致性。只追求数据的最终一致性。

什么意思呢?就是我们不在乎用户读取到Redis数据和MySQL数据的差异,只在乎在最终情况下的一致性。比如,用户在买一个物品的时候,添加到购物车是10元,但是他生成订单付款的时候,发现是9元。这就是最终一致。

怎么实现呢,就是修改数据库数据,然后直接删除一次Redis的数据。并不执行延迟操作。这样我们需要考虑的就是用户删除Redis一定要成功。

那么,为什么一定要保证删除Redis删除成功呢,不成功可不可以?

首先我们要明白为什么是删除,而不是更新缓存?

我们以先更新数据库,再删除缓存来举例。

如果是更新的话,那就是先更新数据库,再更新缓存

举个例子:如果数据库1小时内更新了1000次,那么缓存也要更新1000次,但是这个缓存可能在1小时内只被读取了1次,那么这1000次的更新有必要吗?

反过来,如果是删除的话,就算数据库更新了1000次,那么也只是做了1次缓存删除,只有当缓存真正被读取的时候才去数据库加载。

还有一个情况,我用户更改了这个MySQL数据,然后删除了Redis的旧数据,是为了后期用户读取数据的时候能够获得一个最新值。而且,也是为了避免这个数据在很长一段时间内没有用户查询,那就没必要在Redis中浪费内存了[而且写入redis中本身也是消耗性能的]。



关心并发

在我们微服务架构下,并发引发的一致性问题一直是我们所考量的具体情况,但是如果硬要考量解决强一致性问题,最常见的方案是 2PC、3PC、Paxos、Raft 这类一致性协议,但它们的性能往往比较差,而且这些方案也比较复杂,还要考虑各种容错问题。我们这个时候就要明白使用Redis的实质意义是什么了。那就是提高性能。

正所谓鱼与熊掌不能兼得,面临一致性问题。性能和一致性就像天平的两端,无法做到都满足要求。所以为了我们的初衷,我们更多的是考虑数据的最终一致性方案。

怎么保证删除成功呢,用户可以使用消息队列MQ的重试+手动确认机制来删除Redis的数据。

逻辑就是:假设我们删除Redis缓存失败了,我们会发送一个消息到MQ,然后我们再写一个程序来消费MQ里面的消息,只有删除成功以后才确认,删除不成功就利用MQ的重试机制一直删。

这种方式是可以的,但是它对我们的程序耦合度要求较高,比如说我要修改一个接口,或者新增接口,或者其他的一些情况,我每一个都用这样的方法去加消息,发消息,这个操作本身不仅麻烦,还浪费性能。

而且,我引入消息队列,这又增加了更多的维护成本,这样做值得吗?

但我们思考这样一个问题:如果在执行失败的线程中一直重试,还没等执行成功,此时如果项目「重启」了,那这次重试请求也就「丢失」了,那这条数据就一直不一致了。

所以,这里我们必须把重试或第二步操作放到另一个「服务」中,这个服务用「消息队列」最为合适。这是因为消息队列的特性,正好符合我们的需求:

  • 消息队列保证可靠性:写到队列中的消息,成功消费之前不会丢失(重启项目也不担心)

  • 消息队列保证消息成功投递:下游从队列拉取消息,成功消费后才会删除消息,否则还会继续投递消息给消费者(符合我们重试的场景)

至于写队列失败和消息队列的维护成本问题:

  • 写队列失败:操作缓存和写消息队列,「同时失败」的概率其实是很小的

  • 维护成本:我们项目中一般都会用到消息队列,维护成本并没有新增很多

所以还有一种低耦合的解决思路,就是使用canal,利用MySQL的主从复制机制,伪装成MySQL的一个从机,它可以不断监听我们的二进制日志文件。当我们数据发生变化的时候,它会主动给MQ发送一条消息。所以,假如删除Redis数据失败,异步发送到消息队列这个操作。我们就不需要在代码中实现,这个操作交给我们的canal去实现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值