欢迎关注公众号(通过文章导读关注:【11来了】),及时收到 AI 前沿项目工具及新技术的推送!
在我后台回复 「资料」 可领取
编程高频电子书
!
在我后台回复「面试」可领取硬核面试笔记
!文章导读地址:点击查看文章导读!
感谢你的关注!
前言:
🚀 春招季即将来临,你准备好迎接挑战了吗? 🌟
🎯 【30天面试冲刺计划】 —— 专为大厂面试量身定制!
🔥 跟随学习,一起解锁面试新高度! 🔥
文章目录
- 滴滴后端二面:12306场景设计、Redis缓存设计、MyBatis两级缓存(下)
- 题目分析
- 5、坐过高铁吧,有抢过票吗?你说说抢票会有哪些情况?
- 6、现在我们来给 12306 抢票系统设计一个缓存,kv 存什么?
- 7、那对于数据不一致我们一般怎么处理?
滴滴后端二面:12306场景设计、Redis缓存设计、MyBatis两级缓存(下)
题目分析
5、坐过高铁吧,有抢过票吗?你说说抢票会有哪些情况?
抢票会存在线程安全的问题,因为高铁票是作为一个共享的数据存在,多个线程去读写共享的数据,就会存现线程安全的问题
具体的线程不安全问题就是:高铁票的 少卖
和 超卖
先说一下整个抢票中所涉及的流程:生成订单、扣减库存、用户支付
那么为了保证高并发,扣减库存的操作可以放在本地去做,生成订单的操作通过异步,可以大幅提高系统并发度
接下来先说一下如何 优化抢票性能
:
将库存放在每台机器的本地,比如总共有 1w 个余票库存,共有 100 台机器,那么就在每台机器上方 100 个库存
当用户抢票之后,就会在本地先扣减库存,如果本地库存不足,此时可以给用户返回一个友好提示,让用户稍后再重试抢票,再将用户抢票的请求路由到其他有库存的机器上去
如果本地库存足够的话,就先扣除本地库存,之后再发送一个 MQ 消息异步的生成高铁票的订单,等待用户支付,如果用户十分钟内不支付的话,订单就失效,返还库存
接下来分析一下上边的流程是否会出现少卖和超卖的问题:
对于超卖来说,每次用户请求时,先扣除库存,再去生成订单,这样当库存不足时,就不会再生成订单了,因此肯定不会出现超卖的问题
对于少卖来说,总共有 100 台机器,每台机器有 100 个库存,如果其中的几台机器宕机了,那么宕机的机器上的库存就没办法继续售卖,就会出现少卖的问题
解决少卖问题:
可以在每台机器上放一些冗余的库存,如果其他机器发生了宕机,就将宕机的机器上的库存给放到健康的机器上去,就可以避免机器宕机而导致一部分库存卖不出去的问题了
那么这样的话,就需要使用 Redis 来统一管理每台机器上的库存,也就是在分布式缓存 Redis 中存储一份缓存,在每台机器的本地也存储一份缓存,当扣减完机器本地的库存之后,再去发送一个远程请求扣减 Redis 上的库存
最后完整的抢票流程:
- 用户发出抢票请求,在本地进行扣减库存操作
- 如果本地库存不足,返回用户友好提示,可以稍后重试,如果所有机器上的库存都不足的话,可以直接返回用户已售罄的提示
- 如果本地库存充足,在本地扣减库存之后,再向 Redis 中发送网络请求,进行库存扣减(这里 Redis 的作用就是统一管理所有机器上的库存数量)
- 扣减库存之后,再发送 MQ 消息,异步的生成订单,之后等待用户支付即可
如有不足,欢迎指出
6、现在我们来给 12306 抢票系统设计一个缓存,kv 存什么?
在回答的时候,要先给面试官分析一下业务场景,再说怎么去设计缓存
在 12306 中如果要设计缓存的话,可以考虑给余票设计一个缓存,因为余票信息是读取比较多的数据,并且在首页,放在缓存中可以大大加快用户查询的速度,如下图
- 余票信息缓存
余票信息缓存的话,将车站到车站之间的信息以及余票信息给存储到缓存中,比如当用户查询 A 车站到 B 车站的车票信息时,直接从缓存中获取,如果缓存中没有的话,去数据库中查询,并且在 Redis 缓存中构建一份缓存数据
key 设计为站点的信息,比如查询 2023 年 12 月 15 日 A 车站到 B 车站的车票信息:remaining_ticket_info:{year}:{month}:{day}:{起使车站}:{终止车站}
value 为起使车站到终止车站的信息,比如车次号、余票信息、票价信息、经过车站等一些信息
这里我觉得余票数量可以和其他缓存给分开存储,因为像余票信息的话,用户购买后是需要修改的,如果将余票数量和其他缓存数据放在一起的话,每次修改的时候,都要重新构建很多数据,比较麻烦
- 余票数量缓存
余票数量缓存的 key 设计为:remaining_ticket_num:{year}:{month}:{day}:{起使车站}:{终止车站}
value :存储余票的数量
扩展:MyBatis 两级缓存
这里既然说了 Redis 缓存了,就再扩展说一下 MyBatis 的两级缓存吧
MyBatis 设计了两级缓存用于提升数据的检索效率,避免每次数据的访问都需要去查询数据库
MyBatis 一级缓存:
MyBatis 中的一级缓存是会话(SqlSession)级别的,也叫本地缓存,每个用户在执行查询的时候都需要使用 SqlSession 来执行查询语句,MyBatis 将查询出来的数据缓存到 SqlSession 的本地缓存中,避免每次查询都去操作数据库
MyBatis 默认开启一级缓存
一级缓存在执行了增、删、改操作之后,缓存中的相关数据会失效并删除
MyBatis 二级缓存:
MyBatis 中的二级缓存是跨 SqlSession 级别的,一级缓存是只在当前的 SqlSession 中生效,当多个用户在查询数据时,只要有一个 SqlSession 拿到了数据就会存入到二级缓存中去,其他的 SqlSession 都可以读取到二级缓存中的数据
一级、二级缓存的缺点:
一级缓存的话是基于 SqlSession 的,如果 SqlSession 关闭,那么会导致一级缓存中的数据被清空
MyBatis 的一、二级缓存都会出现数据不一致的情况,由于 MyBatis 的缓存都是基于本地的,因此分布式环境中,多个机器的 SqlSession 修改了数据库的数据,如果有些 SqlSession 没有及时更新缓存中的数据,会导致数据不一致,产生了脏数据
一级缓存和二级缓存的使用:
一级缓存默认开启,因此不用手动去设置
二级缓存默认关闭,如果系统对数据的一致性要求不严格,那么开启二级缓存可以显著提升性能
MyBatis 查询流程:
在用户进行查询时,如果开启了二级缓存,则会先去二级缓存中进行查询,如果二级缓存中没有,再去查一级缓存,最后查询数据库
MyBatis 一二级缓存实现原理图:
一级缓存效果演示:
public class MyBatisCacheDemo {
public static void main(String[] args) {
try (InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml")) {
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
// 第一次查询,会执行 SQL 并缓存结果
YourMapper mapper = sqlSession.getMapper(YourMapper.class);
User user = mapper.selectUserById(1);
// 输出第一次查询的结果
System.out.println("First query result: " + user);
// 第二次查询,由于一级缓存,不会执行 SQL,直接从缓存中获取结果
User cachedUser = mapper.selectUserById(1);
System.out.println("Second query result (cached): " + cachedUser);
// 确保两次查询返回的是同一个对象
System.out.println("Are they the same object? " + (user == cachedUser));
} finally {
sqlSession.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
7、那对于数据不一致我们一般怎么处理?
这里的不一致指的应该是数据库和缓存的数据不一致,这里就以商品缓存的更新举例:
如果不采用更新数据时双写来保证数据库与缓存的一致性的话,可以通过 canal + RocketMQ
来实现数据库与缓存的最终一致性,对于数据直接更新 DB,通过 canal 监控 MySQL 的 binlog 日志,并且发送到 RocketMQ 中,MQ 的消费者对数据进行消费并解析 binlog,过滤掉非增删改的 binlog,那么解析 binlog 数据之后,就可以知道对 MySQL 中的哪张表进行 增删改
操作了,那么接下来我们只需要拿到这张表在 Redis 中存储的 key,再从 Redis 中删除旧的缓存即可,那么怎么取到这张表在 Redis 中存储的 key 呢?
可以我们自己来进行配置,比如说监控 sku_info
表的 binlog,那么在 MQ 的消费端解析 binlog 之后,就知道是对 sku_info
表进行了增删改的操作,那么假如 Redis 中存储了 sku 的详情信息,key 为 sku_info:{skuId}
,那么我们就可以在一个地方对这个信息进行配置:
// 配置下边这三个信息
tableName = "sku_info"; // 表示对哪个表进行最终一致性
cacheKey = "sku_info:"; // 表示缓存前缀
cacheField = "skuId"; // 缓存前缀后拼接的唯一标识
// data 是解析 binlog 日志后拿到的 key-value 值,data.get("skuId") 就是获取这一条数据的 skuId 属性值
// 如下就是最后拿到的 Redis 的 key
redisKey = cacheKey + data.get(cacheField)
那么整体的流程图如下: