想要保证缓存与数据库的双写一致,一共有4种方式:
先更新缓存,再更新数据库;
先更新数据库,再更新缓存;
先删除缓存,再更新数据库;
先更新数据库,再删除缓存。

我们需要做的是根据不同的场景来使用合理的方式来解决数据问题。

第一种:先删除缓存,再更新数据库
在出现失败时可能出现的问题:

  1. 线程A删除缓存成功,线程A更新数据库失败;
  2. 线程B从缓存中读取数据;由于缓存被删,进程B无法从缓存中得到数据,进而从数据库读取数据;此时数据库中的数据更新失败,线程B从数据库成功获取旧的数据,然后将数据更新到了缓存。
    最终,缓存和数据库的数据是一致的,但仍然是旧的数据。

第二种:先更新数据库,再删除缓存
假设这会有两个请求,一个请求A做查询操作,一个请求B做更新操作,那么会有如下情形产生

  1. 缓存刚好失效
  2. 请求A查询数据库,得一个旧值
  3. 请求B将新值写入数据库
  4. 请求B删除缓存
  5. 请求A将查到的旧值写入缓存
    如果发生上述情况,确实是会发生脏数据。
    然而,发生这种情况的概率又有多少呢?
    发生上述情况有一个先天性条件,就是步骤3的写数据库操作比步骤2的读数据库操作耗时更短,才有可能使得步骤4先于步骤5
    数据库的读操作的速度远快于写操作的(不然做读写分离干嘛,做读写分离的意义就是因为读操作比较快,耗资源少),因此步骤3耗时比步骤2更短,这一情形很难出现。
    先更新数据库,再删缓存依然会有问题,不过,问题出现的可能性会因为上面说的原因,变得比较低。

第三种:给所有的缓存一个失效期
第三种方案可以说是一个大杀器,任何不一致,都可以靠失效期解决,失效期越短,数据一致性越高。但是失效期越短,查数据库就会越频繁。因此失效期应该根据业务来定。

  1. 并发不高的情况:
    读: 读redis->没有,读mysql->把mysql数据写回redis,有的话直接从redis中取;
    写: 写mysql->成功,再写redis;
  2. 并发高的情况:
    读: 读redis->没有,读mysql->把mysql数据写回redis,有的话直接从redis中取;
    写:异步话,先写入redis的缓存,就直接返回;定期或特定动作将数据保存到mysql,可以做到多次更新,一次保存;

第四种:加锁,使线程顺序执行
如果一个服务部署到了多个机器,就变成了分布式锁,或者是分布式队列按顺序去操作数据库或者 Redis,带来的副作用就是:数据库本来是并发的,现在变成串行的了,加锁或者排队执行的方案降低了系统性能,所以这个方案看起来不太可行。

第五种:采用双删
先删除缓存,再更新数据库,当更新数据后休眠一段时间再删除一次缓存。

方案推荐两种:

  1. 项目整合quartz等定时任务框架,去实现延时3–5s再去执行最后一步任务 。(推荐使用)
  2. 创建线程池,线程池中拿一个线程,线程体中延时3-5s再去执行最后一步任务(不能忘了启动线程)

第六种:异步更新缓存(基于订阅binlog的同步机制)
MySQL binlog增量订阅消费+消息队列+增量数据更新到redis读Redis
热数据基本都在Redis写MySQL:增删改都是操作MySQL更新Redis数据:MySQ的数据操作binlog,来更新到Redis:
1)数据操作主要分为两大块:一个是全量(将全部数据一次写入到redis)一个是增量(实时更新)。
这里说的是增量,指的是mysql的update、insert、delate变更数据。
2)读取binlog后分析 ,利用消息队列,推送更新各台的redis缓存数据。
这样一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新。
其实这种机制,很类似MySQL的主从备份机制,因为MySQL的主备也是通过binlog来实现的数据一致性。
这里可以结合使用canal(阿里的一款开源框架),通过该框架可以对MySQL的binlog进行订阅,而canal正是模仿了mysql的slave数据库的备份请求,使得Redis的数据更新达到了相同的效果。
当然,这里的消息推送工具你也可以采用别的第三方:kafka、rabbitMQ等来实现推送更新Redis。

以上就是redis和数据库数据保持一致的方案。