网站首页 > 技术文章 正文
快速了解:
1、redis缓存与数据库数据一致性产生原因
2、解决数据一致性的常见方案
面试官可能问:
1、有这么一个场景,我读取redis数据的时候,发现与数据库的数据不一致,这种情况怎么规避呢?
2、redis缓存什么情况下导致与数据库数据不一致
3、如何保证redis缓存与数据库数据一致性
日常生产场景中,为了避免大量请求同时打在数据库上导致故障或者快速响应数据,数据库+缓存的方式已经成了日常标配。引入了缓存,必然会带来缓存一致性的问题,如果想达到强一致性,只能加锁,但是会损耗性能,并且降低了并发性,所以大部分追求的是数据的最终一致性。如何解决数据的最终一致性,到底是先操作数据库还是先操作缓存,这点可能困扰着很多人。
从大方向的解决方案看:
一、先操作缓存、再操作数据库
二、先操作数据库、再操作缓存
细分:
先更新或删除缓存,再操作数据库;先操作数据库,再更新或者删除缓存。操作缓存的话也有说法,是更新呢,还是删除呢,这里的缓存操作推荐直接删除而不是更新,因为可以尽可能的避免脏数据的读取,还有另外一个是懒加载的思想,因为每次更改数据之后,不一定立马就有人来用。如果缓存更新的成本很高的话(比如涉及到十几张表的查询计算),此时也会非常浪费性能资源。
从项目的解决场景中,衍生出了很多细致的解决方案,比如延迟双删、监听binglog删除以及重试方案等等,其目的都是保证数据最终一致性。
常见的解决方案
方案:先更新数据库,后删除缓存/先删除缓存,再操作数据库
方案:延迟双删
延迟双删:首先,删除缓存中的数据;然后,更新数据库中的数据;最后,在一定的延迟(例如睡眠一段时间)后,再次删除缓存中的数据
演变1:睡眠一会再删。弊端:可能仍然存在旧数据的读取(发生的节点:1、在第一次删除缓存之后,未更新完数据库,就有新的线程来读取的,读取的仍然是旧值,尽管第二次有再次删除缓存的操作,也可能有删除失败的风险。2、或者例如线程2的读取写入缓存的时候,线程1的数据库更新事务仍未提交)
演变二:引入MQ进行重试删除。主要是规避演变1的二次缓存删除失败的场景,但同时也提高了系统代码的耦合度。
方案:异步监听binlog删除 + 重试
核心流程:
- 更新数据库
- 监听binlog删除缓存(databus、Canal)
- 缓存删除失败则通过MQ不断重试,直至删除成功
1)脏数据时间窗口“较大”
这个脏数据时间窗口较大,是相对同步删除来说。在你收到binlog之前,他中间要经过:binlog从主库同步到从库、binlog从库到binlog监听组件、binlog从监听组件发送到MQ、消费MQ消息,这些操作每个都是有一定的耗时的,可能是几十毫秒甚至几百毫秒,所以说它其实整体是有一个脏数据的时间窗口。
而同步删除是在更新完数据库后马上删除,时间窗口大概也就是1毫秒左右,所以说binlog的方式相对于同步删除,可能存在的脏数据窗口会稍微大一点。
2)极端场景下存在长期脏数据问题
- binlog抓取组件宕机导致脏数据。该方案强依赖于监听binlog的组件,如果监听binlog组件出现宕机,则会导致大量脏数据。
- 拆库拆表流程中可能存在并发脏数据
拆库拆表流程中并发脏数据问题
表A正在进行数据库拆分,当前进行到灰度切读流量阶段:部分读新库,部分读老库
数据库拆分大致流程:增量数据同步(双写)、全量数据迁移、数据一致性校验、灰度切读、切读完毕后停写老库。
小结:该方案在大多数场景下没有太大问题,业务比较小的场景可以使用,或者在其基础上进行适当补充。
读+缓存请求流程
读数据,如果缓存没有,则读mysql,并且回写到缓存的逻辑代码的实现?
双检加锁机制:加锁前从redis中查一次,加锁后再查一次。
@Service
@Slf4j
public class UserService {
public static final String CACHE_KEY_USER = "user:";
@Resource
private UserMapper userMapper;
@Resource
private RedisTemplate redisTemplate;
/**
* 业务逻辑没有写错,对于小厂中厂(QPS<=1000)可以使用,但是大厂不行
* @param id
* @return
*/
public User findUserById(Integer id)
{
User user = null;
String key = CACHE_KEY_USER+id;
//1 先从redis里面查询,如果有直接返回结果,如果没有再去查询mysql
user = (User) redisTemplate.opsForValue().get(key);
if(user == null)
{
//2 redis里面无,继续查询mysql
user = userMapper.selectByPrimaryKey(id);
if(user == null)
{
//3.1 redis+mysql 都无数据
//你具体细化,防止多次穿透,我们业务规定,记录下导致穿透的这个key回写redis
return user;
}else{
//3.2 mysql有,需要将数据写回redis,保证下一次的缓存命中率
redisTemplate.opsForValue().set(key,user);
}
}
return user;
}
/**
* 加强补充,避免突然key失效了,打爆mysql,做一下预防,尽量不出现击穿的情况。
* @param id
* @return
*/
public User findUserById2(Integer id)
{
User user = null;
String key = CACHE_KEY_USER+id;
//1 先从redis里面查询,如果有直接返回结果,如果没有再去查询mysql,
// 第1次查询redis,加锁前
user = (User) redisTemplate.opsForValue().get(key);
if(user == null) {
//2 大厂用,对于高QPS的优化,进来就先加锁,保证一个请求操作,让外面的redis等待一下,避免击穿mysql
synchronized (UserService.class){
//第2次查询redis,加锁后
user = (User) redisTemplate.opsForValue().get(key);
//3 二次查redis还是null,可以去查mysql了(mysql默认有数据)
if (user == null) {
//4 查询mysql拿数据(mysql默认有数据)
user = userMapper.selectByPrimaryKey(id);
if (user == null) {
return null;
}else{
//5 mysql里面有数据的,需要回写redis,完成数据一致性的同步工作
redisTemplate.opsForValue().setIfAbsent(key,user,7L,TimeUnit.DAYS);
}
}
}
}
return user;
}
}
- 上一篇: 分库分表最全详解(图文全面总结)
- 下一篇: 分布式系统一致性保障:CAP理论与BASE原则
猜你喜欢
- 2024-11-30 分布式系统最全详解(图文全面总结)
- 2024-11-30 架构设计最全详解(万字图文总结)
- 2024-11-30 最佳实践:确保MySQL和Redis数据一致性的方法
- 2024-11-30 面试京东实习,如何保障三个数据库的数据一致性?
- 2024-11-30 无锁编程——从CPU缓存一致性讲到内存模型
- 2024-11-30 分布式系统一致性保障:CAP理论与BASE原则
- 2024-11-30 分库分表最全详解(图文全面总结)
- 2024-11-30 微服务架构数据一致性的解决思路
- 2024-11-30 如何确保高并发秒杀场景下Redis与数据库库存的一致性
- 2024-11-30 CPU缓存一致性原理
你 发表评论:
欢迎- 最近发表
- 标签列表
-
- oraclesql优化 (66)
- 类的加载机制 (75)
- feignclient (62)
- 一致性hash算法 (71)
- dockfile (66)
- 锁机制 (57)
- javaresponse (60)
- 查看hive版本 (59)
- phpworkerman (57)
- spark算子 (58)
- vue双向绑定的原理 (68)
- springbootget请求 (58)
- docker网络三种模式 (67)
- spring控制反转 (71)
- data:image/jpeg (69)
- base64 (69)
- java分页 (64)
- kibanadocker (60)
- qabstracttablemodel (62)
- java生成pdf文件 (69)
- deletelater (62)
- com.aspose.words (58)
- android.mk (62)
- qopengl (73)
- epoch_millis (61)
本文暂时没有评论,来添加一个吧(●'◡'●)