大量的访问请求使得数据库操作频繁,结果导致服务器性能下降,为了解决该问题可以引入reids
,让其作为数据库的缓存。这样,在客户端请求数据时,能从缓存中读取就可以不必去数据库中读取,从而减轻数据库压力,提高服务器性能。但是如果数据发生变化,而数据又存在于数据库和redis
中,此时就会产生数据一致性问题。
首先结论是该方案不能解决数据一致性问题,原因出在并发上。比如现在有 A 请求和 B 请求,这两个请求同时更新同一条数据,此时如果出现如下顺序:
此时,数据库中的数据为 2,而缓存中数据为 1,出现数据不一致的问题,所以该方案不成立。
该方案依然不成立,道理如方案一种的案例一样,只不过将数据库和缓存换一下位置,最后依然会出现数据不一致。
不更新缓存,而是删除缓存中的数据,然后,到读取数据时,发现缓存中没有数据后再去数据库中读取数据并更新到缓存中。
此时又引申出一个问题,是先删除缓存再更新数据库还是先更新数据库再删除缓存。
结论是改方案不成立。假设有一用户年龄为20, A 请求更新该用户年龄为21,B 请求读取该用户年龄,此时出现如下顺序:
此时,数据库中年龄为 21,而缓存中数据为 20,出现数据不一致问题。对于此情况有如下解决方案:
# 删除缓存 redis.delKey(X) # 更新数据库 db.update(X) # 睡眠 Thread.sleep(N) # 再删除缓存 redis.delKey(X)
睡眠操作主要是为了确保 A 请求在睡眠的时候, B 请求能够在这一段时间完成所有操作。最后再删掉缓存数据,所以 A 请求的睡眠时间要大于 B 请求从数据库读数据+写入缓存
的时间。但是具体睡眠多久是个玄学问题,很难评估出来,所以这个方案也只是尽可能保证数据一致性而已,还是会有数据不一致的问题,因此更建议使用方案四。
假设开始年龄数据在缓存中不存在,A 请求读取年龄,B 请求更新年龄,此时出现如下顺序:
此时,缓存中数据为 20, 而数据库中数据为 21,出现数据不一致问题。但是在实际中,这个问题出现的概率并不高,因为缓存的写入通常要远远快于数据库的写入,所以认为该方案是可以保证数据一致性的。
同时也可以给缓存加上过期时间,万一出现数据不一致,一旦数据过期被移除缓存,之后请求数据就需要从数据库中取数据,然后把数据同步到缓存中进而保持数据一致性。
但是此时还存在其他问题,即删除缓存的操作不一定成功,这就会导致客户端得到的是未更新的数据。现在有如下两种方案解决这个问题。
引入消息队列,将要从缓存中删除的数据加入到消息队列中。
在数据库更新成功后就会产生一条变更日志,记录在 binlog 中。于是可以通过订阅 binlog 日志,拿到具体要操作的数据,然后再执行缓存删除。阿里巴巴的 Canal 中间件就是基于这个实现。
Canal 模拟 MySQL 主从复制的交互协议,把自己伪装成一个 MySQL 从节点,向 MySQL 主节点发送 dump 请求,MySQL 收到请求后就会开始推送 binlog 给 Canal,Canal 解析 binlog 字节流后,转换为便于读取的结构化数据,供下游订阅使用。
如果在业务中对缓存命中率有很高的要求,此时就需要更新数据库+更新缓存
的方案。为了解决数据不一致问题,现有提供如下两种做法: