在实习的项目开发中,遇到了一个问题,就是流量平台的数据都存储在数据库中,缓存会存一部分,但由于有TTL,会定期删除,因此如何保证Redis和Mysql中的数据一致性是个亟待解决的问题。一起来看看吧~
具体来说:
如果只是将redis的TTL=-1,可能会导致问题:
a. 缓存利用率低:不经常访问的数据,还一直留在缓存中
b. 数据不一致:因为是「定时」刷新缓存,缓存和数据库存在不一致(取决于定时任务的执行频率)
并发中,我们考虑同步对二者内部的数据进行修改,有两种方案:
a. 先更新缓存,后更新数据库
b. 先更新数据库,后更新缓存
a的话,若后者失败,则前者失效后,再读则会重置旧值
b的话, 若后者失败,要一段时间后才会更新
并发的时候,会出现不一致的问题,添加分布式锁可以解决,但每次数据发生变更,都「无脑」更新缓存,但是缓存中的数据不一定会被「马上读取」,这就会导致缓存中可能存放了很多不常访问的数据,浪费缓存资源。这种「更新数据库 + 更新缓存」的方案,不仅缓存利用率不高,还会造成机器性能的浪费。
因此,使用删除缓存的解决办法:
a. 先删除缓存,后更新数据库
b. 先更新数据库,后删除缓存
a并发的时候,还是有不一致的情况发生; b由于更新数据库(写)的时间比读的时间长, 且会加锁,发生问题概率低
如果后者失败,则多次重试:
立即重试很大概率「还会失败」
「重试次数」设置多少才合理?
重试会一直「占用」这个线程资源,无法服务其它客户端请求
因此要异步重试:把重试请求写到「消息队列」中,然后由专门的消费者来重试,直到成功。
或者更直接的做法,为了避免第二步执行失败,我们可以把操作缓存这一步,直接放到消息队列中,由消费者来操作缓存。
消息队列保证可靠性:写到队列中的消息,成功消费之前不会丢失(重启项目也不担心)
消息队列保证消息成功投递:下游从队列拉取消息,成功消费后才会删除消息,否则还会继续投递消息给消费者(符合我们重试的需求)
如果确实不想在应用中去写消息队列,近几年比较流行的解决方案:订阅数据库变更日志,再操作缓存。
具体来讲就是业务应用在修改数据时,「只需」修改数据库,无需操作缓存。MySQL 举例,当一条数据发生修改时,MySQL 就会产生一条变更日志(Binlog),我们可以订阅这个日志,拿到具体操作的数据,然后再根据这条数据,去删除对应的缓存。
订阅变更日志,目前也有了比较成熟的开源中间件,例如阿里的 canal,使用这种方案的优点在于:
无需考虑写消息队列失败情况:只要写 MySQL 成功,Binlog 肯定会有
自动投递到下游队列:canal 自动把数据库变更日志「投递」给下游的消息队列
至此,我们可以得出结论,想要保证数据库和缓存一致性,推荐采用「先更新数据库,再删除缓存」方案,并配合「消息队列」或「订阅变更日志」的方式来做。
如果使用「先更新数据库,再删除缓存」方案,其实也发生不一致:
线程 A 更新主库 X = 2(原值 X = 1)
线程 A 删除缓存
线程 B 查询缓存,没有命中,查询「从库」得到旧值(从库 X = 1)
从库「同步」完成(主从库 X = 2)
线程 B 将「旧值」写入缓存(X = 1)
最终 X 的值在缓存中是 1(旧值),在主从库中是 2(新值),也发生不一致。
缓存延迟双删策略: 线程 A 可以生成一条「延时消息」,写到消息队列中,消费者延时「删除」缓存。
双删的策略就是保证每次在数据修改的时候去吧redis 的数据删完 然后让它去查数据库