一、什么是布隆过滤器?有什么用?
- 首先需要明白一个场景:缓存穿透,缓存穿透是指大量请求参数在Redis中无法查到,导致请求直接请求了数据库。
- 解决缓存穿透的办法
- 就是增加布隆过滤器
- 查询到的数据库数据加入到缓存中(如果大量请求不存在的数据,会Redis里面会出现很多无用的数据)
- 对请求参数增加限制,不符合规则的直接拒绝
- 什么是布隆过滤器?
- 底层数据结构:bitmap
- 存储方法
- 存储数字直接按照bit位存储
- 存储字符串,存储字符串会使用多次hash计算

- 优点
- 高效的查询性能
- 较低的内存需求
- 支持高并发访问
- 可扩展性强
- 简单高效的实现
- 缺点
- 存在误判的可能(假阳),数据不存在
- 不支持删除操作
- 无法获取元素本身的内容
- 初始化时存在计算开销
二、查询200条数据耗时200ms,如何在500ms内查询1000条数据?
- 使用多线程分批查找
java
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(5);
List<Future<List<String>>> futures = new ArrayList<>();
List<String> result = Collections.synchronizedList(new ArrayList<>());
long startTime = System.currentTimeMillis();
for (int i = 0; i < 5; i++) {
futures.add(executorService.submit(ThreadDemo::demo))
;
}
for(Future<List<String>> future : futures){
List<String> list = future.get();
result.addAll(list);
}
long endTime = System.currentTimeMillis();
System.out.println("耗时:"+(endTime - startTime) );
}
public static List<String> demo() throws InterruptedException {
List<String> res = new ArrayList<>();
for (int i = 0; i < 200; i++) {
res.add(String.valueOf(i));
}
Thread.sleep(200);
return res;
}
- 并行查找
java
public class ParallelQueryDemo {
// 模拟单个查询方法(简化版)
private List<String> queryData(int start, int size) {
// 模拟查询耗时,假设查询时间与数据量成正比
try {
Thread.sleep(size);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return IntStream.range(start, start + size)
.mapToObj(i -> "Data-" + i)
.collect(Collectors.toList());
}
// 并行查询实现
public List<String> parallelQuery(int totalSize, int batchSize) {
int batches = (totalSize + batchSize - 1) / batchSize;
return IntStream.range(0, batches)
.parallel() // 开启并行流
.mapToObj(i -> {
int start = i * batchSize;
int currentSize = Math.min(batchSize, totalSize - start);
return queryData(start, currentSize);
})
.flatMap(List::stream)
.collect(Collectors.toList());
}
public static void main(String[] args) {
ParallelQueryDemo demo = new ParallelQueryDemo();
long startTime = System.currentTimeMillis();
List<String> result = demo.parallelQuery(1000, 200);
long duration = System.currentTimeMillis() - startTime;
System.out.println("查询到数据量: " + result.size());
System.out.println("耗时: " + duration + "ms");
}
}
三、事务失效的场景
- 代码抛出TimeoutException时,可以使用注解@Transactional(rollbackFor = {TimeoutException.class})添加自己想添加的异常类型(非RuntimeException)
- 调用本类的方法时,没有创建动态代理的对象,故无法进行增加处理;
- 通过将当前类注入,然后调用方法,解决事务失效的问题
- 通过
((xxx)AopContext.currentProxy()).methodName()获取到当前调用的动态代理对象。 推荐
- 在多线程调用另外一个事务方法,多个线程使用的是同一个事务吗?子线程抛出异常,主线程能否进行回滚呢?
- 子线程:通过
((xxx)AopContext.currentProxy()).methodName()获取到当前调用的动态代理对象。 推荐 - 想要实现子线程异常,主线程也同时回滚,需要引入分布式事务,因为Spring创建多线程时,每个线程单独持有JDBC的连接,故无法进行事务回滚
- 注意 使用(xx)AopContext.currentProxy()时需要在启动类上增加
@EnableAspectJAutoProxy(exposeProxy = true)
- 子线程:通过
- 调用private方法、final修饰的方法
- 类没有被SPring管理
- 数据库不支持回滚
四、count(1)、count(*)、count(某个字段)的区别
- count(*):统计所有行的数量,包括NULL值
- count(1):统计所有行的数量,1被视为常量值
- count(字段):该字段非NULL的值
- MyISAM中count(*)较快,因为可以直接获取到总行数
- Innodb中count(*)会选择索引,然后利用索引统计总行数
- count(字段):首先会判断字段是否有索引,没有则全表扫描,有索引则走索引(就算有索引也会过滤掉NULL)
五、写一个死锁
java
public static Object res1 = new Object();
public static Object res2 = new Object();
public static void main(String[] args) {
new Thread(()->{
synchronized (res1){
System.out.println("获取到res1");
try {
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
throw new RuntimeException();
}
synchronized (res2){
System.out.println("获取到res2");
}
}
},"Thread1").start();
new Thread(()->{
synchronized (res2){
System.out.println("获取到res2");
try {
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
throw new RuntimeException();
}
synchronized (res1){
System.out.println("获取到res1");
}
}
},"Thread2").start();
}
- 死锁产生的必要条件
- 互斥使用:资源一次只能由一个进程/线程独占使用
- 不可抢占:已分配给某进程的资源,不能被其他进程强行夺取
- 请求和保持:进程持有至少一个资源,同时等待获取其他被占用的资源
- 循环等待:存在一个进程等待的循环链
- 解决死锁的方法
- 设置超时时间:tryLock(timeout)
- 降低锁的粒度
- 避免锁嵌套:设计时避免在一个锁内获取另一个锁
- 专锁专用:为不同资源分配独立的锁
- 银行家算法:系统在分配资源前,先计算此次分配是否会导致系统进入不安全状态
六、Eureka实现原理
- 服务注册与发现
当一个服务实例启动时,会向Eureka发送注册请求,将自己的信息注册到注册中心,Eureka Server会把这些信息保存到内存中,并提供REST接口供其他服务查询。服务消费者可以通过查询服务实例列表来获取服务提供者实例信息,从而实现服务发现 - 服务健康检查
Eureka通过心跳机制来检测实例的健康状态,服务实例会定期向Eureka Server发送心跳,表明自己的状态,如果一段时间内没有收到某个实例的心跳,则标记为不可用,并从服务列表移除,下线实例 - 服务负载均衡
Eureka客户端再调用其他服务时,会从本地缓存中获取服务的注册信息,如果本地没有,则会向Eureka Server发送查询请求,收到返回的服务实例列表后,客户端进行负载均衡然后调用
七、如何防止秒杀、抽奖、抢购中的羊毛党?
- 注册时增加手机号验证、实名认证
- 下单时增加动态图形验证码
- 判断账号的状态(新号)
- 提高消费门槛
- 设置一些规则,为用户做分类,优待忠诚用户
- 限流,自动扩容伸缩
- 设置黑名单
- 给用户做画像,建模(腾讯云;天御)
八、Spring boot读取配置文件的六种方式
- @Value("${value:}");在static和final上无法生效
- @ConfigurationProperties(prefix = "xxx");可以将配置文件映射到对象上,适合批量映射
- 通过注入Environment ,然后调用getProperty("xxxx")方法
- @PropertySources(value = "xxxx",encoding = "utf-8")获取外部配置文件
九、缓存穿透、缓存击穿、缓存雪崩
- 缓存穿透是指请求查询数据时,缓存中没有该数据,请求会去查询数据库,这样的话缓存就是去了意义,大量请求会导致服务不可用
解决方案:- 缓存空结果信息:可能会有大量空数据
- 布隆过滤器:具体实现见:一、
- 过滤非法的参数,拦截无效请求
- 缓存击穿:指热点数据在高并发场景下过期,大量请求直接请求数据库
解决方案:- 热点数据永不过期
- 当发现热点数据过期时,就加锁去更新缓存
- 定时任务刷新
- 缓存雪崩:是指大量的热点数据同时过期
- 设置热点数据永不过期
- 缓存分批过期,防止同一时间过期
十、本地消息表如何保持分布式一致性的?
- 第一步:事务执行
业务服务在本地数据库执行业务操作(如创建订单)
在同一个事务中,向本地消息表插入一条待发送的消息记录
只有这两个操作都成功,事务才会提交 - 第二步:消息投递
后台定时任务定期扫描本地消息表
发现待发送的消息后,将其发送到消息队列(如RabbitMQ)
发送成功后更新消息状态为"已发送" - 第三步:消息消费
消费者从消息队列获取消息
处理业务逻辑(如扣减库存)
处理成功后向消息队列确认 - 异常处理
如果消息发送失败,会保留在本地消息表中等待下次重试
如果消费失败,消息队列会重新投递
设置最大重试次数,超过后进入死信队列人工处理
十一、什么雪花算法?
- 64bit的long类型
- 第一个bit代表 正负(0-正,1-负)
- 1-42bit 毫秒级时间戳
- 42bit - 52 bit (机房和机器ID),前五bit代表机房ID,后五bit代表机器ID
- 最后12bit (同一毫秒内产生的ID),序列号
十二、Mysql索引失效的场景
- 不满足最左匹配原则
假设索引包含code、name、age,查询时需要按照顺序判断,否则不会使用全部索引 - 使用Select *
- order By:排序方向不一致;使用非索引列排序;查询字段和排序字段不是联合索引
- 字段类型不同
- like 语句左边包含%
- 列对比
- 使用or(8.0中or左右两边都有索引,会索引生效)
- 范围查询(范围查询回表较少会走索引,回表较多不走索引)
- 隐式转换
十三、Spring和Spring boot 代码不同之处
-
重名Bean
- Spring 是后面的Bean覆盖前面的
- Spring Boot启动会报错,可以通过配置文件
javaspring.main.allow-bean-definition-overriding= true spring.main.profiles.active= default -
循环依赖
- Spring支持循环依赖
- Spring boot不支持,可以通过配置保持和Spring一致
javaspring.main.allow-circular-references=true -
@Value获取不到值
- Spring使用@Value获取不到值得时候,会把@Value里面得参数作为值
- Spring boot使用@Value获取不到值得时候会报错,可以使用
@Value("${xxx:defaultValue}")配置 - 或者通过如下配置实现保持和Spring一致
java
@Configuration
public class AppConfig {
@Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer();
configurer.setIgnoreUnresolvablePlaceholders(true); // 关键设置
// 其他设置...
return configurer;
}
}
- 动态代理类型
- Spring中类如果实现了接口则使用JDK动态代理,如果没有实现则使用CGLib动态代理
- Spring boot默认使用CGLib动态代理;也可以配置spring.aop.proxy-target-class=false配置和Spring一致
十四、如何使用Redis记录上亿用户连续登录天数
- 使用bitmap存储数据,存储30天的的key,然后根据用户ID映射到bitmap上,最终计算总天数只需要从最后一天往前推,找到对应用户的标志位进行判断即可
十五、 Tomcat默认线程数是多少?
- 默认线程数是200
十六、Mysql最左前缀原则一定需要最左列吗?
- 不一定
- 如果查询的字段可以走到索引覆盖,则可以使用索引
十七、CAP定理和BASE理论
- CAP定理
- C:一致性
- A:可用性
- P:分区容错性
- Base理论
- BA:基本可用:使用服务降级达到效果
- S:软状态:程序运行的中间态
- E:最终一致性:允许程序一段时间内状态不一致
十八、什么内存泄漏、内存溢出
- 内存泄漏:堆内存中存在内存空间无法释放,如数据库连接未关闭、Map/List中参在大量的对象未清理等
- 内存溢出:堆内存空间满了,如大对象/大数据集操作;内存配置不当;元空间不足,栈溢出(递归)
十九、Mysql隐式转换
- 当字符串与数字比较时,Mysql会将字符串转为数字
java
SELECT '10' > 9; -- 结果为 1(true),因为 '10' 被转换为 10
- 当字符串与日期比较,会将字符串转换为日期
java
SELECT '2023-10-01' > '2023-09-30'; -- 结果为 1(true),字符串被转换为日期
- 当数字与日期比较,会将数字转换为日期
java
SELECT 20231001 > '2023-09-30'; -- 结果为 1(true),数字被转换为日期
- 当字符串和布尔值比较,会将布尔值转换为数字(true = 1,false = 0)
java
SELECT '1' = TRUE; -- 结果为 1(true),因为 '1' 被转换为 1
隐式类型转换导致的问题
- 索引失效:如果字段是varchar类型,通过id = 1则会导致索引失效
- 数据丢失
- 意外的结果
- 性能问题:隐式转换也会导致性能的开销
二十、 MVCC多版本并发控制
- 实现原理通过undolog +read view控制
undolog:存储历史版本数据链
read view:存储当前事务ID信息
具体参考MVCC多版本并发控制详解
二十一、服务降级和服务熔断对比
| 特性 | 服务降级 (Degradation) | 服务降级 (Degradation) |
|---|---|---|
| 核心目的 | 保核心、舍边缘:确保核心业务可用,主动放弃部分非关键功能或内容。 | 防雪崩、快恢复:防止不断失败调用拖垮系统,快速失败并自我修复。 |
| 触发条件 | 主动预测:通常由人工预设(如大促期间)、或系统资源(如CPU、线程池)达到阈值时触发。 | 到阈值时触发。 被动响应:由连续失败次数/失败率达到阈值时自动触发。 |
| 动作对象 | 功能维度:关闭或简化某个或某些非核心功能(如关闭商品评论、将推荐算法降级为热门榜单)。 | 服务维度:切断对某个故障下游服务的全部调用。 |
| 状态机制 | 无状态:降级开关只有"开"和"关"两种简单状态。 | 有状态机:包含"关闭"、"开启"、"半开" 三种状态,自动探测下游是否恢复。 |
| 思维角度 | 战略放弃:是一种设计理念和预案,着眼于整体系统稳定性。 | 战术防护:是一种容错机制,着眼于单个服务的故障隔离与恢复。 |
| 举例 | 餐厅人多时:老板主动宣布"今天不提供炒饭和甜品",以保证主菜能快速出餐。 | 厨房着火了:经理立刻拉下电闸(熔断),防止火势蔓延。等火扑灭后,再试探性地送电(半开)。 |
二十二、内存200M读取1G文件,并统计重复数据次数
- 解决办法哈希分片
详细见后续的案例代码
二十三、防止用户重复下单
- 通过redis的setnx实现,通过用户ID+商品等关键因素进行判断
- 可以在进入下单详情页的时候生成一个唯一的订单ID,然后通过redis的setnx实现
二十四、String s= "s",和String s = new String("s")的区别?
- String s = "s" 最多创建1个对象,如果字符常量池中已有则直接引用
- String s = new String("s") 最多创建两个对象,如果字符串常量池中已有则不会创建"s",仅创建String对象
二十五、 MQ如何解决消息丢失问题
- 生产者:同步发送消息,阻塞线程,并增加事务回滚;异步+重试+人工补偿
- MQ服务:更改为同步刷盘机制,保证服务宕机不会导致消息丢失
- 消费者:通过自动重试机制,通知MQ消费失败,下次重新消费
二十六、MQ的重复消费?如何避免?如何做到幂等性
- 待续