大家好,我是一安,今天聊一下实际开发中如何保证缓存数据一致性。
前言
数据库和缓存双写数据一致性问题,是一个跟开发语言无关的公共问题。尤其在高并发的场景下,这个问题变得更加严重。
通常情况下,为了系统性能的提升,我们一般都会将部分数据放入缓存中以便快速访问,而数据库承担数据落盘工作。
实际项目中我们是这样使用缓存的:
注意:
高并发下查缓存需要加锁来避免缓存穿透或缓存击穿,锁可以用本地锁也可以采用分布式锁,可查看Redis缓存失效问题:缓存穿透-缓存雪崩-缓存击穿
在开发中,凡是放入缓存中的数据都应该指定过期时间,使其可以在系统即使没有主动更新数据也能自动触发数据加载进缓存的流程,避免业务崩溃导致的数据永久不一致问题
正常情况下,这样查询是没任何问题的,但如果数据库某条数据更新,怎么办?
-
如果对一致性要求没那么严格,可以等缓存过期后自动更新 -
如果对一致性要求很严格,必须马上看到,这就需要我们能及时的更新缓存数据,常见的方案有: -
先写缓存,再写数据库 -
先写数据库,再写缓存 -
先删缓存,再写数据库 -
先写数据库,再删缓存
下面就围绕这几种方案一一介绍:
正文
这里正常情况是指缓存和数据库都可以写入,不存在其中某一环节宕机或断电说法。
先写缓存,再写数据库
-
正常情况且非高并发,看着还可以哦 -
正常情况且高并发 -
非正常情况(不考虑缓存和数据库都无法写入) -
如果刚写完缓存,突然网络出现了异常,导致写数据库失败了,这种情况是缓存有,数据库没有,此时缓存中的数据就变成了脏数据
先写数据库,再写缓存
这种方案可以避免脏数据带来的问题。
-
正常情况且非高并发,看着也还可以哦 -
正常情况且高并发 -
非正常情况(不考虑缓存和数据库都无法写入) -
如果把写数据库和写缓存操作在同一个事务当中,当写缓存失败了,我们可以把写入数据库的数据进行回滚,仅适合并发量较小且对接口性能要求不太高的系统 -
如果把写数据库和写缓存操作不在同一个事务当中,最终也会导致数据库和缓存数据不一致
先删缓存,再写数据库
这种方案可以避免脏数据的问题。
-
正常情况且非高并发,看着也还可以哦 -
正常情况且高并发 -
两个写操作 -
一个读操作,一个写操作
网上有人提出缓存双删: 即在写数据库之前删除一次,写完数据库后,再删除一次。但你如何保证第二次删就一定在读操作之后呢?
先写数据库,再删缓存
-
正常情况且非高并发,看着也还可以哦 -
正常情况且高并发 -
一个读操作,一个写操作
总结
推荐大家使用先写数据库,再删缓存的方案,虽说不能100%避免数据不一致问题,但出现问题的概率,相对于其他方案来说是最小的。
针对读写不一致的问题,可以采用分布式读写锁来解决
如果删除缓存异常,也会导致数据不一致,这里可以利用rabbitmq实现重试机制,想了解rabbitmq,可查看一文让你了解rabbitmq
大家有兴趣也可以去了解一下阿里开源的框架Canal
分布式读写锁伪代码:
引入redisson
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>2.9.3</version>
</dependency>
/**
* 保证一定能读到最新数据,修改期间,写锁是一个排它锁(互斥锁、独享锁),读锁是一个共享锁
* 写锁没释放读锁必须等待
* 读 + 读 :相当于无锁,并发读,只会在Redis中记录好,所有当前的读锁。他们都会同时加锁成功
* 写 + 读 :必须等待写锁释放
* 写 + 写 :阻塞方式
* 读 + 写 :有读锁。写也需要等待
* 只要有读或者写的存都必须等待
* @return
*/
@GetMapping(value = "/write")
@ResponseBody
public String writeValue() {
String s = "";
RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
RLock rLock = readWriteLock.writeLock();
try {
//1、改数据加写锁,读数据加读锁
rLock.lock();
s = UUID.randomUUID().toString();
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
ops.set("writeValue",s);
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rLock.unlock();
}
return s;
}
@GetMapping(value = "/read")
@ResponseBody
public String readValue() {
String s = "";
RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
//加读锁
RLock rLock = readWriteLock.readLock();
try {
rLock.lock();
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
s = ops.get("writeValue");
try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
} catch (Exception e) {
e.printStackTrace();
} finally {
rLock.unlock();
}
return s;
}
号外!号外!
如果这篇文章对你有所帮助,或者有所启发的话,帮忙点赞、在看、转发、收藏,你的支持就是我坚持下去的最大动力!
原文始发于微信公众号(一安未来):实际开发中,如何保证数据库和缓存双写一致性
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/44953.html