一、概述

        redis是一种开源、使用内存存储数据介质的键值对存储系统。redis的读写速度非常快,常用于应用与数据库之间做缓存层,能够减少数据库IO操作,提升数据库性能,并提高应用端的请求响应速度。但涉及到并发读写数据时就容易出现redis与数据库数据一致性的问题。

二、原因

        应用对数据库的操作无外乎两个操作读操作写操作。 redis作为应用与数据库之间的缓存层,通常应用在操作数据库之前先操作redis,读操作时只有redis不存在,才会操作数据库,写操作时需要更新数据库和redis缓存。针对这两个操作流程,我们需要分析下在什么场景下才会出现redis与数据库数据不一致的问题。

1、读取数据

读取数据流程如下:

1. 应用程序需要从数据库读取数据时,先查询redis的缓存数据是否命中。

2. 若命中,直接返回。若未命中,再去查询数据库。

3. 将查询到的数据先保存到redis中,并设置过期时间,再将数据返回到应用。

以上是常用的一个读取数据的场景,根据场景分析,只读的情况下是不会出现redis与数据库数据不一致的情况。

2、写数据

写数据流程一般操作流程可以分为以下4种:

1. 先更新缓存再更新数据库。

2. 先删除缓存再更新数据库。

3. 先更新数据库再更新缓存。

4. 先更新数据库再删除缓存。

根据以上4种流程分析,可以明确出两个问题,一个是缓存是更新还是删除,另外一个是先操作数据库还是先操作缓存呢。

2.1、缓存是更新还是删除

推荐使用删除缓存。因为缓存的更新成本太高。由于大多数情况下数据并不是直接写入缓存的,需要经过一系列复杂的计算再写入缓存的。若采用更新方式,那么每次写入数据库后,都需计算写入缓存的值,无疑是浪费性能的。删除缓存操作简单,副作用只是增加了一次chache miss,建议使用删除策略。

2.2 先操作数据库还是先操作缓存
2.2.1 先操作缓存

先操作缓存的流程如下:

先操作缓存的流程,就是先将缓存中数据删除,再更新数据库。

数据不一致

在读写并发操作的情况下,如何出现的数据不一致的问题呢。先看下并发流程:

1. 线程1发起修改数据请求,会进行删除缓存操作。

2. 接着更新数据库时出现了网络延迟。

3. 线程1由于网络延迟还未对数据库进行修改,此时线程2执行查询请求,会去缓存中查询数据。

4. 线程2在缓存中未查询到数据,再去查询数据库。

5. 线程2将查询到数据旧数据放到缓存中,并将数据返回。

6. 线程1在线程2数据查询完成后,才对数据库进行了修改。

在这个过程中就出现了redis与数据库数据不一致的问题,只有等redis中数据过期时间到了,才能将新数据更新到缓存中。

2.2.2 先操作数据库

先操作数据库的流程如下:

先操作数据库的流程,就是先对数据库进行修改,再将缓存中的数据进行删除。

数据不一致

在读写并发操作的情况下,如何出现的数据不一致的问题呢。

1. 线程1发起修改数据请求,先更新数据库。

2. 线程2在线程1更新数据库期间,发起查询请求,从缓存中获取到旧数据(脏数据)。

3. 线程1完成数据库更新后,删除缓存中的数据。

在这个过程中出现了短暂的数据不一致,但redis和数据库数据是最终一致性的。所以推荐先操作数据库再操作缓存。

通过上述原因分析,可以得出在并发的读写情况下,正常使用redis与数据库不管是先操作redis还是先操作数据库,可能都会数据不一致问题。

注意:由于redis和数据库操作不是原子的,若在redis和数据库之间加锁是可以实现数据一致,但也违背了使用redis的初衷。

二、解决方案

 在不考虑redis操作失败的情况下,保证redis与数据库数据一致性的解决方案有4种。

1、延迟删除机制

       该机制是在数据库数据更新后,先延迟一段时间后再次删除缓存数据。线程1写请求,线程2查询请求,通过延迟双删机制保证redis与数据库数据一致性。

通过(6)步延迟一段时间后再进行redis的删除,在并发读写情况下保证redis与数据库数据一致性。具体延迟多长时间,需评估项目读数据业务逻辑耗时(即线程2从数据库读取数据到更新缓存成功的时间)。确保查询请求结束,更新请求可以删除查询请求造成的缓存脏数据。

2、binlog同步删除机制

        通过canal组件订对binlog日志进行订阅,模仿数据库的slave数据库的备份请求,使得redis缓存数据删除,保证redis与数据库数据一致性。

通过上面两种方式,在并发读写的情况下保证redis与数据库数据最终一致性。但可能存在redis删除失败的情况,一旦出现就会有redis与数据库数据不一致的问题。只有等redis中数据过期时间到了,才能将新数据更新到缓存中。

3、异步重试删除机制  

        一旦缓存删除失败,可以通过重试机制设置重试次数保证一定删除成功。如重试3次,三次操作都失败则记录日志并发送告警,通知技术人员进行人工介入处理。在高并发环境下,重试最好使用异步方式,可以通过MQ实现这种机制。

通过(6)步延迟删除缓存数据时,删除时失败,缓存中存储的还是脏数据(旧数据)。线程1的应用作为producer异步发送需要删除key到MQ。线程1的应用监听MQ,重试删除操作。

通过重试删除机制,可以能够保证redis缓存一定能删除成功,保证redis与数据库数据一致性。但这种方式对业务代码造成侵入,代码过于耦合。

4、binlog解耦异步重试机制

        可以使用阿里巴巴开源框架canal来实现程序解耦。通过利用canal提供的java客户端,监听canal通知消息。当java客户端(项目)收到binlog变化的消息时,完成对缓存的处理。

数据库更新后,canal订阅binlog日志,将变更的数据发送消息通知给java客户端(spring boot项目)。java客户端执行延迟删除缓存,若删除失败,java客户端作为producer异步发送需要删除key的MQ消息进行重试。客户端监听MQ消息,执行重试删除缓存操作。

三、总结

        通过上述分析我们知道造成redis与数据库数据不一致的问题主要在于并发情况下,读写并发操作可能会出现这个问题。通过4中解决方案,能够很好的解决redis与数据库数据一致性问题。

        

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐