在我们使用redis作为缓存的时候,数据库和缓存数据保持一致性就显得尤为重要,因为如果不做处理的话很有可能读取到的数据会出现差错,那这里怎么进行解决呢?
首先我们先来看一下操作数据到底是直接删除数据还是说通过修改的方式来修改数据呢?
如果是直接删除的话,那当然我们直接删除缓存的数据就行了,但是如果是更新 Redis 中的数据,可能会涉及到一系列复杂的业务逻辑计算,整个更新操作所需要付出的成本是比删除操作更高的,所以我们会选择直接删除数据。
我们先来看看先删缓存中的数据会出现什么问题:
整个步骤已经在图中进行标明,这里还是大概说一下步骤:
线程1去操作数据,先去缓存中删除数据,然后去数据库中进行更新,但是这个时候出现了网络延迟或其他问题,导致数据库的数据还没更新成功,线程2就开始从redis中进行读取数据;
线程2发现redis中没有这条数据,所以就去数据库中读,但是此时数据库中的数据依然是没有更新的数据,所以读取到的数据还是老数据,然后同步到redis当中;
这个时候线程1才将数据修改到数据库中,但是对于之后的线程读取的数据依旧是redis中的老数据,只有等到redis的缓存过期之后,才会从数据库读到新的数据。
解决办法
延时双删
既然是因为缓存的原因,那我可不可以把缓存中的数据再删一次,然后去数据库里面读取新数据呢?这个想法是没毛病滴~
当线程2从数据库读取到数据之后,再把数据同步到redis当中,这个时候我们就需要做一个删除的操作,但是这个延迟删除的时间需要是等到线程1把数据同步到数据库中后,才进行删除,不然读到的还是旧数据,这个延时的时间需要你根据业务的整个执行时间去判断大概需要延时到多少~
当然,这个解决方法的话还是会出现部分请求到的数据依旧是老数据,但是最终结果上是解决了数据不一致的问题~
所以这里还提供了一种方法,使用最终一致性和强一致性,这种就可以解决我们前几次还是会出现缓存不一致的问题。
你可以想想,因为redis和数据库都是分布在不同的机器上分别进行处理的,即使是在同一台机器上执行也是需要两步操作才能完成,所以如果我们要保证操作缓存和操作数据库的原子性,就需要对他进行加锁。
但是我们加锁了肯定是会影响吞吐量的,但是我们设置缓存的目的就是减少系统吞吐量,这样一来加锁之后肯定会对系统性能造成影响,所以为了保证系统性能,我们还是不建议使用这种做法~
还留有另外一种操作数据的方法
这张图也给出了一个大概步骤,我这里再梳理一下流程:
线程1操作数据,先去数据库中删除数据,然后再更新到混存当中,然后线程2从缓存中读取数据并返回。
但是这里会存在问题,你可以想一想,如果我的线程1在删除数据库数据的时候,因为网络延迟或者某些原因,还没有执行完成,这个时候缓存数据过期,线程2只能从数据库中获取数据,但是获取到的数据依旧是旧数据,然后线程1将数据库修改成功后再去删除缓存,然后线程2再把数据同步到缓存中,那这个时候缓存中的数据依旧是旧数据,从而导致数据一致性被破坏。
如果我数据库布置的是集群,那么主数据库写数据,从数据库读数据,如果这个时候我对主数据库写完数据后,从数据库还没来得及更新,那么其它线程来请求数据的时候,在从数据里面读取到的数据就是还未更新的数据,那么数据一致性也会被破坏掉~
解决办法
删除+延迟双删
和上一个提到的延迟双删一样,先删除一次缓存,然后再做一次延迟双删,当然什么时候做延迟双删的操作还是需要根据业务中的整个时间来进行判断,得等到我数据库修改完成再删除缓存后,再进行删除。
当然,频繁的删除操作对缓存是不友好的,很容易出现缓存击穿的问题~
删除失败怎么办
如果我们在删除缓存的时候删除失败了怎么办,就拿我们先更新数据库,在删缓存的场景来讲,我们可以借助消息队列设置一个重试机制,在重试达到我们设置的值之后,还是没能删除成功的话,我们可以把这个删除失败的消息通过消息中间件把消息返回给线程1,并发送告警给相关人员,从而人工介入~
如果是在高并发的情况下出现的这种情况,我们建议使用异步的方式来实现消息发送~
当然,使用这种方式的话,整个代码的耦合性还是很高的,如果想要降低耦合,我们可以引入一个组件——canal,但是引入组件后,整个系统的复杂性就会增加,这里就不多说了,感兴趣的小伙伴可以再深入了解一下~