前言
前面的篇章讲述了关于redis的客户端的实战使用,但是会用客户端还不够,我们在平时开发的时候还是会遇到很多问题,比如说读多写少的高并发场景,我们肯定会使用缓存来提升速度,但是我们同时会遇到数据一致性的问题。
那关于这个数据一致性的问题,我们要怎么处理比较好呢?
正文
数据一致性
缓存使用场景
针对读多写少的高并发场景,我们可以使用缓存来提升查询速度。 当我们使用 Redis 作为缓存的时候,一般流程是这样的:
- 如果数据在 Redis 存在,应用就可以直接从 Redis 拿到数据,不用访问数据库。
- 如果 Redis 里面没有,先到数据库查询,然后写入到 Redis,再返回给应用。
一致性问题的定义
因为这些数据是很少修改的,所以在绝大部分的情况下可以命中缓存。但是,一旦被缓存的数据发生变化的时候,我们既要操作数据库的数据,也要操作 Redis 的数据,所以问题来了。 现在我们有两种选择:
-
先操作 Redis 的数据再操作数据库的数据
-
先操作数据库的数据再操作 Redis 的数据
- 到底选哪一种?
首先需要明确的是,不管选择哪一种方案, 我们肯定是希望两个操作要么都成功,要么一个都不成功。不然就会发生 Redis 跟数据库的数据不一致的问题。
但是,Redis 的数据和数据库的数据是不可能通过事务达到统一的,我们只能根据相应的场景和所需要付出的代价来采取一些措施降低数据不一致的问题出现的概率,在数据一致性和性能之间取得一个权衡。
对于数据库的实时性一致性要求不是特别高的场合,比如 T+1 的报表,可以采用定时任务查询数据库数据同步到 Redis 的方案。
由于我们是以数据库的数据为准的,所以给缓存设置一个过期时间,是保证最终一致性的解决方案。
方案选择
Redis:删除还是更新?
这里我们先要补充一点,当存储的数据发生变化,Redis 的数据也要更新的时候,我们有两种方案,一种就是直接更新,调用 set;还有一种是直接删除缓存,让应用在下次查询的时候重新写入。
- 这两种方案怎么选择呢?这里我们主要考虑更新缓存的代价。
更新缓存之前,是不是要经过其他表的查询、接口调用、计算才能得到最新的数据,而不是直接从数据库拿到的值。如果是的话,建议直接删除缓存,这种方案更加简单,而且避免了数据库的数据和缓存不一致的情况。在一般情况下,我们也推荐使用删除的方案。
这一点明确之后,现在我们就剩一个问题:
- 到底是先更新数据库,再删除缓存
- 还是先删除缓存,再更新数据库
我们先看第一种方案。
先更新数据库,再删除缓存
正常情况: 更新数据库,成功。 删除缓存,成功。
异常情况:
- 更新数据库失败,程序捕获异常,不会走到下一步,所以数据不会出现不一致。
- 更新数据库成功,删除缓存失败。数据库是新数据,缓存是旧数据,发生了不一致的情况。
- 这种问题怎么解决呢?我们可以提供一个重试的机制。
比如: 如果删除缓存失败,我们捕获这个异常,把需要删除的 key 发送到消息队列。 然后自己创建一个消费者消费,尝试再次删除这个 key。 这种方式有个缺点,会对业务代码造成入侵。
所以我们又有了第二种方案(异步更新缓存): 因为更新数据库时会往 binlog 写入日志,所以我们可以通过一个服务来监听 binlog 的变化(比如阿里的 canal),然后在客户端完成删除 key 的操作。如果删除失败的话,再发送到消息队列。
总之,对于后删除缓存失败的情况,我们的做法是不断地重试删除,直到成功。
无论是重试还是异步删除,都是最终一致性的思想。
先删除缓存,再更新数据库
正常情况: 删除缓存,成功。 更新数据库,成功。
异常情况:
- 删除缓存,程序捕获异常,不会走到下一步,所以数据不会出现不一致。
- 删除缓存成功,更新数据库失败。 因为以数据库的数据为准,所以不存在数据不一致的情况。
看起来好像没问题,但是如果有程序并发操作的情况下: 1)线程 A 需要更新数据,首先删除了 Redis 缓存 2)线程 B 查询数据,发现缓存不存在,到数据库查询旧值,写入 Redis,返回 3)线程 A 更新了数据库
这个时候,Redis 是旧的值,数据库是新的值,发生了数据不一致的情况。
那问题就变成了:能不能让对同一条数据的访问串行化呢?代码肯定保证不了,因为有多个线程,即使做了任务队列也可能有多个服务实例。数据库也保证不了,因为会有多个数据库的连接。只有一个数据库只提供一个连接的情况下,才能保证读写的操作是串行的,或者我们把所有的读写请求放到同一个内存队列当中,但是这种情况吞吐量太低了。
所以我们有一种延时双删的策略,在写入数据之后,再删除一次缓存。
A 线程: 1)删除缓存 2)更新数据库 3)休眠 500ms(这个时间,依据读取数据的耗时而定) 4)再次删除缓存
伪代码:
public void write(String key,Object data){
redis.delKey(key);
db.updateData(data);
Thread.sleep(500);
redis.delKey(key);
}
By the way
有问题?可以给我留言或私聊 有收获?那就顺手点个赞呗~ 想找工作机会也可以联系我噢~
当然,也可以到我的公众号下「6曦轩」,
回复“学习”,即可领取一份 【Java工程师进阶架构师的视频教程】~
回复“面试”,可以获得: 【本人呕心沥血整理的 Java 面试题】
回复“MySQL脑图”,可以获得 【MySQL 知识点梳理高清脑图】
还有【阿里云】【腾讯云】的购买优惠噢~具体请联系我
曦轩我是科班出身的程序员,php,Android以及硬件方面都做过,不过最后还是选择专注于做 Java,所以有啥问题可以到公众号提问讨论(技术情感倾诉都可以哈哈哈),看到的话会尽快回复,希望可以跟大家共同学习进步,关于服务端架构,Java 核心知识解析,职业生涯,面试总结等文章会不定期坚持推送输出,欢迎大家关注~~~