文章目录
- 前言
- 一、缓存案例
- 1.1 缓存常见用法
- 1.2 缓存不一致产生的原因
- 二、解决方案
- 2.1 先删除缓存,再更新数据库
- 2.2 先更新数据库,删除缓存
- 2.3 只更新缓存,由缓存自己同步更新数据库
- 2.4 只更新缓存,由缓存自己异步更新数据库
- 2.5 引入 MQ
- 三、总结
前言
在高并发的场景下,大量的请求直接访问MySQL很容易造成性能瓶颈。所以,我们都会用Redis来做数据的缓存,削减对数据库的压力。但是,MySQL和Redis是两种不同的数据库,如何保证不同数据库之间数据的一致性就非常关键了。
关于MySQL和Redis中的数据一致性问题,可以先说一下结论:没有完美的方案,只有最适合某场景的方案。这个问题表面上看是数据一致性的问题,其实根本上,又是数据一致性、系统性能和系统复杂度的选择与取舍。我们所能做到的是尽可能让它们的数据在绝大部分时间内保持一致,并保证最终是一致的。
一、缓存案例
1.1 缓存常见用法
通常情况下,我们使用缓存的主要目的是为了提升查询的性能。大多数情况下,我们使用缓存的例子如下图。
1.用户请求过来之后,先查缓存有没有数据,如果有则直接返回。
2.如果缓存没数据,再继续查数据库。
3.如果数据库有数据,则将查询出来的数据,放入缓存中,然后返回该数据。
4.如果数据库也没数据,则直接返回空。
这是缓存非常常见的用法,一眼看上去,好像没有啥问题。但如果数据库中的某条数据,放入缓存之后,又立马被更新了,那么缓存中的数据就和数据库中的数据不一致了。
1.2 缓存不一致产生的原因
如果数据一直没有变更,那么就不会出现Redis和MySQL数据不一致性的问题。
两者之间数据不一致是因为一者发生了数据的变更,另一者如何在短时间内同步数据的问题。因为每次数据变更需要同时操作数据库和缓存,而他们又属于不同的系统,无法做到同时操作成功或失败,总会有一个时间差。在并发读写的时候可能就会出现缓存不一致的问题。
二、解决方案
缓存更新的设计方法大概有5种,下面分别对这四种方案进行描述。
2.1 先删除缓存,再更新数据库
-
这种方法在并发读写的情况下容易出现缓存不一致的问题。
-
如上图所示,其可能的执行流程顺序为:
1.客户端1 触发更新数据A的逻辑
2.客户端2 触发查询数据A的逻辑
3.客户端1 删除缓存中数据A
4.客户端2 查询缓存中数据A,未命中
5.客户端2 从数据库查询数据A,并更新到缓存中
6.客户端1 更新数据库中数据A
可见,最后缓存中的数据A跟数据库中的数据A是不一致的,缓存中的数据A是旧的脏数据。因此一般不建议使用这种方式。
2.2 先更新数据库,删除缓存
- 这种方法在并发读写的情况下,也可能会出现短暂缓存不一致的问题。
- 如上图所示,其可能执行的流程顺序为:
1.客户端1 触发更新数据A的逻辑
2.客户端2 触发查询数据A的逻辑
3.客户端3 触发查询数据A的逻辑
4.客户端1 更新数据库中数据A
5.客户端2 查询缓存中数据A,命中返回(旧数据)
6.客户端1 让缓存中数据A失效
7.客户端3 查询缓存中数据A,未命中
8.客户端3 查询数据库中数据A,并更新到缓存中
可见,最后缓存中的数据A和数据库中的数据A是一致的,理论上可能会出现一小段时间数据不一致,不过这种概率也比较低,大部分的业务也不会有太大的问题。
2.3 只更新缓存,由缓存自己同步更新数据库
- 只更新缓存,再由缓存去同步更新数据库。一个Write Through的例子如下:
- 如上图所示,其可能的执行流程顺序为:
1.客户端1 触发更新数据A的逻辑
2.客户端2 触发查询数据A的逻辑
3.客户端1 更新缓存中的数据A,返回
4.客户端2 查询缓存中的数据A,命中返回
5.缓存异步更新数据A到数据库中
这种方式的优势是读写的性能都非常好,基本上只要操作完内存后就返回给客户端了,但是其是非强一致性,存在丢失数据的情况。
如果在缓存异步将数据更新到数据库中时,缓存服务挂了,此时未更新到数据库中的数据就丢失了。
2.4 只更新缓存,由缓存自己异步更新数据库
- 只操作更新缓存,再由缓存异步去更新数据库,例如:
- 如上图所示,其可能的执行流程顺序为:
1.客户端1 触发更新数据A的逻辑
2.客户端2 触发查询数据A的逻辑
3.客户端1 更新缓存中的数据A,返回
4.客户端2 查询缓存中的数据A,命中返回
5.缓存异步更新数据A到数据库中
这种方式的优势是读写的性能都非常好,基本上只要操作完内存后就返回给客户端了,但是其是非强一致性,存在丢失数据的情况。
如果在缓存异步将数据更新到数据库中时,缓存服务挂了,此时未更新到数据库中的数据就丢失了。
2.5 引入 MQ
在高并发的业务场景中,MQ(消息队列)是必不可少的技术之一。它不仅可以异步解耦,还能削峰填谷。对保证系统的稳定性是非常有意义的。MQ的生产者生产了消息之后,通过指定的topic发送到MQ服务器。然后MQ的消费者订阅该topic的消息,读取消息数据之后,做业务逻辑处理。使用MQ重试的具体方案如下:
- 当用户操作写完数据库,但删除缓存失败了,产生一条消息,发送给MQ服务器。
- 消费者读取MQ消息,重试5次删除缓存。如果其中有任意一次成功了,则返回成功。如果重试了5次,还是失败,则写入死信队列中。
- 推荐MQ使用RocketMQ,重试机制和死信队列默认是支持的。使用起来非常方便,而且还支持顺序消息,延迟消息和事务消息等多种业务场景。
当然在该方案中,删除缓存可以完全走异步。即用户的写操作,在写完数据库之后,不用立刻删除一次缓存。而直接发送消息,到MQ服务器,然后有消费者全权负责删除缓存的任务。因为MQ的实时性还是比较高的,因此改良后的方案也是一种不错的选择。
三、总结
1.我们能放入缓存的数据本就不应该是实时性、一致性要求超高的。所以缓存数据的时候加上过期时间,保证能够再容忍的时间段内拿到当前最新数据即可。
2.我们不应该过度设计,增加系统的复杂性。
3.遇到实时性、一致性要求高的数据,就应该查数据库,即使慢点。