如何保证数据库与 Redis 的数据一致性
目录
读场景
写场景
延时双删
先删除 Redis 中的数据,还是先修改数据库
先删除 Redis,再修改数据库
先修改数据库,再删除 Redis
为什么要两次删除 Redis 中的数据
为什么要延时一段时间
需要保证强一致性
分布式锁
共享锁与排他锁
可以有一定的延时
使用 MQ 中间件
使用 Canal 中间件
对于这个问题,分为读场景和写场景。
读场景
客户端发送请求后,会先从 Redis 中获取信息,若获取到,就直接返回;若没有获取到,就会查询数据库,并将查询到的数据写回 Redis 中,从而保证数据库与 Redis 的数据一致性。
写场景
对于写场景,由于数据库中的数据发生了改变,需要将 Redis 中的数据也同步进行改变,这就涉及到了数据的一致性问题。
延时双删
先删除 Redis 中的数据,再修改数据库中的数据,延迟一段时间后再次删除 Redis 中的数据。
对于延时双删,有下面几个问题:
先删除 Redis 中的数据,还是先修改数据库
在下面的情景中,Redis 与数据库中存储的数据均为 10。
先删除 Redis,再修改数据库
第一种情况:

现有两个线程,线程1 先将缓存删除,再将数据库中的数据更新为 20。这时线程2 查询数据,由于此时数据库中的数据还未同步至 Redis 中,那么线程2 在 Redis 中查询不到数据,就会从数据库中查询到 20,并将 20 写回 Redis 中。此时 Redis 与数据库中的数据是一致的。
第二种情况:

当线程1 删除 Redis 中的数据后,线程2 查询数据,但是由于 Redis 中没有数据,就会从数据库中查询到 10,并将 10 写入 Redis 中。但是线程1 将数据库修改为 20,这也就导致了 Redis 与数据库中的数据不一致。
先修改数据库,再删除 Redis
第一种情况:

线程2 将数据库修改为 20,并将 Redis 中的数据删除。此时线程1 查询 Redis 中的数据,由于没有查询到,就会从数据库中获取,由于线程2 已经将数据库修改为 20,于是线程1 就会获取到 20,并将 20 写入 Redis 中,此时数据库与 Redis 中的数据是一致的。
第二种情况:

线程1 先查询缓存信息,由于缓存的信息可能过期,于是就会查询不到,那么就会查询数据库,数据库中的数据为 10。此时线程2 将数据库中的数据更新为 20,并删除 Redis 中的数据,但是线程 1 将10 写入 Redis 中,就导致数据库中的数据与 Redis 不一致。
为什么要两次删除 Redis 中的数据
删除两次 Redis 中的数据后,无论 Redis 中的数据是正确的,还是错误的,都会被删除,那么当下一次查询数据时,由于 Redis 中没有数据,就会将数据库中的新数据同步至 Redis 中,保证了 Redis 与数据库的数据一致性。
为什么要延时一段时间
这是因为数据库也是主从结构的,当主节点修改完数据后,需要一定的时间将数据同步至从节点。若不加以等待,直接将数据同步至 Redis 中,那么此时 Redis 中的数据就有可能是从节点中的旧数据,也就不能保证 Redis 与数据库的数据一致性。
但是在实际开发场景中,一般不会使用延时双闪策略,有两个原因:
- 可能会出现脏数据
- 由于不知道数据库主从同步如何结束,就不能把握延时的时间
其实对于写场景来说,有两种情况:
- 需要保证强一致性
- 可以有一定的延时
下面针对这两种情况给出不同的策略。
需要保证强一致性
分布式锁

在写数据时加锁,在读数据时也加锁,这就能保证在操作的同时不会被其他线程干预。但是代码性能较低。
共享锁与排他锁
使用 Redisson 实现的读写锁。在读的时候添加共享锁,保证读与读之间不互斥,读写互斥。当更新数据时,添加排他锁,读读、读写都互斥。保证了在写数据的同时,别的线程不能进行读操作,避免了脏数据。
需要注意的是,读方法与写方法需要上同一把锁。
可以有一定的延时
使用 MQ 中间件
当修改完数据库中的数据时,使用 MQ 异步通知删除 Redis 中的数据。
使用 Canal 中间件
不需要更改业务代码,只需部署一个 Canal 服务。Canal 服务把自己伪装成mysql的一个从节点。当数据库更新以后,Canal 会读取 binlog 数据,然后再通过 Canal 的客户端获取到数据,并更新缓存即可。
