逐字稿示例(20K*14) 背调信息

简介: 张三,Java开发工程师,曾任职于广州天星数字科技有限公司,主导微服务架构设计与高并发场景优化。精通SpringCloud、Redis分布式锁、MySQL性能调优,熟悉Seata分布式事务及MQ异步解耦。擅长通过缓存预热、Lua脚本、分库分表等手段提升系统性能,在优惠券超卖控制、数据一致性保障等方面有丰富实战经验。具备良好的技术沟通能力与团队协作精神,追求代码质量与系统稳定性。

背调信息
【背调模板】
姓名:张三
上一家公司名称:广州天星数字科技有限公司
上一家公司地址:广州天河软件园孵化中心2期D栋软件路11号五楼502室A区
在公司的职位:Java开发工程师
在公司负责项目:盂县电子网格,盂县e定快信贷审批系统,智能贷后预警系统
项目主要的流程:简单描述这个项目的业务
什么时候进的公司:2023.4
什么时候离职的:2024.9
我的离职原因:奔赴爱情
公司规模:29
公司有几个开发人员:19
离职前薪资:10000
工资组成:基本+绩效+补贴
公司有哪些项目:信贷,绩效,电子网格,贷后预警
直属上级:给我取得名字
公司老板名字:真实的名字
给我设定的角色:技术总监

【背调人】
张三 176 8888 8888
人事题
离职原因
切忌不要抱怨原公司
回答样本:
上一家公司核心项目我都参与开发了2年多,没太多的技术挑战性和业务成长,长期待在舒适圈也不是个事儿,所以希望能进入更高的舞台、接触不同的业务,来提高自己的综合能力
家人在广州/深圳/武汉(自己面试公司所在地),也希望在这边长期发展,现在时间也比较合适,所以就跳槽了
期望薪资
看对方范围,如9-11K可以报价10【不要低于范围值,会降低筛选通过几率】,大致情况
专科应届:7-9K
专科3年:13-15K
本科应届:9-11K
本科3年:14-18K
专/本5年:16-25K
到岗时间
一周内,如果公司确定下发offer,我需要1-2天做体检+搬家
加班?大小周?出差?
回答提示:好多公司问这个问题并不证明一定要加班,只是想测试你是否愿意为公 司奉献。
回答样本:如果是工作需要我会义不容辞加班,我现在单身,没有任何家庭负担,可以全身
心的投入工作。但同时,我也会提高工作效率,减少不必要的加班。
职业规划
回答提示:这是每一个应聘者都不希望被问到的问题,但是几乎每个人都会被问到,比较多的答案是“管理者”。但是近几年来,许多公司都已经建立了专门的技术途径。这些工作地 位往往被称作“顾问”、“参议技师”或“高级软件工程师”等等。当然,说出其他一些你感 兴趣的职位也是可以的,比如产品销售部经理,生产部经理等一些与你的专业有相关背景的工作。考官总是喜欢有进取心的应聘者,此时如果说“不知道”,或许就会使你丧失一个好机会。最普通的回答应该是“我准备在技术领域有所作为”或“我希望能按照公司的管理思路发展”。
回答样本:目前工作经验也只有3/4年,还是希望可以借助贵司的业务把技术搞扎实,提高自己的技术影响力,在公司内部做一些技术分享,帮助公司做一些架构选型、代码优化、性能调优的事情。后续公司如果有相应的平台给我向上发展,我也很愿意走架构师、项目管理的路线,不过刚进去的话还是尽全力做好手头的事情。
自我介绍
自我介绍
面试官您好,我叫张三,来自于湖南长沙,毕业于武汉大学, 目前从事Java后端开发有4年了
上一家任职的公司是上海有限公司,主要的职责是参与项目核心业务功能的开发、测试和部署,然后也参与过一些项目的技术方案选型 ,系统优化等等
然后我最近做的一个项目是哈奇马,它是一个一站式宠物服务平台,包含宠物商城,宠物服务,宠物社区,和活动任务中心等功能,分为客户端app,服务端app,微信用户小程序和后台管理端
项目是基于微服务框架搭建的, 框架用的是springcloud Alibaba及其相关组件 、数据库用的是mysql,redis, 持久层框架用的是mybatis mq,然后消息中间件是rabbitMq ,容器化平台docker这些,然后我负责的模块主要是运营服务管理,活动领券,门户,收藏 这几个模块
在开发这些模块的过程中我有主导一些技术方案设计 解决实际开发中的问题
比如说: 高并发环境下的优惠券的超卖超领问题,数据库和缓存一致性问题,分布式环境下的异步解耦,事务管理 和任务调度等等
以上就是我的一个基本情况,您看看有什么想要了解的。
(设计缓存方案去提升热点数据的访问效率 使用乐观锁悲观锁去解决优惠券的超卖超发问题,使用到工厂模式,策略模式去设计优惠券规则,以及用xxl-job在分布式环境下调度任务,也有使用MQ来做异步通信,做到流量削峰来优化性能等等)
项目的生命周期
需求分析:调研市场需求,定义平台的核心功能和业务逻辑。
可行性研究与规划:技术可行性评估,制定项目时间表和任务分配。
系统设计:包括系统架构设计和详细功能设计,设计数据库、API等。
开发与编码:前端和后端的开发,完成平台各个功能模块的实现。
测试:
功能测试、单元测试、集成测试
性能测试、压力测试、稳定性测试
部署与上线:配置生产环境,部署系统,并逐步上线。
运维与支持:系统的持续监控、问题修复和版本迭代。
项目收尾:总结项目经验,归档项目文档,完成验收
项目一 哈奇马
项目整体介绍(项目背景 业务 整体介绍)
业务介绍
核心业务:
线上宠物商城:销售品牌授权的宠物用品。
宠物医疗服务:提供24小时在线兽医支持与健康咨询。
线下宠物服务:包括洗澡、美容、驱虫、疫苗接种、医疗、寄养等 。
上门宠物服务: 包括日常护理, 遛狗, 洗澡美容 寄养 医疗等
宠物成长记录:记录体重、疫苗、驱虫等信息,并提供提醒功能。
养宠交友平台:用户可以分享宠物日常,进行交友与领养。
附加功能:
线下体验馆,配备专业美容师团队。
宠物鼻纹识别技术,用于健康分析与跟踪。
未来计划扩展至宠物医院和度假酒店,提供更加全面的医疗和休闲服务。
通过创新的技术和完善的服务,推动宠物行业发展和提升宠物生活质量
微服务划分
项目的微服务组成:
用户服务
商品服务
商品订单服务
服务订单服务
支付服务
社区服务
服务预约模块
活动中心服务
消息通知服务
优惠券服务
收藏服务
网关服务
认证授权服务
日志与监控服务
项目技术选型理由:
长远考虑与扩展性 --未来业务规模增长 避免系统重构
解决特定问题 --es的模糊查询全文检索 提升效率和用户体验
团队技术能力与创新性 --与行业接轨 保持技术前瞻性
01.运营服务管理模块
02.优惠券模块
数据库表设计
优惠券活动表:
优惠券活动管理模块主要操作优惠券活动表。
优惠券活动记录优惠券活动信息,运营人员新增优惠券活动将写入此表,此表是优惠券管理主要维护的表。
关键字段:活动id、活动名称、优惠券类型、折扣、发放时间等。
优惠券表:
抢券模块主要操作优惠券表。
优惠券表记录用户领取的优惠券,用户抢券存在限制,每种优惠券一个用户只允许领取一张,优惠券的总数有限制。
关键字段:用户id、活动id、折扣、优惠券类型、有效期等。
举例:一个优惠券活动发放100张优惠券最多有100个用户去领取,每人领取一张,每个用户领取的一张优惠券会记录在优惠券表中,即该优惠券活动对应优惠券表最多100条记录。
优惠券核销表:
在使用优惠券模块当用户成功使用一张优惠券会在优惠券核销表记录一条记录,记录是哪个用户的哪个订单使用了哪个优惠券。
关键字段 :用户id、优惠券id、订单id,核销时间。
优惠券退回表:
如果用户取消订单,则会退回优惠券,具体操作是向优惠券退回表添加一条记录(记录用户退回优惠券的信息),并向优惠券核销表删除一条对应的记录,表示取消优惠券的核销。
关键字段:用户id、优惠券id、退回时间。
业务流程
优惠券模块包括:活动管理、抢券、核销三个模块。
活动管理:
对优惠券活动进行管理,运营人员新增优惠券活动、修改优惠券活动、撤销优惠券活动及优惠券统计等。
抢券:
到了优惠券发放时间用户进行抢券,抢券过程对优惠券库存、对用户领取优惠券数量等进行校验,抢券成功记录用户领取优惠券的记录。
核销:
用户在下单时使用优惠券得到优惠金额,实付金额等于订单金额减去优惠金额,下单成功优惠券核销成功。
优惠券核销是指:顾客在购买商品使用优惠券,当此次消费符合优惠券的条件时提交订单后将优惠券的折扣应用到顾客的订单中,最后将优惠券标记为已使用或作废。
优惠券核销后还可以取消核销,如果用户取消订单会将优惠券取消核销即退回优惠券,退回优惠券后可以继续使用。
优惠券规则
逐字稿优惠券模块表述
逐字稿
面试官您好,我叫 21届大学计算机学院毕业, 目前从事Java后端开发有3年多了 上一家公司是在上海那边
在过去的几年工作中, 主要的职责是参与项目核心业务功能的开发、测试和部署,然后也参与过一些项目的技术方案选型 ,系统优化这些
然后我最近做的一个项目是一个一站式宠物服务平台,包含宠物商城,宠物服务,宠物社区,和活动任务中心等功能,分为客户端app,服务端app,微信用户小程序和后台管理端
项目是基于微服务框架搭建的, 用的是springcloud Alibaba及其相关组件 、数据库用的是mysql,redis, 持久层框架用的是mybatis mq,然后消息中间件是rabbitMq ,容器化平台docker这些,然后我负责的模块主要是运营服务管理,优惠券,门户,收藏 这几个模块 其中比较有技术难点的是优惠券模块
在开发这些模块的过程中 我有去主导一些技术方案设计来解决实际开发中的一些场景问题
比如说: 高并发环境下的缓存预热,超卖超领,数据库和缓存一致性问题,分布式环境下的异步解耦,事务管理 和任务调度等等
以上就是我的一个基本情况,您看看有什么想要了解的。
优惠券模块流程
先介绍一下优惠券模块的数据库表设计
优惠券模块主要涉及四张表 分别是
优惠券活动表 活动id、活动名称、优惠券类型、折扣、库存 发放时间
优惠券表 用户id、活动id、折扣、优惠券类型、有效期等
优惠券核销表 用户id、优惠券id、订单id,核销时间
优惠券退回表 用户id、优惠券id、退回时间
核销表:
记录使用情况:保存用户在订单中成功使用优惠券的记录,标记优惠券已被核销。
跟踪优惠券使用统计:用于分析优惠券的使用频率、用户行为及活动的有效性。
防止重复使用:确保同一张优惠券只能在一个订单中核销,防止重复使用。
退回表:
记录退回优惠券:在某些场景下(如订单取消或未完成),记录用户退回的优惠券,便于后续重新发放或处理。
数据分析:用于分析未成功使用的优惠券及退回原因,优化发放和活动策略。
优惠券模块的业务流程主要分为三个部分 ,分别是 活动管理 抢券 核销
活动管理模块主要是运营人员新增, 修改 和下架优惠券活动 对优惠券进行基本的管理 维护的表主要是优惠券活动表
抢券模块的话
首先是用户查看活动界面 因为在活动期间的用户访问量会比较大嘛 然后活动界面就会被比较高频的访问到
为了提高这个界面的查询性能 我就引入了redis作为缓存 将优惠券的活动信息存入redis中,避免直接访问数据库
具体实现方案就是通过xxl-job执行定时任务将热点活动信息预热到缓存redis中 这种方式也能预防缓存击穿这种问题
不过xxl-job设定的执行间隔是一分钟一次嘛 然后活动界面的正在进行中的活动信息就无法达到一个实时性
然后针对这个情况,我是让前端去根据活动开始时间进行一个倒计时,达到开始时间就将活动移到进行中的界面
然后请求后端接口,后端根据活动时间和当前时间判断得出实时的活动状态
其次是用户进行抢券 在一些优惠力度比较大的活动场景嘛 我们这个抢券场景的qps可以达到一个1000出头的样子
因为在高并发情况下抢券就是对优惠券库存这个共享资源进行操作嘛 然后就会出现线程不安全问题
就是我优惠券库存只有100但是可能会发出去超过一百的数量 就是一个超卖超发问题
针对这个问题我当时就想了几种方案
首先想到的就是乐观锁和悲观锁 悲观锁的话比如直接使用syn和reentranlock这种实现串行化 不过这种只能作用于
单个jvm 在分布式环境下肯定是失效的 乐观锁的话就是给优惠券的库存信息表添加一个version字段 每次更新库存时都
先去查询库存和版本号 然后更新时用where检查版本号 基于一个CAS的思想来实现乐观锁 不过这么做也有一个问题就是
在高并发的场景下这个version的比较可以会频繁失败重试 造成性能下降 然后又想到了数据行锁 直接在update语句的where
条件中判断库存大于0即可扣减库存 因为这个sql本身就会加行锁 所以会串行化执行更新语句 但是考虑后发现这种数据库行锁
方案依旧会大量的请求数据库 锁的开销也比较大 因此性能上也不理想所以没采用
然后就考虑到redis 因为redis是基于内存的嘛 性能会比较好 可以基于redis的setnx这些命令实现分布式锁,或者引入redisson框架
这样就只允许一个线程拿到锁去操作库存 从而保证线程安全
不过redisson框架还是相对有点重量级的一个框架 在 加上串行化操作的同时 底层会频繁的执行上锁和释放锁的命令
在考虑过后 最后还是找了一个相对轻量级且不用频繁上锁的解决方案 就是利用redis的decrement(DECR)这个命令去实现扣减库存
因为redis本身是单线程的嘛 然后DECR命令也是具有原子性的 不过虽然扣减这个命令是原子性的 扣减库存的逻辑是分几个步骤的
需要先查询库存 然后判断库存是否执行扣减 (记录抢券成功队列 记录抢券同步队列)等 因此核心还是需要保证扣减库存这一整个过程的原子性 因此就是需要保证多个redis命令的原子性
然后去了解了一些对应方案后最后选择了lua脚本来实现 因为lua可以保证强原子性而且性能比较好
除了超发问题 还会出现用户对同一张券进行重复领取和重复消费问题 这两个场景都属于经典的幂等场景了
针对用户重复领取 处理方案就是基于上述的lua脚本 在redis扣减库存成功后 用hash记录用户抢券成功队列 大key是优惠券id
小key是用户id value是1 如果判断用户已经抢过该券 则直接返回错误
针对用户重复消费 处理方案是数据库乐观锁, 给优惠券记录表添加一个version字段 默认值为0表示未使用 用户在请求使用优惠券时通过where判断version是否为0 来决定sql更新操作的成功与否
最后是数据一致性问题: 用户抢券成功扣减redis库存后 还需要将redis的数据同步到mysql 因此在前面的lua脚本扣减库存操作后 也创建了一个抢券同步队列 大key活动id_id%10 小key是用户id value是活动id
因此同步数据就是将这个抢券成功队列的数据同步到mysql 当时考虑到性能问题也想了两种方案
一种是利用多线程异步处理 不过因为引入了多线程还是会出现重复消费问题 所以还需要引入redisson实现一个分布式锁
另一种是使用rabbitmq异步处理消息去更新mysql 基于架构复杂度的考虑还是选择了使用mq
具体步骤是使用xxl-job执行定时任务 批量的读取redis的数据 将记录封装成消息后发送到消息队列 然后消费者再异步同步到mysql
考虑到网络延迟等问题可能导致mq消息的重复消费导致幂等问题, 对数据库的user_id和coupon_id创建联合唯一索引
然后通过mq的消息持久化 发布者确认机制 消费者确认机制 已经一个死信队列 来保证消息不丢失
从而实现redis到mysql的一个数据同步一致性问题
最后是核销阶段 就是订单服务远程调用优惠券服务进行核销 如果下单核销成功会给优惠券核销表添加记录
如果取消订单会退回优惠券 然后下单和核销优惠券设计到两个服务 因此这里有一个分布式事务问题 我们项目是通过seata的at模式解决的 at的核心是通过一个undo_log文件 在tc的管理下 一阶段提交事务 二阶段根据事务的情况决定是否回滚

技术难点场景题
活动查询使用缓存提高性能---缓存一致性问题
场景: 抢券界面面向c端且用户在活动期间请求并发较高----提高查询性能
解决方案: 将优惠券活动信息存入redis缓存 避免直接访问数据库----数据一致性问题
redis数据结构: String类型 (永不过期)
key: "ACTIVITY:LIST"
value: 符合条件的优惠券活动列表JSON数据。
解决方案: 通过定时预热程序保证缓存一致性,抢券页面列出的活动信息属于热点信息(一个月以内的活动信息),对于热点信息通过定时预热防止缓存击穿,定时预热程序通过定时任务定时执行,定时将活动信息存入Redis。
数据流:
场景: 如何保证活动状态实时自动改变(在活动进行中页面显示可抢优惠券)
解决方案:
前端请求后端接口查询活动信息,后端根据活动时间(开始、结束时间)及当前时间得到活动当前准确的状态,并将状态返回给前端。
前端进行控制,根据活动开始时间进行倒计时,达到开始时间将活动移到进行中界面。
解决多发问题
QPS: 20w+用户量 1-2万日活 活动参与率10-20% QPS 500-1000多
场景: 在并发抢券的场景下 对库存这个共享资源进行操作存在线程不安全---导致库存1000最终卖出1000+的场景
解决方案: 首先想到锁---悲观锁 乐观锁
悲观锁: synchronized和ReentrantLock 串行化 单个jvm 微服务多实例情况没法用
乐观锁: CAS即Compare And Swap --- 优惠券的库存信息表添加字段-version--冲突率高--频繁重试--性能下降
其次是数据库行锁控制方案
悲观锁--select...for update--查询时就上锁,高并发下数据压力大--适合银行转账等需要严格一致 并发不高
乐观锁方案:---高并发情况请求数据压力大
数据库的行级锁也可以实现乐观锁,通用的做法是在表中添加一个version版本字段,在更新时对比版本号,更新成功将版本号加1,SQL为(这个会导致还有库存,但是同时只能有一个人抢成功):
针对扣减库存业务扣减库存SQL(这里的库存都是数据库实时数据,不是用户传递的,所以不会超卖):
多线程执行上边的SQL,假如线程1先执行会添加排他锁,当事务没有结束前其它线程去更新同一条记录会被阻塞,等到线程1更新结束其它线程才可以更新库存。
等值的version比较失败率比较高
非等值比较的,依然存在大量请求到数据库,可能短时间内消耗掉所有连接数,导致CPU陡增
redis分布式锁方案--------JVM中的线程去争抢同一个分布式锁,在扣减库存前先获取分布式锁,拿到锁再扣减库存,执行完释放锁之后其它JVM的线程才可以获取锁继续扣减库存 --setnx命令和lua保证原子性
-----------锁商品id,能解决超卖,但是让并行变串行,会导致有库存但是不能同时买,所以放弃
最终方案:redis的DECR命令具有原子性 ---- 利用redis+lua实现并发分布式方案控制
扣减库存逻辑如下:
1、首先查询库存
2、判断库存大小,如果大于0则扣减库存,否则 直接返回
3、记录抢券成功的记录----用于判断用户不能重复抢券的依据。
4、记录抢券同步的记录----异步处理-----抢券结果保存到数据库
多个命令的原子性方案:
MULTI 事务命令
Pipeline管道命令
Redis+Lua实现
Lua脚本在redis集群上执行需要注意什么?
在redis集群下执行redis命令会根据key求哈希,确定具体的槽位(slot),然后将命令路由到负责该槽位的 Redis 节点上。
执行一次Lua脚本会涉及到多个key,在redis集群下执行lua脚本要求多个key必须最终落到同一个节点,否则调用Lua脚本会报错:ERR eval/evalsha command keys must be in same slot。
解决方法:一次执行Lua脚本的所有key中使用大括号‘{}’且保证大括号中的内容相同
解决多领问题
抢券程序请求Redis扣减库存,扣减库存成功记录到抢券成功队列
抢券成功队列:为了校验用户是否抢过该优惠券
抢券成功队列: Hash --- RedisKey:COUPON:SEIZE:LIST:活动id{活动id%10}
HashKey:用户id----HashValue:1 (记录该券被哪些用户抢到 用于判断用户超领)
解决优惠券重复使用问题
场景问题: 用户在多个设备同时使用同一张优惠券
解决方案: 使用数据库乐观锁解决
实现步骤:
为优惠券记录表添加一个 version 字段,默认值为 0(表示未使用)。
当用户发起使用优惠券的请求时,先查询优惠券当前的 version,并在更新时通过 WHERE 条件限制 version 必须为 0。
如果某个请求成功修改了 version 字段(比如将 0 更新为 1),则表示该优惠券已经被使用。其他并发请求在 version != 0 时无法成功更新,因此也无法重复使用。
SQL 示例:
如果 SQL 返回的更新行数为 1,说明操作成功;如果为 0,说明优惠券已经被使用,提示用户优惠券不可用。
抢券整体方案流程
说明如下:
1、由预热程序将待生效库存同步到redis(活动开始将不允许更改库存)
2、活动开始后,抢券程序请求Redis扣减库存,扣减库存成功向抢券成功队列和抢券同步队列写入记录
Redis中两个队列的作用如下:
抢券成功队列:为了校验用户是否抢过该优惠券。
抢券同步队列:将抢券结果同步到数据库
3、通过定时任务程序根据Redis中同步队列记录的用户抢券结果信息将数据同步到MySQL,具体操作如下:
向优惠券表插入用户抢券记录。
更新优惠券活动表的库存。
写入数据库完成后删除Redis中同步队列的相应记录,删除后表示同步完成,如果同步过程失败将保留Redis同步队列的相应记录。
数据流
redis数据结构
活动信息: String--key: "ACTIVITY:LIST"---value: 符合条件的优惠券活动列表JSON数据。
优惠券活动库存: Hash --- RedisKey:COUPON:RESOURCE:STOCK:{活动id%10}(同活动id分配到同节点)
HashKey:活动id----HashValue: 库存
抢券成功队列: Hash --- RedisKey:COUPON:SEIZE:LIST:活动id
{活动id%10}
HashKey:用户id----HashValue:1 (记录该券被哪些用户抢到 用于判断用户超领)
抢券同步队列: Hash---RedisKey:QUEUE:COUPON:SEIZE:SYNC:{活动id%10}
HashKey:用户id----HashValue:活动id,即哪些人抢到了哪些券,后续同步DB
抢券的Lua脚本做的什么工作?
判断用户是否在该活动抢过券。
判断库存是否充足
写入抢券成功列表
扣减库存
写入抢券同步列表
抢券同步redis--mysql
解决方案1(架构太复杂了):
使用线程池 每个线程处理一个同步队列
由定时任务去调度,每隔1分钟由多线程对同步队列中的数据进行处理
问题: 如何从Redis 批量取数据?
方案: 使用redisTemplate.opsForHash().scan(H key, ScanOptions options)方法,scan方法通过游标的方式实现从hash中批量获取数据
问题:多线程在执行任务时,如果多个线程从同一个Redis Hash中获取数据就会出现重复处理数据的问题
方案:Redission(原生的Redis加锁存在:锁超时、锁误删、业务续期等问题)
锁误删锁超时问题
redisson基本使用
// 创建Redisson客户端
RedissonClient redissonClient = Redisson.create();
// 获取名为myLock的分布式锁实例,通过此实例进行加锁、解锁
RLock lock = redissonClient.getLock("myLock");
try {
// 尝试获取锁,最多等待3秒,持锁时间为5秒
boolean isLockAcquired = lock.tryLock(3, 5, TimeUnit.SECONDS);
if (isLockAcquired) {
// 获取锁成功,执行业务逻辑
} else {
// 获取锁失败,处理相应逻辑
}
} catch (InterruptedException e) {
// 处理中断异常
} finally {
// 释放锁
lock.unlock();
}
redisson方法参数说明
Redisson看门狗自动锁续期
当设置了leaseTime的时间为10秒,结果任务执行了20秒,当到达到10秒即使任务还没有结束锁将自动释放,
导致多个线程共同去执行任务,可能在并发处理上存在问题。
当执行任务的时间可以控制在一个范围就可以指定leaseTime锁自动释放时间,
如果执行任务的时间不容易通过leaseTime去设置,使用Redisson的看门狗机制
Redisson的"看门狗机制"(Watchdog)是一种用于监测和维护锁的超时时间的机制,它可以确保在任务没有完成时对锁的过期时间进行自动续期
开启看门狗后针对当前锁创建一个线程执行延迟任务,默认每隔10秒将锁的过期时间重新续期为30秒。
看门狗线程会首先判断锁是否存在,如果不存在将不再续期,当程序执行unlock()方法释放锁时会将该锁的对应的延迟任务取消,此时看门狗线程结束任务。
注意:任务结束一定要执行unlock()方法释放锁,否则看门狗线程一直进行续期,导致锁无法释放
数据同步组件---xxl-job--线程池
第一步我们定义处理器:
同步组件会自动从上图的Hash结构中读取数据,处理器负责接收到数据后写入数据库。
第二步启动同步任务:
调用组件syncManager接口的start方法即启动
解决方案2:
rabbitMq异步同步:
当 Redis 扣减库存并记录抢券信息成功后,将该抢券记录封装成消息并发送到消息队列 (定时任务 批量读取)
消费者异步同步到 MySQL
幂等性设计: 防止重复消费---user_id 和 coupon_id 创建一个联合唯一索引
如果一张券可以抢多次--value改为json类型存储多个字段(添加唯一记录id)
每次抢券成功后,生成一个唯一的 record_id(如 UUID),将其与 coupon_id、抢券时间等一起存储在 Redis 的 Hash 结构中。可以把 record_id、抢券时间 和 次数 信息打包到 value 中。
原子性: lua脚本--- 确保 Redis 中的扣减和消息队列发送操作的一致性
用户核销优惠券
用户核销需求分析
可用优惠券列表数据分析
可用优惠券列表根据以下条件对用户的优惠券进行筛选:
属于当前用户的优惠券
符合下边条件的优惠券:
订单金额大于等于满减金额
优惠金额小于等于订单金额
优惠券还没有过期
优惠券还没有使用
可用优惠券列表的信息包括:
活动名称
优惠券类型
满减金额
折扣率
针对该订单的优惠金额(需要根据订单金额和优惠券去计算得出,前端需要此数据)
优惠券过期时间(已过期的优惠券无法使用)
在可用优惠券列表中前端需要拿到优惠金额,从而计算出订单实付金额。
订单的实付金额=订单金额-优惠金额。
根据订单金额和优惠券的优惠金额可以计算优惠金额:
针对满减:优惠券的优惠金额
针对折扣:订单金额乘以(1-折扣率)
核销优惠券
优惠券核销是指:顾客在购买商品时使用优惠券,当此次订单的消费符合优惠券的条件时在下单会使用该优惠券在原有订单金额基础上进行优惠,优惠券使用后标记为“已使用”。
优惠券核销后还可取消核销,如果用户取消订单会将优惠券的状态改为“未使用” 退回的优惠券可继续使用
优惠券核销具体实现
优惠券核销
用户端请求订单管理服务创建订单信息,订单管理服务远程调用优惠券服务核销优惠券,下单成功且优惠券核销成功。
优惠券核销执行以下操作:
限制:订单金额大于等于满减金额。
限制:优惠券有效
根据优惠券id标记优惠券表(coupon)中该优惠券已使用、使用时间、订单id等。
向优惠券核销表(coupon_write_off)添加记录。
核销成功返回最终优惠的金额。
优惠券退回
用户端取消订单,订单管理服务执行取消订单逻辑,如果该订单使用了优惠券则请求优惠券服务退回优惠券。
优惠券退回执行以下操作:
添加优惠券退回记录(coupon_use_back)。
更新优惠券
如果优惠券已过期则标记该优惠券已作废
如果优惠券未过期,标记该优惠券未使用,清空订单id字段及使用时间字段。
删除核销记录。
分布式事务问题
spring本地事务是基于数据事务的
优惠券核销事务控制---使用seata AT模式
启动seata 的事务协调器
配置seata环境(创建undo_log表)
开启全局事务-@GlobalTransactional
处理锁失效和事务失效问题
兑换码生成流程
兑换码
兑换码兑换成优惠卷的流程差不多是相同的,只是不用判断库存超卖,因为在生成兑换码的时候就生成了相对应的库存数量的兑换码。
这里的兑换码是否被兑换是通过bitmap的数据结构来实现的。
图片加载失败
使用设计模式-策略模式定义优惠券规则
策略模式
在工厂模式中,我们可以定义一个抽象的工厂类,该类负责根据不同的代金券策略类型创建相应的策略对象。这样,我们可以将不同的策略对象的创建逻辑封装在各自的具体工厂类中,避免了代码的重复。
策略模式则定义了一个抽象策略接口,所有的具体策略类都要实现该接口。该接口中声明了购买商品时使用代金券的方法,具体的策略类可以根据不同的代金券策略类型实现自己的逻辑。
在使用过程中,客户端通过工厂类创建出对应的策略对象,然后调用策略对象的方法来完成购买商品时的代金券使用。这样,客户端的代码就变得简洁和可维护,而且可以根据需要轻松地添加新的代金券策略类型。
总结起来,工厂模式和策略模式的结合可以通过抽象工厂类和策略接口来实现不同代金券策略的动态创建和调用,从而减少了代码的重复性,提高了系统的可扩展性和可维护性。
线程池问题
问:你在项目中哪些地方用到过线程池?
消息队列同步消息到数据库的过程中 使用线程池加速消息的处理速度
问:你的线程池参数是怎么设置的?
答:线程池的常见参数包括:核心线程、最大线程、队列、线程名称、拒绝策略等。
这里核心线程数我们配置的是2,最大线程数是CPU核数。之所以这么配置是因为发放优惠券并不是高频业务,这里基于线程池做异步处理仅仅是为了减少业务耗时,提高用户体验。所以线程数无需特别高。
队列的大小设置的是200,而拒绝策略采用的是交给调用线程处理的方式。
由于业务访问频率较低,所以基本不会出现线程耗尽的情况,如果真的出现了,就交给调用线程处理,让客户稍微等待一下也行。
Spring事务失效和实际失效场景(未整理)
问:Spring事务失效的情况碰到过吗?或者知不知道哪些情况会导致事务失效?
答:Spring事务失效的原因有很多,比如说:

  • 事务方法不是public的
  • 非事务方法调用事务方法
  • 事务方法的异常被捕获了
  • 事务方法抛出异常类型不对
  • 事务传播行为使用错误
  • Bean没有被Spring管理
    在我们项目中确实有碰到过,我想一想啊。
    我记得是在优惠券业务中,一开始我们的优惠券只有一种领取方式,就是发放后展示在页面,让用户手动领取。领取的过程中有各种校验。那时候没碰到什么问题,项目也都正常运行。
    后来产品提出了新的需求,要加一个兑换码兑换优惠券的功能。这个功能开发完以后就发现有时候会出现优惠券发放数量跟实际数量对不上的情况,就是实际发放的券总是比设定的要少。一开始一直找不到原因。
    后来发现是某些情况下,在领取失败的时候,扣减的优惠券库存没有回滚导致的,也就是事务没有生效。自习排查后发现,原来是在实现兑换码兑换优惠券的时候,由于很多业务逻辑跟手动领取优惠券很像,所以就把其中的一些数据库操作抽取为一个公共方法,然后在两个业务中都调用。因为所有数据库操作都在这个共享的方法中嘛,所以就把事务注解放到了抽取的方法上。当时没有注意,这恰好就是在非事务方法中调用了事务方法,导致了事务失效。
    优惠券的开发难点问题(未整理)
    问:在开发中碰到过什么疑难问题,最后是怎么解决的?
    优惠券难点(仅供参考)
    锁实现问题(未整理)
    面试官:那你这里聊到悲观锁,是用什么来实现的呢?
    由于在我们项目中,优惠券服务是多实例部署形成的负载均衡集群。因此考虑到分布式下JVM锁失效问题,我们采用了基于Redisson的分布式锁。
    (此处面试官可能会追问怎么实现的呢?如果没有追问就自己往下说,不要停)
    不过Redisson分布式锁的加锁和释放锁逻辑对业务侵入比较多,因此就对其做了二次封装(强调是自己做的),利用自定义注解AOP,以及SPEL表达式实现了基于注解的分布式锁。(面试官可能会问SPEL用来做什么,没问的话就自己说)
    我在封装的时候用了工厂模式来选择不同的锁类型,利用了策略模式来选择锁失败重试策略,利用SPEL表达式来实现动态锁名称。
    (面试官可能追问锁失败重试的具体策略,没有就自己往下说)
    因为获取锁可能会失败嘛,失败后可以重试,也可以不重试。如果重试结束可以直接报错,也可以快速结束。综合来说可能包含5种不同失败重试策略。例如:失败后直接结束、失败后直接抛异常、失败后重试一段时间然后结束、失败后重试一段时间然后抛异常、失败后一直重试。
    (面试官如果追问Redisson原理,可以参考黑马的Redis视频中对于Redisson的讲解)
    性能问题(未整理)
    面试官:加锁以后性能会比较差,有什么好的办法吗?
    答:解决性能问题的办法有很多,针对领券问题,我们可以采用MQ来做异步领券,起到一个流量削峰和整型的作用,降低数据库压力。
    具体来说,我们可以将优惠券的关键信息缓存到Redis中,用户请求进入后先读取Redis缓存,做好优惠券库存、领取数量的校验,如果校验不通过直接返回失败结果。如果校验通过则通过MQ发送消息,异步去写数据库,然后告诉用户领取成功即可。
    当然,前面说的这种办法也存在一个问题,就是可能需要多次与Redis交互。因此还有一种思路就是利用Redis的LUA脚本来编写校验逻辑来代替java编写的校验逻辑。这样就只需要向Redis发一次请求即可完成校验。
    优惠券规则(未整理)
    使用优惠券的订单可能包含多个商品,如果出现部分商品退款的情况,你们如何处理退款金额?优惠券是如何处理的?
    你在项目中有没有使用到线程池或者并发编程?
    那你能不能聊一聊CountdownLatch的基本原理?
    兑换码算法
        **优惠券推广类型是指定发放的时候,就是用户使用兑换码来兑换这个场景,我们就需要发放的同时,来生成这个兑换码**
        **兑换码至少满足几个特征:**
    

    1.可读性好:兑换码是要给用户使用的,用户需要输入兑换码,因此可读性必须好,所以我们兑换码的长度不会超过10个字符,只能是24个大写字母和8个数字组成。
    2.数据量大:优惠活动比较频繁,必须有充足的兑换码
    3.唯一性:10亿兑换码都必须唯一,不能重复,否则会出现兑换混乱的情况
    4.不可重兑:兑换码必须便于校验兑换状态,避免重复兑换
    5.防止爆刷:兑换码的规律性不能很明显,不能轻易被人猜测到其它兑换码
    6.高效:兑换码生成、验证的算法必须保证效率,避免对数据库带来较大的压力
    有考虑过UUID,SnowFlake算法,自增ID,我们选择了Base32转码,Base32更适合的原因是它生成的标识符比较简洁,方便用户输入和识别。而且Base32也有足够的唯一性,所以我们最终使用Base32。

    3.2.重兑校验算法

    两种方案:
    1.基于数据库:我们在设计数据库时有一个字段就是标示兑换码状态,每次兑换时可以到数据库查询状态,避免重兑,这样优点是实现起来简单,缺点就是对数据库压力大,并且高并发场景下容易不准。
    2.基于BitMap:兑换或没兑换就是两个状态,对应0和1,而兑换码使用的是自增id.我们如果每一个自增id对应一个bit位,用每一个bit位的状态表示兑换状态,而且这种算法刚好就是BitMap的底层实现,而且Redis里边的BitMap刚好能支持2的32次方个bit位,这样更加高效,性能好。

    3.3.防爆刷校验算法

        **防爆刷是一个跟业务高度耦合的算法场景,目前还没有成熟框架可以直接使用,需要我们自己来设计这套算法了。但是有很多类似的算法机制是可以借鉴的,就比如熟悉的JWT。JWT的请求报文主要包含:Header:记录算法,Payload:记录用户信息,Verify Signature:验签,用于验证整个token。**
        **Header和Payload采用的是Base64算法,与我们Base32类似,几乎算是明文传输,它通过秘钥对Header、PayLoad进行加密得到密文,服务端拿到上述3部分数据之后,对Header、PayLoad也进行一次加密(客户端-服务端密文各自保存,并且严格保持一致)。如果服务端计算的跟前端给过来的一致就允许请求,不一致则说明有人修改的请求信息,直接拦截。**
       **比如我们先准备一个密钥,然后利用密钥对自增id做加密,生成签名,把签名、自增id利用Base32转码后生成兑换码,只要秘钥不泄露,就没有人能伪造兑换码。只要兑换码被篡改,就会导致验签不通过。**
    

    03.门户模块
    缓存技术方案
    前端: 静态资源CDN加速:将门户界面的静态资源通过CDN( 腾讯云COS )加速分发。
    定位界面上的已开通区域列表
    服务搜索/商品搜索
    首页服务列表/商品推荐
    热门服务列表/热门商品
    服务信息/商品信息
    Spring Cache
    Redis访问工具 Spring data redis框架,通过RedisTemplate访问Redis
    Spring Cache最终也是通过Lettuce 去访问redis
    缓存框架,基于AOP原理,实现了基于注解的缓存功能
    常用注解
    使用分布式调度平台XXL-JOB
    调度中心:
    负责管理调度信息,按照调度配置发出调度请求,自身不承担业务代码;
    主要职责为执行器管理、任务管理、监控运维、日志管理等
    任务执行器:
    负责接收调度请求并执行任务逻辑;
    主要职责是执行任务、执行结果上报、日志服务等
    XXL-JOB调度中心可以配置路由策略,比如:第一个、轮询策略、分片等,它们分别表示的意义如下:
    第一个:即每次执行任务都由第一个执行器去执行。
    轮询:即执行器轮番执行。
    分片:每次执行任务广播给每个执行器让他们同时执行任务。
    如果根据需求每次执行任务仅由一个执行器去执行任务可以设置路由策略:第一个、轮询。
    如果根据需求每次执行任务由多个执行器同时执行可以设置路由策略为:分片。
    技术选型和实现思路 (gpt版)
    在我负责的宠物一站式平台微信小程序端门户首页模块中,主要展示了宠物服务列表、热门服务、热门商品以及推荐商品等内容。这是一个流量较大的页面,用户打开小程序的第一屏,因此如何保障首页数据加载的性能和实时性是这个模块的核心挑战。
    项目背景:
    该宠物平台致力于为宠物主人提供全方位的宠物服务,包括宠物用品商城和上门服务。门户首页需要展示实时更新的热门服务和商品,同时根据用户的浏览行为推荐相关的商品。
    技术选型和实现思路:
    微服务架构:
    平台采用微服务架构,商城服务和宠物服务分别由不同的微服务来处理,门户首页模块通过 API 聚合多个微服务的数据。各个微服务基于 Spring Cloud 进行开发,实现了服务的独立扩展和部署。
    缓存机制 - 使用 Spring Cache + Redis:
    性能优化:由于门户首页的热门服务和商品是高频访问的内容,出于性能和响应速度的考虑,我使用了 Spring Cache 与 Redis 结合的方式对这些数据进行缓存。每次用户访问门户首页时,首先从 Redis 缓存中读取服务和商品数据,大大减少了对数据库的直接访问,优化了页面加载时间。
    缓存的合理使用:我们针对不同类型的数据设计了细粒度的缓存键。例如,热门商品的缓存键为 hotProducts::page=1&size=10,而宠物服务的缓存键为 hotPetServices::location=city1。通过这种方式,能够根据分页和地区等条件动态地缓存不同的结果,提升了缓存的命中率。
    XXL-JOB 定时任务 - 更新缓存:
    缓存更新策略:为了保证门户首页的热门服务、商品和推荐商品的实时性和准确性,我利用了 XXL-JOB 定时任务框架,在每天凌晨执行任务,重新计算和刷新 Redis 中的缓存数据。这样可以确保热门服务和商品数据及时更新,同时避免频繁地从数据库中获取数据。
    XXL-JOB 的选型优势:XXL-JOB 作为轻量级、分布式的任务调度框架,具备高可用性和易扩展的特点,能够确保定时任务的稳定执行。而且可以方便地管理调度任务、查看执行日志以及配置调度策略。
    推荐商品的动态更新:
    基于用户行为的推荐:推荐商品是基于用户的浏览历史、购买行为等数据动态生成的。为了平衡实时推荐与性能之间的关系,我设计了缓存预热策略。部分推荐数据每天凌晨更新,但对于一些重要用户交互的推荐商品,仍然可以根据用户的操作即刻刷新缓存,保证用户能够看到与其兴趣相关的最新商品。
    合理的数据模型设计:在处理推荐商品时,数据模型上划分了不同的商品类别和标签,并结合数据分析来推荐最受欢迎的商品,提高了用户点击和购买转化率。
    异步与并发处理:
    为了进一步优化门户首页的响应速度,我对多项数据请求进行异步处理。比如,用户请求首页时,热门服务、热门商品和推荐商品的数据是通过异步线程池并发获取,避免了串行阻塞问题,缩短了页面加载时间。
    监控和报警机制:
    通过 Redis 自带的监控工具以及 XXL-JOB 提供的日志功能,定时监控 Redis 缓存的命中率和定时任务的执行情况。针对异常情况(如缓存失效、定时任务未成功执行),我还设置了报警机制,通过邮件或短信通知相关运维人员,确保问题能够及时响应和处理。
    结果与收益:
    通过 Spring Cache + Redis 结合定时任务的方案,实现了门户首页数据的高效缓存与更新,大幅降低了数据库的负载,首页的响应时间提升了约 50%,并且通过缓存的动态更新策略,确保了展示给用户的数据是及时且准确的。在流量高峰期,系统的稳定性也得到了显著提升。此外,XXL-JOB 的使用简化了定时任务的管理,使得系统在任务调度和运维管理方面更加高效。
    点赞模块(copy待完善)
    set、zset保存点赞信息
    那你们Redis中具体使用了哪种数据结构呢?
    zset作用
    那你ZSET干什么用的?
    使用zset优缺点
    那为什么一定要用ZSET结构,把更新过的业务扔到一个List中不行吗?
    04.收藏模块
    我的收藏模块
    具体实现步骤:
    数据结构设计:
    Redis 数据结构:
    Hash 结构:用于存储单个用户的所有收藏信息。
    Key:user:{userId}:collections
    Field:{itemId}_{type}
    Value:收藏时间戳或收藏状态(0 表示已取消,1 表示已收藏)
    Set 结构:用于快速判断用户是否收藏某个商品。
    Key:user:{userId}:collectionset
    Member:{itemId}
    {type}
    过期时间:可以设置过期时间或使用定期任务清理无效数据。
    数据库表设计:
    user_collections 表:
    id:主键
    user_id:用户 ID
    item_id:收藏对象 ID
    type:收藏类型(商品、服务等)
    status:收藏状态(0:已取消,1:已收藏)
    created_at:创建时间
    updated_at:更新时间
    收藏与取消收藏逻辑:
    收藏操作:
    用户发起收藏请求时,首先写入 Redis 缓存。
    将该操作通过消息队列发送至数据同步服务。
    取消收藏操作:
    从 Redis 中删除相应的收藏数据。
    同样将该操作通过消息队列发送至数据同步服务。
    数据同步服务:
    从消息队列中获取数据变更信息,更新数据库中的收藏数据。
    定期任务同步:
    定时任务(如 XXL-JOB)每隔一定时间(如每 5 分钟)扫描 Redis 中的数据,将变更同步到数据库。
    任务执行流程:
    从 Redis 中获取所有用户的收藏数据。
    与数据库中数据进行比对,找出新增或删除的收藏数据。
    执行批量插入或更新操作,确保数据库与 Redis 数据一致。
    清理 Redis 中已经同步的数据。
    数据一致性保障:
    事务支持:在将缓存数据同步到数据库时,可以使用数据库事务,确保数据同步的原子性。
    重试机制:在数据同步失败时,消息队列可以提供重试机制,确保数据最终成功写入数据库。
    缓存失效策略:在数据写入数据库后,可以设置 Redis 中相关数据的失效时间,避免缓存与数据库不一致的情况。
    接口设计:
    收藏接口:
    POST /api/collect:添加收藏,参数为 user_id、item_id、type。
    取消收藏接口:
    DELETE /api/collect:取消收藏,参数为 user_id、item_id、type。
    查询收藏列表接口:
    GET /api/collect:查询用户收藏列表,支持分页、排序。
    异常处理与监控:
    异常处理:
    对 Redis 操作失败的情况(如 Redis 不可用)进行处理,直接将数据写入数据库。
    消息队列失败时,进行告警并重试。
    监控与告警:
    实时监控 Redis 与数据库的数据一致性情况。
    对 Redis 同步失败、消息队列堆积等情况进行告警。
    项目二 小新到家| 家政保洁上门服务平台
    项目三 嘉祥县人民医院智慧医疗
    项目四 美达智慧养老
    暂存问题待整理
    常见场景题
    00.项目中遇到过什么难点么 怎么解决的
    在项目中,遇到的一个印象深刻的 bug 是订单同步时,两个系统的订单状态经常不一致。系统 A 负责订单创建,系统 B 负责订单处理,但由于它们各自独立的事务管理,网络延迟或服务不可用时,可能导致订单状态不同步,系统 A 和 B 的订单状态不一致。
    详细解决方案:
    问题排查:
    日志分析:我们首先通过日志发现,订单从系统 A 创建后,系统 B 没有同步成功,订单状态会在系统 A 和 B 中显示不同。通过进一步检查,我们发现这是由于分布式事务处理中的部分提交未成功,系统 B 的操作超时,导致系统 A 认为订单处理完成,而系统 B 仍然未更新状态。
    数据不一致的根因:这是典型的分布式系统中数据一致性问题,由于没有有效的分布式事务管理,某些服务在处理订单时成功,某些服务却失败,导致部分数据提交不成功。
    补偿机制的引入:
    为了解决这个问题,我们引入了可靠消息最终一致性的模式。每当订单状态更新时,我们将订单变更信息发送到消息队列(如 RabbitMQ)。系统 B 处理消息队列中的订单信息,并更新其本地状态。
    消息队列的好处:使用消息队列确保了服务之间的松耦合,即使系统 B 在某一时刻不可用,也可以在恢复后处理未完成的订单更新任务。我们还为消息队列配置了重试策略,当处理失败时,系统会自动进行重试,直到订单同步成功。
    事务监控与重试:
    我们还设置了一个定时任务,对同步失败的订单定期检查,确保漏掉的订单可以被重新处理。如果某个订单长时间没有成功同步状态,系统会触发警报,并且可以进行手动干预。
    监控系统:通过引入Prometheus + Grafana 的监控方案,定期分析订单状态同步的成功率和错误率。当错误率超过阈值时,系统会发出预警信号,及时定位和修复问题。
    效果:
    通过这种补偿机制和监控重试机制,最终确保了分布式系统的数据一致性。状态不一致的问题得到了解决,系统稳定性提高,并且订单同步的准确率大大提升。系统的可维护性也增强了,减少了因状态不同步引发的手动操作。
    01.项目中遇到过什么印象深刻的故障或者bug么 怎么解决的
    大表加字段问题
    故障案例:订单大表加字段导致锁表问题及解决方案
    背景:
    在订单系统中,我们有一张包含数亿条记录的订单大表。业务需求要求我们在该表中添加一个新的字段。然而,开发团队直接使用了传统的 DDL 语句(ALTER TABLE ADD COLUMN),未考虑 Online DDL 的方式。
    故障经过:
    在执行 DDL 变更的最后阶段,数据库自动为表添加了 Metadata Lock(元数据锁)。这一锁阻塞了所有试图访问该表的查询和更新操作,导致相关 SQL 请求进入长时间等待,最终引发了 SQL 慢查询。随着等待时间的积累,应用线程池迅速被打满,系统的响应速度大幅下降,部分服务甚至出现超时或不可用的情况。
    问题分析:
    传统的 DDL 操作会对表加全表锁,特别是在大表中执行变更时,容易引发锁等待,影响到大量 SQL 操作。
    Metadata Lock 是不可避免的,当锁时间过长,会引发连锁反应,导致 SQL 堆积。
    没有采用 Online DDL 是导致问题的主要原因,数据库的老版本也不支持在线 DDL。
    解决方案:
    暂停操作:在故障发生后,紧急中止了 DDL 操作,手动释放了 Metadata Lock,使得应用恢复正常。
    使用 Online DDL:在升级后的数据库中,通过 Online DDL 重新执行字段添加操作,避免了全表锁,保证了在变更期间表仍然可读写。
    变更策略优化:之后在所有大表变更中,严格执行在线 DDL 策略,并在业务低峰期进行变更,确保系统的可用性和性能
    SQL 触发全表扫描导致数据库 CPU 飙升及解决方案
    故障案例:SQL 触发全表扫描导致数据库 CPU 飙升及解决方案
    背景:
    在某次凌晨系统中,接到数据库 CPU 占用率 100% 的报警。排查发现某条 SQL 查询语句(where a= and b= and c= and d= order by id desc limit 20)由于缺少对应的索引,导致全表扫描,并通过主键索引走了全表扫描。该表只有 idx_a_b_e 的联合索引,无法高效地满足查询条件。
    故障分析:
    SQL 查询缺少合适的联合索引,导致全表扫描。
    在高并发环境下,频繁的全表扫描严重占用 CPU 资源。
    解决过程:
    限流 SQL:首先在数据库运维平台上对这类 SQL 进行了无差别限流,暂时缓解了数据库压力。
    删除无效数据:通过物理删除一些历史无效数据,减小了表的数据量,略微提升了性能。
    增加索引:最终,通过临时创建一个 idx_a_b_c_d 的全字段覆盖索引来优化查询,避免全表扫描,彻底解决了问题。
    结果:
    通过增加覆盖索引,SQL 查询性能大幅提升,数据库 CPU 立即恢复正常,系统性能得到了稳定。
    02.线程池用到过么? 具体场景是怎么样?
    03.设计模式用到过么?具体场景?为啥用?优点是啥?
    设计模式的核心原则:
    单一职责原则(SRP):每个类只负责一个职责。
    开闭原则(OCP):对扩展开放,对修改关闭。
    里氏替换原则(LSP):子类应能替代父类。
    依赖倒置原则(DIP):依赖于抽象而非具体实现。
    接口隔离原则(ISP):使用多个小接口,而不是一个大接口。
    其他设计原则:
    合成复用原则:优先使用组合而非继承。
    迪米特法则(LoD):最少知道原则,只与直接朋友通信。
    最少惊讶原则:设计应符合预期,避免让用户惊讶。
    高内聚低耦合:类内部高内聚,类之间低耦合。
    KISS 原则:保持设计简单。
    DRY 原则:避免代码重复。
    策略模式定义优惠券
    策略模式(Strategy Pattern)是一种行为设计模式,它通过将算法或行为封装到不同的策略类中,使得它们可以互换。这种模式可以在优惠券规则的定义中很好的应用,以实现灵活的优惠策略切换和管理。接下来,我将详细阐述如何在定义优惠券规则时使用策略模式。

  1. 应用场景描述
    在优惠券模块中,可能有多种优惠券类型,每种优惠券的使用规则不同,例如:
    满减优惠券:满一定金额减去固定金额。
    折扣优惠券:按照一定比例打折。
    现金券:直接抵扣一定金额。
    限时优惠券:只能在特定时间段使用。
    如果不使用策略模式,所有优惠规则会被硬编码到一个类中,这样会导致类变得臃肿,难以维护和扩展。通过策略模式,可以将不同的优惠规则封装到各自的策略类中,当需要添加新的优惠规则时,只需新增一个策略类即可,不需要修改原有的代码,符合“开闭原则”(对扩展开放,对修改关闭)。
  2. 策略模式的设计
    2.1. 策略接口
    首先,定义一个策略接口 CouponStrategy,该接口规定所有优惠券策略的通用方法,比如计算优惠金额的方法 calculateDiscount()。
    2.2. 具体策略类
    根据不同的优惠券规则,定义多个策略类,每个策略类实现 CouponStrategy 接口,并根据不同的优惠逻辑来实现 calculateDiscount() 方法。
    满减优惠策略(满100减20)
    折扣优惠策略(打9折)
    现金优惠策略(减10元)
    2.3. 上下文类
    上下文类 CouponContext 持有一个 CouponStrategy 的引用,通过该类的 setCouponStrategy() 方法可以动态切换不同的策略。
    2.4. 客户端代码
    在实际使用中,可以根据用户选择的优惠券类型,动态设置对应的优惠策略。
    输出结果:
  3. 策略模式的优势
    扩展性好:增加新的优惠券类型,只需新增一个策略类,而不需要修改现有代码。
    简化代码结构:将各类优惠规则封装到独立的策略类中,避免了大量的 if-else 或者 switch 语句,使代码结构更加清晰。
    动态选择策略:可以根据用户的具体情况动态选择不同的策略,增加了系统的灵活性。
  4. 策略模式的改进与扩展
    策略工厂:如果策略类较多,可以引入策略工厂模式,将策略类的创建逻辑封装到工厂类中。
    策略配置:通过配置文件或者数据库表管理不同优惠券对应的策略类,增加策略管理的灵活性。
    组合模式:将策略模式与组合模式结合,支持多种优惠策略的组合使用,例如满减+折扣的组合优惠。
  5. 总结
    通过策略模式,可以有效地将不同的优惠规则进行解耦和封装,从而在项目中实现灵活的优惠策略切换和管理,满足不同业务场景的需求。同时,在后期维护和扩展中,也只需新增或修改相应的策略类,大大提高了系统的可维护性。
    微服务环境下常用的设计模式
  6. 单例模式(Singleton Pattern)
    模式描述: 单例模式确保某个类只有一个实例,并提供一个全局访问点。
    使用场景: 在微服务中,尤其是服务的某些核心资源(如配置、连接池、日志管理器等),可能需要全局唯一的实例。例如,在管理数据库连接池时,采用单例模式能够确保系统只有一个连接池实例,避免重复创建连接池带来的资源浪费和冲突。
    在并发中的注意事项: 在并发环境中,单例模式的实现需要注意线程安全,通常会使用双重检查锁(Double-Checked Locking)或者通过静态内部类的方式来确保单例对象的创建是线程安全的。
  7. 工厂模式(Factory Pattern)
    模式描述: 工厂模式通过定义一个接口或抽象类,允许子类决定实例化的具体类。
    使用场景: 在微服务中,不同服务可能需要创建不同类型的对象,但对象的具体类型依赖于运行时的上下文。例如,在处理不同类型的消息或事件时,通过工厂模式创建不同的处理器来处理不同的消息类型。这样可以通过灵活的工厂来管理对象的创建,而无需在代码中硬编码每种类型的创建逻辑。
    在并发中的注意事项: 工厂模式用于高并发场景时,如果对象的创建成本较高,可以结合对象池模式(Object Pool Pattern)来缓存和重用已经创建的对象,减少频繁创建和销毁对象带来的性能开销。
  8. 代理模式(Proxy Pattern)
    模式描述: 代理模式为另一个对象提供一个代理或占位符来控制对该对象的访问。
    使用场景: 在微服务中,常见的代理模式应用场景是服务的访问控制、负载均衡、缓存等。尤其在分布式服务系统中,可以使用代理模式来代理对某个远程服务的访问,甚至可以添加缓存层来减少对远程服务的频繁调用。
    在并发中的注意事项: 代理模式在并发场景中可能需要处理多个线程对代理对象的并发访问,通常可以借助于线程池、队列或同步机制来确保高效、线程安全的代理访问。
  9. 观察者模式(Observer Pattern)
    模式描述: 观察者模式定义了一种一对多的依赖关系,多个观察者对象会自动监听某个主题对象的状态变化。
    使用场景: 在微服务架构中,服务之间通常通过事件或消息进行解耦的通信。观察者模式非常适合应用在事件驱动的系统中,当某个服务的状态发生变化时(例如订单状态的更新、库存变化等),可以通过事件通知机制,自动触发多个相关服务(如通知服务、日志记录服务等)的执行。基于观察者模式的事件驱动架构可以提高系统的可扩展性和响应性。
    在并发中的注意事项: 在高并发场景中,多个观察者可能同时处理同一个事件。为了防止竞态条件(Race Condition)和保证数据一致性,可以引入消息队列(如Kafka、RabbitMQ),确保事件的处理是顺序的和幂等的。
  10. 策略模式(Strategy Pattern)
    模式描述: 策略模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换。
    使用场景: 在微服务架构中,服务可能需要根据不同的请求参数或上下文选择不同的处理策略。例如,在负载均衡、限流、熔断策略中,策略模式可以用来灵活选择不同的算法(如随机、轮询、权重等)来处理请求。这样可以实现处理逻辑的解耦,并允许在运行时动态改变处理策略。
    在并发中的注意事项: 策略模式在高并发场景下的使用需要注意策略的线程安全性。策略的选择通常是无状态的,但是如果策略中涉及到共享资源的读写操作,需使用适当的同步机制来保证线程安全。
  11. 责任链模式(Chain of Responsibility Pattern)
    模式描述: 责任链模式将多个处理器串联成一个链,请求在处理器链上传递,直到有处理器处理该请求。
    使用场景: 在微服务中,责任链模式常用于请求过滤器、拦截器的实现。例如,在处理用户请求时,通常会有多个处理步骤(如认证、鉴权、限流、数据验证等),每个步骤可以由一个责任链上的节点来处理,节点之间相互独立。这样不仅可以灵活添加或移除处理节点,还能增强系统的可扩展性。
    在并发中的注意事项: 在并发场景中,责任链的各个节点之间应避免共享状态,保证链条的每一环都能独立运行。如果某个节点涉及到外部资源的访问,可以考虑使用异步或并发的方式来处理。
  12. 命令模式(Command Pattern)
    模式描述: 命令模式将请求封装成对象,以便使用不同的请求、队列或日志来参数化对象。
    使用场景: 在微服务架构中,尤其是有并发和分布式事务的情况下,命令模式常用于任务的调度与执行。例如,在处理并发事务时,可以将各个事务操作封装成命令对象,并将这些命令对象放入队列中按序执行或回滚操作。这也为重试机制和事务的幂等性提供了支持。
    在并发中的注意事项: 命令模式下,如果多个命令对象需要并发执行,通常需要保证命令对象的状态是线程安全的。可以使用线程池管理命令的执行,避免资源的过度消耗。
  13. 熔断器模式(Circuit Breaker Pattern)
    模式描述: 熔断器模式是为了解决系统过载和服务雪崩问题的一种设计模式。当某个服务的错误率达到一定阈值时,熔断器会自动打开,阻止继续发送请求,保护系统。
    使用场景: 在微服务系统中,尤其是有并发需求的场景下,当某个下游服务不可用或响应时间过长时,熔断器可以保护调用方服务不至于因长时间等待而崩溃。熔断器通常与监控系统结合,用来动态调整和监控下游服务的健康状况。
    在并发中的注意事项: 熔断器在并发环境中要保证状态的切换是线程安全的,通常使用原子操作或并发数据结构来实现熔断器的状态变化。同时,可以与限流器结合,确保高并发下的服务稳定性。
    04.拦截器的过滤器了解么?说说具体的流程
    过滤器:属于 Servlet 规范,执行优先级高,在请求进入 Servlet 之前和响应离开 Servlet 之前执行,用于全局处理请求或响应(如日志、编码、跨域)。
    拦截器:属于 Spring MVC,主要在请求进入控制器之前和离开控制器之后执行,针对控制器请求,可以用于业务逻辑处理(如权限验证、请求修改)
    示例流程
    示例流程
    假设你有一个请求 /example,此请求经过了一个过滤器和一个拦截器。
    过滤器执行:
    过滤器的 doFilter() 方法在请求到达控制器之前执行。你可以在这里修改请求参数,验证用户权限,或记录日志。
    chain.doFilter(request, response) 使请求继续传递到下一个过滤器或控制器。
    拦截器的 preHandle() 执行:
    请求到达拦截器时,preHandle() 方法会被调用,通常用于权限控制、记录日志等。如果 preHandle() 返回 false,请求会被终止,不会继续到达控制器。
    控制器执行:
    如果 preHandle() 通过,控制器的业务逻辑会被执行。
    拦截器的 postHandle() 执行:
    控制器执行完毕,postHandle() 会被调用。此时,控制器还没有返回视图。你可以在这里修改 ModelAndView,进行一些后置处理。
    拦截器的 afterCompletion() 执行:
    请求响应完毕后,afterCompletion() 会被调用。这里通常用于资源的清理或者记录最终的日志。
    过滤器的后续处理:
    chain.doFilter() 之后,过滤器中的后置处理代码将执行,处理完之后将响应返回给客户端。
    权限校验
    过滤器:通过 doFilter() 方法拦截请求并检查权限,在请求进入 Servlet 之前进行处理。
    拦截器:通过 preHandle() 方法拦截控制器请求,在控制器方法执行之前进行权限验证。
  14. CompletableFuture的原理和使用场景
    原理
    底层原理:CompletableFuture 基于 ForkJoinPool 实现异步任务的调度,通过非阻塞编程模型让任务可以并发执行。
    回调机制:通过 thenApply、thenAccept 等方法注册回调,任务完成后自动执行后续逻辑,避免传统阻塞式 get() 调用。
    任务组合与状态管理:支持任务的组合,如 thenCompose、thenCombine 等,可以灵活地构建异步任务链,并通过内部的状态管理机制处理任务的结果和异常。
    线程池与性能优化:可以使用默认的 ForkJoinPool 或自定义 Executor 来管理线程资源,优化异步任务的执行。
    使用场景
    商品价格查询:并发查询不同供应商的价格并汇总,返回最低或最高价格。
    订单状态跟踪:同时从多个服务(如物流、支付等)获取订单状态,最终汇总展示。
    批量任务处理:将多个独立的计算任务(如大数据处理)并行化,节省时间并提高效率。
    在实际的项目中,CompletableFuture 常用于处理 异步调用,尤其是涉及并发请求、IO密集型任务或需要非阻塞操作的场景。一个典型的应用场景是 并发调用多个外部服务(如微服务或API)并聚合结果,例如电商平台查询多个仓库的库存情况、查询多个供应商的价格等。
    业务场景示例:查询多个仓库的库存并汇总
    场景描述:
    假设我们有一个电商平台,用户购买商品时需要查询不同仓库的库存情况,以决定从哪个仓库发货。每个仓库的库存数据是通过调用不同的微服务(或外部API)获取的。为了减少等待时间并提高系统的响应速度,我们可以使用 CompletableFuture 来并发查询多个仓库的库存,并在所有查询完成后汇总结果。
    06.sql调优做过没 说一个具体的场景
    订单场景
  15. 使用覆盖索引:
    如果查询中涉及的列比较多,尤其是如果 SELECT * 查询出所有字段,数据库在执行查询时无法完全利用索引。可以通过覆盖索引来优化,避免回表操作。
    在创建了索引 user_id 和 order_time 后,可以只选择需要的列,改写 SQL 查询为:
    这样,查询可以直接从索引中获取结果,减少回表的次数,大幅度提高查询性能。
  16. 基于主键或索引的延迟分页优化:
    为了避免大量的偏移操作 (LIMIT offset),可以采用主键或者索引的延迟分页。假设订单表的主键是 order_id,可以改写 SQL,利用上一页的最后一条数据来优化分页。
    示例优化 SQL:
    在这种方式下,前端可以记录当前页最后一条订单的 order_time,然后将其传给后续分页查询的 SQL 作为条件,实现基于索引的分页跳转。这样可以避免使用 LIMIT offset,提升分页查询效率。
  17. 缓存热数据:
    对于高频次访问的订单数据(如最近一周的订单),可以考虑在缓存中(如 Redis)缓存部分热门订单数据。每次用户访问时,先从缓存中读取较新的数据,再从数据库中获取不在缓存范围内的历史数据。
    优化效果:
    覆盖索引的使用,可以避免回表操作,直接从索引中获取数据,减少 I/O 操作。
    延迟分页优化,避免 LIMIT 偏移带来的性能问题,尤其适用于分页深度较大的场景。
    缓存热数据,减少数据库的查询压力,提升高频访问的响应速度。
    07.有没有用到事务?都有什么事务?事务失效的场景
    事务失效场景:
    在哈奇马宠物服务平台的优惠券模块中,用户下单时系统会扣减优惠券库存并记录优惠券使用情况。这个过程中,涉及到优惠券库存更新、订单生成等多个服务的远程调用操作。有一次,在服务调用时,优惠券库存更新成功,但由于订单服务调用失败,导致订单未生成。而由于这些操作属于不同的服务,没有在同一个事务中执行,最终导致了数据不一致的现象。
    解决方案:
    为了解决这个问题,我使用了 分布式事务管理框架(如 Seata),确保不同服务的调用都在同一个分布式事务中执行。通过二阶段提交协议,保证了优惠券扣减和订单生成操作的一致性,即如果订单服务调用失败,优惠券库存扣减操作会被回滚,从而避免了数据不一致问题。此外,还增加了 幂等性设计 和 补偿机制,进一步提升了系统的健壮性。
    为啥要引入补偿机制
    AT 模式
    AT 模式(Automatic Transaction模式)主要用于处理分布式事务,其工作原理如下:
    业务执行:在每个参与的微服务中,业务操作被封装在一个全局事务中执行。Seata 会在每个操作前自动生成一个回滚日志,保存原始数据快照。
    阶段一(准备阶段):执行业务逻辑,并将变更记录在回滚日志中,但数据尚未提交。
    阶段二(提交或回滚):如果全局事务成功,则提交所有操作;如果失败,Seata 会根据回滚日志撤销更改,恢复原始状态。
    08.双写一致性问题以及解决方案
    延迟双删的一般流程如下:
    删除缓存:首先删除缓存中与数据库数据相关联的 key。
    更新数据库:执行数据库的写操作,更新数据库中的数据。
    休眠一段时间:设置一个合理的延迟时间(通常是缓存被再次填充的时间,几百毫秒左右,具体时长依据业务特性决定)。
    再次删除缓存:在延迟时间过后,再次删除缓存,确保缓存中的数据与数据库一致。
    09.幂等问题和场景以及常见解决方案
    保证MQ幂等性通常是指保证消费者消费消息的幂等性。
    1、使用数据库的唯一约束去控制。
    比如:添加唯一索引保证添加数据的幂等性
    2、使用token机制
    发送消息时给消息指定一个唯一的ID
    发送消息时将消息ID写入Redis
    消费时根据消息ID查询Redis判断是否已经消费,如果已经消费则不再消费
    10.说说你们生产环境redis部署情况
    一般部分服务做缓存用的Redis直接做主从(1主1从)加哨兵就可以了。单节点不超过10G内存,如果Redis内存不足则可以给不同服务分配独立的Redis主从节点。尽量不做分片集群。原因:
    维护起来比较麻烦
    集群之间的心跳检测和数据通信会消耗大量的网络带宽
    集群插槽分配不均和key的分批容易导致数据倾斜
    客户端的route会有性能损耗
    集群模式下无法使用lua脚本、事务
    11.Excel的批量导入导出(数据的导入导出)
    在项目中,我们经常遇到需要批量导入导出 Excel 文件的场景。为了解决这一问题,我选择使用了 EasyExcel 库。相对于 Apache POI,EasyExcel 更加轻量,并且在处理大数据量时,内存占用更低,性能更好,特别适合需要高并发和大批量数据导入导出的场景。
    导出操作:
    在导出数据时,我封装了一个通用的导出方法。这个方法可以接收不同类型的 Java 对象,并动态生成 Excel 文件。通过 EasyExcel.write() 方法,可以指定输出文件、数据类型以及 Excel 表头和数据的映射关系。由于 EasyExcel 采用流式写入的方式,即使是百万级的数据也可以高效地导出而不会导致内存溢出。
    导入操作:
    对于批量导入数据,我同样封装了通用的导入函数,使用 EasyExcel.read() 读取 Excel 文件,并借助 AnalysisEventListener 逐行解析数据。在处理大文件时,我采用了分批次处理的策略,每读取一定数量的数据就进行一次处理,以减少内存压力。同时,导入过程中还会对数据进行校验,并对异常数据进行记录和处理。
    扩展性:
    我还为导入导出的功能做了模块化封装,能够适应不同业务场景。例如,不同的数据模型可以通过参数化传递,这样我们在项目中只需少量修改就可以实现对不同 Excel 文件的读写操作,大大提高了代码的可复用性和维护性。
    总的来说,EasyExcel 在性能、内存占用和易用性上都有优势,特别是在处理大数据量时表现非常出色。通过封装,我们确保了导入导出功能的灵活性和可扩展性,同时也保证了系统的性能。
    数据的导入导出常见场景
  18. Excel 导入导出
    在处理 Excel 文件时,我使用了 EasyExcel 库来实现批量导入导出操作。通过封装通用的导入导出方法,项目可以轻松导入用户数据、财务报表等业务数据。在处理大数据量时,我使用了流式写入和分批次导入策略,确保系统性能的稳定性。
  19. CSV 文件导入导出
    由于 CSV 文件结构简单、体积较小且兼容性好,在一些轻量级的数据导入导出需求中(如导入日志、交易记录等),我会使用 OpenCSV 或者直接通过 Java 原生的 IO 进行 CSV 解析和生成。CSV 文件的导入导出操作与 Excel 类似,但由于 CSV 没有复杂的格式问题,因此实现上更为简单,适合处理轻量数据。
  20. 数据库的数据导入导出
    在项目中,我们还遇到过需要将数据库中的数据批量导出为 CSV、Excel 或其他格式,或者从外部系统导入数据到数据库的场景。为了提高效率,我使用了 批处理 方式进行数据导入,配合事务管理,确保数据的一致性与完整性。此外,为了避免一次性处理大量数据导致的性能问题,我使用了分页查询和批量插入技术。
  21. JSON 数据的导入导出
    在与第三方系统集成时,常常需要以 JSON 格式传递数据。我使用 Java 的 Jackson 或 Gson 库,将对象序列化为 JSON,或者反序列化为 Java 对象,完成数据的导入导出。这种方式在 RESTful API 交互中非常常见,比如导入导出用户信息、订单数据等。
  22. XML 数据的导入导出
    在一些与传统系统对接的场景中,我们还使用了 XML 格式进行数据交换。使用 JAXB 或 DOM 解析 XML 数据,并将其映射为 Java 对象,或将 Java 对象转化为 XML 格式进行导出。这在银行系统或政府系统等对安全性和数据结构有较高要求的场景中比较常见。
  23. 批量导入外部数据源
    我们在项目中有时需要从外部系统导入大量数据到我们的数据库或缓存系统(如 Redis),这时我们会使用 ETL(Extract, Transform, Load) 工具,如 Apache NiFi 或自定义的 ETL 处理流程,从多个数据源(如 API、CSV 文件、Excel 文件等)批量导入数据,并在导入过程中进行数据清洗和转换。
    12.登录的设计流程
    登录流程
  24. 前端部分:
    首先,从前端角度来看,登录通常是通过一个简单的表单来实现。用户输入用户名(或邮箱、手机号)和密码,然后点击“登录”按钮,表单会将这些数据通过POST请求发送到后端API。
    在前端,我会做一些基本的验证,比如检查用户名和密码是否为空,以及密码的长度是否符合要求。提交前,我们会确保通过HTTPS来加密传输,避免敏感信息在网络上传输时被截获。
  25. 后端接收请求:
    当后端收到登录请求后,会执行以下几个步骤:
    参数校验:首先,对提交的数据做基本的校验,确保输入内容符合要求,比如防止SQL注入等安全问题。
    用户查找:通过用户名或邮箱在数据库中查找用户信息。如果用户不存在,我会返回一个通用的错误信息,比如“用户名或密码错误”,以防止恶意攻击者通过用户名判断账户是否存在。
    密码校验:如果用户存在,我会使用安全哈希算法对用户输入的密码进行比对。通常我会使用bcrypt或argon2来加密存储的密码,因为这些算法在性能和安全性之间有很好的平衡。每次登录时,我会将用户输入的明文密码通过同样的加密算法处理,然后和数据库中的哈希值进行比较,确保密码正确。
  26. 认证方式:
    如果密码验证通过,接下来就是用户认证的部分。通常我会采用以下两种认证方式之一:
    JWT(JSON Web Token):
    我会使用JWT生成一个Token,里面包含用户的身份信息,比如用户ID、角色等。
    生成Token时,我会使用服务器的密钥进行签名,并设置一个有效期(比如1小时)。
    Token会返回给前端,前端可以将其存储在localStorage或httpOnly Cookie中。每次前端发起请求时,都会携带这个Token,从而完成用户的身份验证。
    Session认证:
    对于需要基于服务器状态的项目,我可能会使用Session。服务器会创建一个Session ID,并通过Set-Cookie将其发送给客户端。
    浏览器之后每次请求都会自动带上这个Session ID,服务器会通过Session来确认用户身份。
  27. 安全性设计:
    在设计登录流程时,安全性是非常重要的考虑因素。我的设计包括以下几个方面:
    HTTPS加密:登录请求必须通过HTTPS传输,防止中间人攻击。
    密码加密存储:所有密码都经过bcrypt等哈希算法加密存储,避免即使数据库泄露,明文密码也不会泄露。
    防暴力破解:如果同一用户或IP在短时间内多次登录失败,我会锁定该账户或启用验证码,防止暴力破解。
    CSRF防护:如果使用Session认证,我会结合CSRF Token机制来防止跨站请求伪造。
    XSS防护:对于JWT,我建议使用httpOnly Cookie存储,防止Token被恶意JavaScript脚本窃取。
  28. 扩展性设计:
    为了提高登录系统的扩展性,我会设计支持多种登录方式:
    第三方登录(OAuth):集成第三方的登录方式,比如Google、GitHub等,用户可以选择直接通过OAuth登录,简化登录流程。
    多因素认证(MFA):为了增加安全性,我会引入多因素认证,用户登录后还需要通过短信验证码或身份验证器进行二次认证。
    单点登录(SSO):在企业级应用中,我会通过OAuth或SAML实现单点登录,确保用户在多个子系统之间只需要登录一次。
  29. 总结:
    整个登录流程从前端表单提交,到后端接收处理,进行数据库查询、密码校验、生成Token或Session,最后将结果返回给前端,并确保登录过程的安全性和用户体验。这个设计既能保证安全性,也有较好的扩展性
    13.项目中接口的安全性设计
    接口安全性问题(例如订单和支付服务)
  30. HTTPS 加密通信
    首先,最基础的安全措施是使用HTTPS协议进行通信。所有的接口请求,无论是从订单服务调用支付服务,还是支付服务返回结果,都应该通过TLS(传输层安全协议)加密,以防止数据在传输过程中被窃取或篡改。
    订单服务与支付服务之间的通信都必须通过HTTPS进行,这样可以避免中间人攻击,防止敏感信息如订单ID、支付金额、支付方式等在传输过程中被泄露。
  31. 身份认证与授权
    在确保通信安全后,还需要验证请求发起者的身份。常用的认证方式有:
    API Key:每个服务对外提供接口时,都会生成一个唯一的API Key。订单服务在调用支付服务时,必须携带该API Key。支付服务接收到请求后,会验证API Key的合法性,确认是信任的调用方。
    OAuth 2.0:如果系统比较复杂且涉及多个服务之间的相互调用,可以采用OAuth 2.0协议。OAuth 2.0允许订单服务在请求支付服务前,先获取一个Access Token,然后携带这个Token请求支付服务。支付服务通过验证Token来确认请求的合法性。
    使用OAuth 2.0时,订单服务会先向认证服务器请求Token,获取Token后将其附加在调用支付服务的请求头中:
    JWT(JSON Web Token):有时候会使用JWT来替代OAuth 2.0中的Access Token。JWT包含用户的身份信息和权限,并由服务端签名,支付服务接收到请求后,通过解密和验证签名,确保Token的合法性和未篡改。
    在系统中,我会生成JWT并加密签名,在请求支付服务时,将JWT传递给支付服务,支付服务通过验证JWT的签名和解码来确认请求者身份:
  32. 请求的完整性和签名验证
    为了防止请求在传输过程中被篡改,可以使用签名机制来保证请求的完整性:
    签名机制:订单服务在发出请求时,对请求体或请求参数进行签名处理,将签名值附加在请求中。支付服务接收请求后,通过同样的算法和密钥验证签名的正确性。签名确保即使攻击者截获了请求并修改了参数,支付服务也能发现异常并拒绝处理。
    例如,订单服务可以将请求内容如order_id、amount等参数按一定顺序拼接成一个字符串,再通过HMAC-SHA256算法生成签名:
    这个签名会附加在请求中,支付服务收到请求后也会用相同的密钥计算签名并验证。
  33. 时间戳和防重放攻击
    在设计接口安全性时,还需要考虑到重放攻击。攻击者可能会截获请求包并在之后重新发送,以此达到欺骗服务器执行重复支付等恶意操作。为了防止这种攻击,可以采取以下措施:
    时间戳机制:在请求中附带一个时间戳,服务器会校验时间戳是否在允许的时间范围内(例如5分钟)。如果时间戳超出允许的范围,服务器会拒绝该请求。
    Nonce(随机数)机制:每次请求时,订单服务生成一个唯一的随机数(nonce)并发送给支付服务,支付服务会记录该随机数,并确保该随机数只被使用一次。这样可以避免攻击者重复发送相同的请求。
    结合时间戳和随机数,支付服务可以有效防止重放攻击:
  34. 最小权限原则
    在服务之间的相互调用中,应该严格遵循最小权限原则,即每个服务只授予它在某个业务场景下所需要的最低权限。通过细粒度的权限控制来提高安全性:
    权限管理:每个调用方在获取Token或API Key时,都会被分配相应的权限。比如订单服务在调用支付服务时,可能只具有“创建支付订单”权限,而不能执行其他操作(如退款、查询敏感信息等)。
    细化API权限:支付服务提供的接口应根据功能细分,不同的接口只能由特定的服务调用,避免不必要的权限泄露。
  35. 防止SQL注入和输入验证
    在接收到远程请求后,需要对所有输入参数进行严格的校验和过滤,以防止恶意用户通过请求传递特殊字符或SQL语句,导致SQL注入等攻击。
    使用ORM或预编译语句来防止SQL注入。
    对所有用户输入进行验证,防止传入非法参数。
  36. 日志和监控
    日志记录:记录所有接口请求的详细日志,包括请求来源、请求内容、响应时间等信息。通过日志可以帮助追踪攻击来源或异常情况。
    异常监控:监控服务接口的调用频率和异常情况,例如频繁的请求、验证失败或签名错误等,帮助识别潜在的安全威胁。
  37. 限流与防止DDoS攻击
    限流机制:防止某一服务被过度调用而导致资源耗尽或拒绝服务。可以通过设置接口的调用频率限制或请求次数来保护服务。
    使用WAF(Web Application Firewall):对于外部暴露的接口,可以使用WAF来检测和防御常见的攻击,如SQL注入、XSS等。
    总结:
    在设计订单服务与支付服务等远程调用的安全性时,我通常会通过以下几个核心措施来确保安全:
    HTTPS 加密传输防止传输过程中的信息泄露。
    身份认证与授权机制(API Key、OAuth、JWT等)确保服务间的请求是可信的。
    签名机制保证请求数据的完整性和防篡改。
    时间戳和Nonce机制防止重放攻击。
    最小权限控制保证调用方只能执行所需操作。
    输入验证和防注入确保输入的安全性。
    日志和监控帮助发现异常和攻击行为。
    限流和DDoS防护保护服务不被恶意滥用。
    通过这些措施,我可以确保在远程服务调用中的数据安全和系统稳定性。
    14.新技术的学习
    es
    云原生
    15.JVM的调优实践
    调优参数
    1)设置堆的初始大小和最大大小,为了防止垃圾收集器在初始大小、最大大小之间收缩堆而产生额外的时间,通常把最大、初始大小设置为相同的值。
    2) 设置年轻代中Eden区和两个Survivor区的大小比例。该值如果不设置,则默认比例为8:1:1。Java官方通过增大Eden区的大小,来减少YGC发生的次数,但有时我们发现,虽然次数减少了,但Eden区满的时候,由于占用的空间较大,导致释放缓慢,此时STW的时间较长,因此需要按照程序情况去调优。
    -XXSurvivorRatio=3,表示年轻代中的分配比率:survivor:eden = 2:3
    3)年轻代和老年代默认比例为1:2。可以通过调整二者空间大小比率来设置两者的大小。
    4)线程堆栈的设置:每个线程默认会开启1M的堆栈,用于存放栈帧、调用参数、局部变量等,但一般256K就够用。通常减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统。
    -Xss 对每个线程stack大小的调整,-Xss128k
    5)一般来说,当survivor区不够大或者占用量达到50%,就会把一些对象放到老年区。通过设置合理的eden区,survivor区及使用率,可以将年轻对象保存在年轻代,从而避免full GC,使用-Xmn设置年轻代的大小
    6)系统CPU持续飙高的话,首先先排查代码问题,如果代码没问题,则咨询运维或者云服务器供应商,通常服务器重启或者服务器迁移即可解决。
    7)对于占用内存比较多的大对象,一般会选择在老年代分配内存。如果在年轻代给大对象分配内存,年轻代内存不够了,就要在eden区移动大量对象到老年代,然后这些移动的对象可能很快消亡,因此导致full GC。通过设置参数:-XX:PetenureSizeThreshold=1000000,单位为B,标明对象大小超过1M时,在老年代(tenured)分配内存空间。
    8)一般情况下,年轻对象放在eden区,当第一次GC后,如果对象还存活,放到survivor区,此后,每GC一次,年龄增加1,当对象的年龄达到阈值,就被放到tenured老年区。这个阈值可以同构-XX:MaxTenuringThreshold设置。如果想让对象留在年轻代,可以设置比较大的阈值。
    9)尝试使用大的内存分页:使用大的内存分页增加CPU的内存寻址能力,从而系统的性能。
    -XX:+LargePageSizeInBytes 设置内存页的大小
    10)使用非占用的垃圾收集器。
    -XX:+UseConcMarkSweepGC老年代使用CMS收集器降低停顿。
    调优工具
    命令:
    jstack--查看java进程内线程的堆栈信息。
    jmap--
    用于生成堆转存快照
    jmap [options] pid 内存映像信息
    jmap -heap pid 显示Java堆的信息
    jmap -dump:format=b,file=heap.hprof pid
     format=b表示以hprof二进制格式转储Java堆的内存        file=<filename>用于指定快照dump文件的文件名。
    
    jstat--
    是JVM统计监测工具。可以用来显示垃圾回收信息、类加载信息、新生代统计信息等。
    可视化工具jconsole:
    调优案例
    内存泄露排查思路
    16.OOM的场景排查和解决方案
    常见OOM场景
    Java堆内存不足(Java Heap Space OOM)
    这是最常见的一种OOM,当应用程序中的对象占用的堆内存超过JVM设置的最大堆内存时,会抛出OutOfMemoryError: Java heap space。
    直接内存不足(Direct Buffer Memory OOM)
    Java的NIO(非阻塞IO)使用直接内存(Direct Memory),如果直接内存分配超过了设置的最大值,会抛出OutOfMemoryError: Direct buffer memory。
    Metaspace(或PermGen)内存不足
    在JDK 8及以后,Metaspace替代了永久代(PermGen),当类加载过多或占用了大量元数据空间时,可能会出现OutOfMemoryError: Metaspace。
    GC无法回收足够内存(GC Overhead Limit Exceeded)
    当GC(垃圾回收)占用了过多时间,并且只能回收很少的内存时,JVM会抛出OutOfMemoryError: GC overhead limit exceeded。
    具体案例
    在我参与的一个电商系统项目中,我们遇到过一次Java堆内存不足(Java heap space)的OOM问题。该项目是一个典型的微服务架构,用户访问量大,尤其是在促销活动期间,系统需要处理大量订单请求和并发操作。某次促销活动期间,我们发现服务响应变慢,甚至部分实例出现了OOM,导致服务崩溃。
    问题分析
    为了排查OOM问题,我们采取了以下步骤:
  38. 通过监控发现异常
    我们在项目中集成了Prometheus + Grafana来监控应用的JVM运行状态,实时查看内存使用情况。通过监控面板,我们发现:
    堆内存(Heap)使用率持续上升,GC频繁触发,最后堆内存达到上限。
    OOM日志显示OutOfMemoryError: Java heap space,明确是堆内存不足。
  39. 捕获堆内存快照(Heap Dump)
    为了深入分析OOM原因,我们在发生OOM时抓取了堆内存快照(Heap Dump),用于后续分析。堆内存快照可以通过jmap工具获取:
    该文件存储了应用程序在OOM时的内存状态。
  40. 使用MAT(Memory Analyzer Tool)分析内存泄漏
    我们将生成的heapdump.hprof文件导入MAT(Memory Analyzer Tool)进行分析。MAT可以帮助我们找到哪些对象占用了最多的内存,以及是否存在内存泄漏。
    通过MAT的“Dominator Tree”分析,我们发现有大量的订单缓存对象没有被及时清理,堆内存中持有了大量的无效数据。这些缓存对象本应该在订单处理完成后被清理,但由于业务代码中的某个缓存管理逻辑存在问题,导致对象没有被正确释放,进而导致堆内存持续增长。
  41. 使用Arthas诊断问题
    为了在生产环境中进一步排查问题,我们使用了Arthas工具。通过Arthas的dashboard命令,我们可以实时查看应用的内存和GC状况。使用以下命令查看内存占用情况:
    通过这个命令,我们确认了内存占用异常的对象主要集中在订单缓存相关的类上。
    我们还使用了watch命令动态查看某些方法的返回值:
    确认了缓存没有按照预期进行清理,进一步佐证了内存泄漏的可能。
    解决方案
    针对OOM问题,我们采取了以下措施:
  42. 优化缓存策略
    我们对缓存管理逻辑进行了优化,改用了LRU(Least Recently Used)缓存策略。在订单缓存达到一定大小时,会自动淘汰最早的无用数据,避免内存占用过多。同时,设置了缓存的过期时间(TTL,Time To Live),保证旧订单数据在一定时间后会被自动清除。
  43. 调整JVM内存配置
    根据实际的内存使用情况,我们适当调整了JVM的堆内存大小:
    将堆内存的最小值设置为4GB,最大值设置为8GB,以确保在高并发和高负载的情况下,系统有足够的内存可用。
  44. 监控和告警
    我们还在Prometheus + Grafana监控平台上增加了内存告警阈值,当堆内存使用率达到80%时,触发告警通知,提醒运维团队及时关注系统内存使用情况,避免OOM再次发生。
  45. 使用JVM参数优化GC行为
    为了防止GC过于频繁并影响系统性能,我们使用了G1 GC来替代之前的CMS GC,并设置了合理的GC停顿时间:
    G1 GC能够更好地管理堆内存分配,并减少Full GC的发生频率,最大停顿时间设置为200ms,以减少GC对响应时间的影响。
    最终效果
    通过这些优化措施:
    缓存逻辑改进后,内存占用得到了有效控制,避免了大量无用对象堆积。
    JVM内存配置调整后,在高并发场景下堆内存的使用更加合理。
    GC行为得到了显著优化,Full GC次数大大减少,GC停顿时间缩短,系统响应时间得到了改善。
    系统再也没有出现OOM异常,整体性能和稳定性都有明显提升
    MAT分析dump文件
  46. 捕获堆内存快照(Heap Dump)
    当我们的系统出现OOM异常时,我使用了jmap工具来生成堆内存快照,命令如下:
    生成的堆内存快照(heapdump.hprof)保存了应用程序在内存中的所有对象和引用关系。随后,我使用MAT(Memory Analyzer Tool)进行深入分析,以确定导致OOM的根因。
  47. 具体分析Dump文件的过程(关键字、工具使用)
    Step 1: 导入Heap Dump文件
    首先,将生成的heapdump.hprof文件导入MAT。在MAT中,这些数据会被解析为内存中的对象和它们之间的引用关系,提供了一份详细的内存报告。
    Step 2: 分析Heap Dump中的“Dominator Tree”
    MAT最有用的功能之一是Dominator Tree。它通过分析对象间的引用关系,告诉我们哪些对象占用了最多的内存(即占用最多空间的“根对象”)。在这个视图中,我们能够看到内存的主要消耗者是哪些对象。
    关键字:Retained Heap:Retained Heap是MAT中的一个关键概念,表示如果某个对象被回收,那么它引用的对象也都会被回收。通过观察Retained Heap最大的对象,我们可以迅速定位哪些对象持有大量的内存。
    在我的项目中,分析Dominator Tree后,发现某些订单缓存对象的Retained Heap非常大。这个缓存对象本应在订单处理完成后被清理,但由于代码中的逻辑问题,未能及时释放。
    Step 3: 检查“Histogram”分析对象分布
    接下来,我通过Histogram视图分析了对象的数量和分布情况。Histogram按对象类型列出了每个类在内存中的实例数及其消耗的内存。
    关键字:Instance Count 和 Shallow Heap:
    Instance Count:表示每个类的实例数量。我们关注的是实例数量过多的对象,特别是那些不应该长期存在的短生命周期对象。
    Shallow Heap:表示每个实例本身的内存占用量。通过这个关键字,可以看到哪类对象总量多,且每个对象占用内存较大。
    在分析时,我发现了某些缓存类(例如OrderCacheEntry)实例数量异常多,且每个实例占用了较多内存。进一步分析,发现这些缓存对象的生命周期不符合预期,并且由于引用链的问题没有被GC回收。
    Step 4: 查找潜在的内存泄漏
    MAT还提供了专门的Leak Suspects功能,自动分析可能的内存泄漏点。通过点击Leak Suspects Report,我们能够得到一份报告,显示内存中最大的对象和它们的引用关系。
    关键字:Path to GC Root:MAT中的Path to GC Root功能非常关键,它帮助我们找到某个对象为何无法被GC回收。在OOM的案例中,我们通过此功能追踪到了缓存对象的引用链,发现这些对象由于某些静态集合没有清理,仍然持有对无效缓存对象的引用,导致这些对象无法被回收。
    Step 5: 追踪具体对象引用路径
    通过Path to GC Roots,我们进一步追踪了问题对象的引用路径,找到了它们是被某个缓存管理器持有引用。通过分析引用路径,确认是由于缓存没有设置合理的TTL(Time-to-Live)机制,导致大量旧的订单数据一直留存在内存中,无法被GC回收。
    关键字:GC Root:MAT会显示对象的GC Root,即能够保留这些对象在内存中的根对象。在这个项目中,我们发现某个缓存集合被持久持有,而这些集合没有被合理地清理,导致大量无效的缓存对象滞留。
    Step 6: 查找大对象(Biggest Objects)
    为了进一步确认内存占用问题,我们使用MAT中的Biggest Objects功能,查看哪些对象占用了最多的内存空间。通过此功能,快速发现了大量大对象,例如缓存的订单数据类,占据了大量的堆内存。
    关键字:Biggest Objects:列出了占用内存最多的对象,在OOM排查中尤为有用,能够快速缩小分析范围。
    在这个项目中,我们通过分析这些大对象,发现大量订单缓存占用了不必要的内存空间,而这些订单数据本应在处理完成后被清理。
    Step 7: 持续监控GC行为
    在分析内存问题的同时,我们还通过GC Root追踪了对象被GC回收的路径,确认了这些缓存对象是因为存在未清理的引用而无法被GC回收。配合jstat工具,我们实时监控了GC的执行情况,发现每次Full GC的效果非常有限,且GC耗时较长。
    Step 8: 结合工具得出结论
    通过以上的MAT分析,我们最终得出结论:
    由于缓存策略不当,导致订单缓存对象没有被及时清理,堆内存被这些对象逐渐填满,最终引发OOM。
    我们发现缓存中的无效数据未被及时淘汰,并且某些对象持有对缓存的强引用,导致GC无法清理这些对象。
    解决方案
    基于MAT分析得出的结果,我们对系统进行了以下优化:
    优化缓存清理策略:引入了LRU(Least Recently Used)淘汰机制,确保缓存不会无限制增长,并且设置了合理的TTL(Time-to-Live),确保过期的订单缓存能够被及时清理。
    调整内存配置:适当调整了JVM堆内存的大小,增加内存空间以适应高并发的需求:
    热点八股
    java基础
  48. jdk jre jvm
    JDK(Java Development Kit)是一\
    吗,,,,,,,,,,, 功能齐全的 Java 开发工具包,包含了 JRE以及编译器 javac 和其他工具,如 javadoc(文档生成器)、jdb(调试器)、jconsole(监控工具)、javap(反编译工具)
    JRE 是运行已编译 Java 程序所需的环境,主要包含以下两个部分:
    JVM : 运行 Java 字节码的虚拟机(HotSpot VM)--一次编译,随处可以运行--跨平台
    Java 基础类库(Class Library):一组标准的类库,提供常用的功能和 API(如 I/O 操作、网络通信、数据结构等)
    class字节码--非热点走解释器---热点走jit存下编译后机器码下次直接使用
    02.java和c++的区别
    Java 和 C++ 都是面向对象的语言,都支持封装、继承和多态
    Java 不提供指针来直接访问内存,程序内存更加安全
    Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承。
    Java 有自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用内存。
    C ++同时支持方法重载和操作符重载,但是 Java 只支持方法重载
    03.包装类
    缓存:Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character 创建了数值在 [0,127] 范围的缓存数据,Boolean 直接返回 True or False。
    比较: 都用equals 缓存区的用==可以比较 缓存区外的==比较地址 强制都用equals
    装箱: valueOf() 拆箱: xxxValue()
    04.静态变量/方法
    静态变量: 只创建一份, 所有实例共享,静态变量只会被分配一次内存,即使创建多个对象,这样可以节省内存。
    静态方法: 属于类 类加载时分配内存 直接用类名访问 不能调用非静态
    05.重载和重写的区别
    重载: 同类中 同类同名不同参 与返回值修饰符无关
    重写: 子类重写父类 同名同参 返回值和异常范围<=父类 访问修饰符>=父类
    06.接口和抽象类的区别
    接口: 可以多实现 可以多继承 8之前只有抽象方法 8之后引入默认方法 静态私有方法 成员变量psfinal 强调行为约束
    抽象类: 只能单继承 抽象方法 非抽象方法都可以有 强调复用和所属
    07.多态的作用
    简化代码扩展与维护 -- 父new子 父类引用调用子类方法 增加子类扩展新功能
    提高代码的可重用性 -- 基于接口或抽象类编程
    解耦设计,提升灵活性 -- 实现"面向接口编程"的思想
    支持运行时动态绑定-- 动态地选择适当的方法来调用 根据传入类型动态绑定
    实现设计模式 -- 策略模式和工厂模式
    08.深拷贝 浅拷贝 引用拷贝 零拷贝(优化技术)
    浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。
    深拷贝:深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。
    引用拷贝: 不同引用指向同一个对象
    零拷贝: 零拷贝"是一种优化技术,减少数据在用户态和内核态之间的复制,直接在内核空间进行数据传输,降低CPU负载,提高IO性能,常用于大文件和网络数据传输。
    09.hashcode()和equals()方法
    不重写时都是默认比较的引用对象的地址
    重写时根据重写方法比较引用对象的内容
    hashcode相同不代表对象相同 可能会发生哈希碰撞
    先判断散列码再判断equals在散列集合中可以提高效率
    10.String StringBuffer StringBuilder
    String: 不可变--保存字符串的字符数据final且私有 类也是私有(jdk9以后三者都采用byte数据存)
    String的+ 和+=是java中唯二重载过的运算符 底层还是StringBuilder 调用 append()
    字符串常量池: 避免重复创建 1.7以后在堆中 底层是StringTable 维护字符串和地址的映射关系
    StringBuffer 有同步锁线程安全
    StringBuilder 线程不安全
    11.异常类
    结构图
    Checked Exception 即 受检查异常 ,Java 代码在编译过程中,如果受检查异常没有被 catch或者throws 关键字处理的话,就没办法通过编译。除了RuntimeException及其子类以外,其他的Exception类及其子类都属于受检查异常 。常见的受检查异常有:IO 相关的异常、ClassNotFoundException、SQLException...
    Unchecked Exception 即 不受检查异常 ,Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。
    RuntimeException 及其子类都统称为非受检查异常
    常见runtime
    12.泛型
    泛型类、泛型接口、泛型方法
    项目中使用泛型场景:
    自定义接口通用返回结果 CommonResult 通过参数 T 可根据具体的返回类型动态指定结果的数据类型
    定义 Excel 处理类 ExcelUtil 用于动态指定 Excel 导出的数据类型
    构建集合工具类(参考 Collections 中的 sort, binarySearch 方法
    泛型擦除: 泛型擦除是指Java在编译期间移除泛型类型信息,以保持与早期版本的兼容。编译时,泛型参数被替换为其边界或原始类型(如Object),导致运行时无法获取泛型类型信息。这带来了无法使用泛型数组、运行时类型检查受限等问题。 ( instanceof List 非法)
    13.反射
    反射可以获取任意一个类的所有属性和方法,还可以调用这些方法和属性
    JDK 实现动态代理,使用了反射类 Method 来调用指定的方法
    注解也用到了反射
    获取class: Class.forName()传入类的全路径获取 对象实例instance.getClass()
    使用场景: 依赖注入 动态代理 对象序列号 动态调用方法 单测调用私有方法
    14.IO流
    顶级父类
    InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
    OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流
    涉及到的设计模式
    装饰器(Decorator)模式 可以在不改变原有对象的情况下拓展其功能--BufferedInputStream
    适配器(Adapter Pattern)模式 主要用于接口互不兼容的类的协调工作--InputStreamReader 和 OutputStreamWriter
    BIO NIO AIO
    BIO 属于同步阻塞 IO 模型 --阻塞等待
    同步非阻塞 IO --断进行 I/O 系统调用轮询数据是否已经准备好
    IO 多路复用模型--线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用
    选择器 ( Selector ) 的概念,也可以被称为 多路复用器。通过它,只需要一个线程便可以管理多个客户端连接。当客户端数据到了之后,才会为其服务
    AIO--异步io 基于事件的回调机制
    总结图
    15.java的值传递(区分引用传递)
    值传递:方法接收的是实参值的拷贝,会创建副本。
    引用传递:方法接收的直接是实参所引用的对象在堆中的地址,不会创建副本,对形参的修改将影响到实参
    基本类型:值传递,方法内部的修改不影响原值。
    引用类型:传递的是引用的拷贝(也是值传递),可以修改引用所指对象的内容,但不能改变引用本身。
    16.动态代理
    jdk的动态代理原理和使用
    在Java 动态代理:InvocationHandler 接口和 Proxy 类是核心。
    Proxy 类中使用频率最高的方法是:newProxyInstance()
    loader :类加载器,用于加载代理对象。
    interfaces : 被代理类实现的一些接口;
    h : 实现了 InvocationHandler 接口的对象;
    方法的调用转发到实现InvocationHandler 接口类的 invoke 方法来调用
    invoke() 方法有下面三个参数:
    proxy :动态生成的代理类
    method : 与代理类对象调用的方法相对应
    args : 当前 method 方法的参数
    通过Proxy 类的 newProxyInstance() 创建的代理对象在调用方法的时候,实际会调用到实现InvocationHandler 接口的类的 invoke()方法。 你可以在 invoke() 方法中自定义处理逻辑
    使用步骤
    定义接口和对应实现类
    定义动态代理类-实现InvocationHandler接口-重写invoke方法
    获取代理对象的工厂类-getProxy():通过Proxy.newProxyInstance()方法获取类的代理对象
    通过工厂类.getProxy() 传入代理类 拿到代理对象
    CGLIB动态代理原理和使用
    CGLIB 动态代理机制
    JDK 动态代理只能代理实现了接口的类
    AOP :如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB
    使用步骤:
    定义一个类;
    自定义 MethodInterceptor 并重写 intercept 方法,intercept 用于拦截增强被代理类的方法,和 JDK 动态代理中的 invoke 方法类似;
    通过 Enhancer 类的 create()创建代理类
    JDK 动态代理只能代理实现了接口的类或者直接代理接口,而 CGLIB 可以代理未实现任何接口的类。 另外, CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为 final 类型的类和方法。
    就二者的效率来说,大部分情况都是 JDK 动态代理更优秀,随着 JDK 版本的升级,这个优势更加明显。
    集合专题
    List(对付顺序的好帮手): 存储的元素是有序的、可重复的。
    Set(注重独一无二的性质): 存储的元素不可重复的。
    Queue(实现排队功能的叫号机): 按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的。
    Map(用 key 来搜索的专家): 使用键值对(key-value)存储,类似于数学上的函数 y=f(x),"x" 代表 key,"y" 代表 value,key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值
    集合选取:
    我们需要根据键值获取到元素值时就选用 Map 接口下的集合,需要排序时选择 TreeMap,不需要排序时就选择 HashMap,需要保证线程安全就选用 ConcurrentHashMap。
    我们只需要存放元素值时,就选择实现Collection 接口的集合,需要保证元素唯一时选择实现 Set 接口的集合比如 TreeSet 或 HashSet,不需要就选择实现 List 接口的比如 ArrayList 或 LinkedList,然后再根据实现这些接口的集合的特点来选用。
    01.ArrayList 和 LinkedList
    线程安全: 都不是同步的 都不是线程安全的
    数据结构: ArrayList是 Object 数组;LinkedList 底层使用的是 双向链表
    元素插入 数组与链表的区别
    数组支持快速访问 链表不支持
    ArrayList 扩容机制
    无参构造为空数组 第一次添加元素容量初始化为10
    达到最大容量 再次添加元素后 会扩容为原来的1.5倍
    每次扩容都是将旧数组的元素复制到新的数组中 有一定的开销(事先预估大小 减少开销)
    02.HashMap
    数据结构: 1.8之前数组+链表 1.8之后数组+链表+红黑树
    key 的 hashCode 经过扰动函数处理过后得到 hash 值,通过 (n - 1) & hash 判断 n为数组长度
    hash(key) 是一个哈希函数 减少hash冲突 分布更均匀
    数组长度>=64且链表长度>8(阈值) 此时链表会转为红黑树
    扩容: 负载因子为0.75 默认容量为16 大于负载就会扩容为两倍
    扩容需要 rehash 复制数据到新数组 消耗性能
    put插入元素: 定位后无元素直接插入 有元素先遍历比较key 相同直接覆盖 不相同先判断是否为树节点 是树节点则执行红黑树插入 如果不是则遍历链表进行尾部插入
    03.HashSet、LinkedHashSet 和 TreeSet的异同
    都是无重复元素 都是线程不安全
    特性
    HashSet
    LinkedHashSet
    TreeSet
    底层实现
    基于 HashMap
    基于 LinkedHashMap
    基于 TreeMap
    (红黑树)
    元素顺序
    不保证顺序
    按照插入顺序排序
    按照元素的自然顺序或自定义比较器排序
    查找/插入性能
    O(1) 平均时间复杂度
    O(1) 平均时间复杂度
    O(log n) 时间复杂度
    有序性
    无序
    按照插入顺序
    按照自然顺序或自定义顺序
    支持的操作
    支持基本集合操作
    支持基本集合操作 + 顺序相关操作
    支持基本集合操作 + 排序相关操作
    null 元素
    允许插入一个 null
    元素
    允许插入一个 null
    元素
    不允许插入 null
    元素
    04.ConcurrentHashMap为什么线程安全
    Node + CAS + synchronized 来保证并发安全
    synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,就不会影响其他 Node 的读写
    key 和 value 不能为 null 主要是为了避免二义性 多线程下无法正确判定键值对是否存在
    ConcurrentHashMap 提供了一些原子性的复合操作,如 putIfAbsent、compute、computeIfAbsent 、computeIfPresent、merge等。这些方法都可以接受一个函数作为参数,根据给定的 key 和 value 来计算一个新的 value,并且将其更新到 map 中
    Collections 提供了多个synchronizedXxx()方法·,该方法可以将指定集合包装成线程同步的集合,从而解决多线程并发访问集合时的线程安全问题。
    SSM springboot
    01.IOC 和AOP
    IOC
    IoC容器来帮我们实例化对象。 降低对象之间的耦合度。依赖注入(DI)是实现这种技术的一种方式
    利用Java的反射机制动态地加载类、创建对象实例及调用对象方法,反射允许在运行时检查类、方法、属性等信息,从而实现灵活的对象实例化和管理。
    AOP
    是面向切面编程,能够将那些与业务无关,却为业务模块所共同调用的逻辑封装起来,以减少系统的重复代码,降低模块间的耦合度。Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候 Spring AOP 会使用 Cglib 生成一个被代理对象的子类来作为代理。
    02.循环依赖
    三级缓存(singletonFactories):存放工厂方法,用于生成 Bean 的早期引用。
    二级缓存(earlySingletonObjects):存放尚未完全初始化的 Bean 实例。
    一级缓存(singletonObjects):存放完全初始化的 Bean。
    Spring 首先创建 A 的实例(未初始化),并将其工厂方法放入三级缓存。
    创建 B 时,B 依赖 A,此时 Spring 去三级缓存中找到了 A 的工厂方法,取出 A 的早期引用,并放入二级缓存。
    B 获取到 A 的早期引用,完成了 B 的初始化,并将 B 放入一级缓存。
    A 再次完成它剩下的初始化步骤(属性填充等),最后也将完整的 A 放入一级缓存。
    03.常用注解
    Spring:
    @Component @Service @Repository @Controller
    @Autowired @Qualifier @Configuration @Bean
    Spring MVC
    @RequestMapping @GetMapping @PostMapping
    @RequestParam @PathVariable @RequestBody @ResponseBody @RestController
    @Transactional @Aspect
    Spring Boot 特有注解
    @SpringBootApplication:组合注解,包括 @Configuration、@EnableAutoConfiguration 和 @ComponentScan,是 Spring Boot 启动类的核心注解。
    @EnableAutoConfiguration:启用 Spring Boot 的自动配置功能。
    @ComponentScan:指定 Spring 容器扫描的包路径,自动发现并注册符合条件的组件。
    @RestControllerAdvice:全局异常处理,监控控制器中的异常并进行统一处理。
    04.Spring事务失效和传播行为
    失效场景
  49. 同类中非代理调用
    原因:Spring 事务依赖 AOP 代理机制进行事务控制。如果在同一个类中,事务方法被其他非事务方法直接调用(即内部调用),AOP 代理无法介入,事务不会生效。
    解决方法:通过代理对象调用事务方法,或使用 AopContext.currentProxy() 进行代理调用。
  50. 事务方法的访问修饰符不是 public
    原因:Spring 的 AOP 代理只会拦截 public 方法
  51. 异常类型不符合回滚规则
    原因:Spring 默认只对未捕获的 RuntimeException(即非检查异常)和 Error 进行回滚,对于 CheckedException(检查异常)不会触发回滚,除非明确配置。
    解决方法:在 @Transactional 注解中使用 rollbackFor 或 noRollbackFor 属性明确指定哪些异常应触发回滚。
  52. 手动捕获并处理了异常
  53. @Transactional 注解应用在接口而非实现类上
    原因:在使用 JDK 动态代理时,事务注解如果应用在接口而不是实现类上,事务会失效。Spring AOP 默认使用基于接口的 JDK 动态代理,只有接口方法会被代理。
    解决方法:确保 @Transactional 注解在具体的实现类或方法上,或使用 CGLIB 代理(代理类本身而非接口)。
  54. 数据库引擎不支持事务
  55. 事务传播行为设置不当
    原因:不合理地使用事务传播行为,例如使用 Propagation.REQUIRES_NEW 会挂起当前事务并开启新事务,可能导致事务隔离或预期行为失效。
    解决方法:根据具体业务需求合理配置事务的传播行为。
  56. 多线程环境下事务失效
    原因:Spring 事务是线程绑定的,事务上下文只能在当前线程中传播。如果在事务方法中启动了新线程,事务上下文不会传播到新线程。
    解决方法:避免在事务中启动新线程,或使用事务管理器(如 @TransactionalEventListener)确保事务跨线程传播。
  57. 异步方法导致事务失效
    原因:@Async 注解会使方法在异步线程中执行,而事务是与当前线程绑定的,异步线程无法感知当前的事务上下文。
    解决方法:避免在异步方法中进行事务控制,或确保事务在异步方法调用前已经提交。
    传播行为
    05.Bean的生命周期
  58. 实例化(Instantiation)
    Spring 容器根据配置(XML、注解或 Java 配置类)创建 Bean 的实例。此时,Bean 已经存在于内存中,但尚未进行属性的依赖注入。
    步骤:Spring 调用 BeanFactory 的 createBeanInstance() 方法,使用构造器创建实例。
  59. 属性注入(Populate Properties)
    Spring 容器根据 Bean 的定义,将需要注入的依赖属性(通过构造器、@Autowired 或 XML 中的 property)注入到 Bean 中。
    步骤:Spring 使用 populateBean() 方法为 Bean 设置属性。
  60. BeanNameAware 接口回调
    如果 Bean 实现了 BeanNameAware 接口,Spring 会调用 setBeanName() 方法,传入当前 Bean 的名称。
  61. BeanFactoryAware / ApplicationContextAware 接口回调
    如果 Bean 实现了 BeanFactoryAware 或 ApplicationContextAware 接口,Spring 分别调用 setBeanFactory() 或 setApplicationContext() 方法,将 BeanFactory 或 ApplicationContext 注入 Bean。
  62. BeanPostProcessor 前置处理
    在 Bean 初始化之前,Spring 会调用所有已注册的 BeanPostProcessor 的 postProcessBeforeInitialization() 方法。此步骤允许在 Bean 初始化之前进行一些自定义逻辑。
  63. InitializingBean 接口回调 / 自定义初始化方法
    如果 Bean 实现了 InitializingBean 接口,Spring 会调用其 afterPropertiesSet() 方法,进行初始化操作。
    如果在 XML 中定义了 init-method 或通过 @PostConstruct 注解标记了初始化方法,Spring 会调用这些自定义的初始化方法。
  64. BeanPostProcessor 后置处理
    在 Bean 完成初始化之后,Spring 会再次调用所有已注册的 BeanPostProcessor 的 postProcessAfterInitialization() 方法。这一步可以对 Bean 的最终形态进行修改或代理增强。
  65. Bean 就绪(Ready to Use)
    经过上述步骤,Bean 已经完成初始化并注入到 Spring 容器中,可以被应用程序使用。
  66. 销毁(Destruction)
    当 Spring 容器关闭时,Bean 会被销毁。如果 Bean 实现了 DisposableBean 接口,Spring 会调用其 destroy() 方法。
    如果在 XML 中配置了 destroy-method 或使用了 @PreDestroy 注解,Spring 也会调用这些自定义的销毁方法,执行 Bean 销毁前的清理操作。
    06.SpringMVC的执行流程
    流程说明
    07.Spring Boot Starter 编写流程总结
    创建 Maven/Gradle 模块:定义 Starter 所需依赖。
    编写自动配置类:使用 @Configuration 和条件注解实现自动配置功能。
    定义可配置属性类:使用 @ConfigurationProperties 提供用户可配置的属性。
    注册到 spring.factories:在 META-INF/spring.factories 中注册自动配置类。
    打包发布:将 Starter 打包发布到 Maven 仓库或其他代码仓库。
    测试 Starter:编写集成测试,确保 Starter 的自动配置和功能正常工作。
  67. Spring Boot 的自动装配原理
    启动时,Spring Boot 通过 @EnableAutoConfiguration 加载 spring.factories 中定义的自动配置类。
    自动配置类中的 @Configuration 配置会根据条件注解检查环境、类路径、配置文件等条件,确定是否需要创建和注入某些 Bean。
    如果满足条件,Spring 容器会自动将符合条件的 Bean 注入到上下文中,无需手动配置。
  68. Mybatis里的 # 和 $ 的区别? 什么时候用$

    (占位符方式):将传入的参数作为预编译的占位符,MyBatis 会自动对参数进行SQL 注入防护,将其当作参数来处理。例如:WHERE id = #{id}。适用于传递变量、数值等。

    $(拼接方式):将传入的参数直接拼接到 SQL 语句中,不会进行参数转义,可能会导致 SQL 注入风险。例如:ORDER BY ${column}。通常用于传递字段名、表名等。
    使用 $ 的场景:
    动态 SQL 语句中,当需要传递字段名、表名等而不是具体的值时使用 $,例如动态的 ORDER BY 或 GROUP BY。
    Mysql
    00.MySql优化经验
    表的设计优化
    ①选择表合适存储引擎:
    myisam: 应用时以读和插入操作为主,只有少量的更新和删除,并且对事务的完整性,并发性要求不是很高的。
    Innodb: 事务处理,以及并发条件下要求数据的一致性。除了插入和查询外,包括很多的更新和删除。尽量 设计 所有字段都得有默认值,尽量避免null。
    ②选择合适的数据类型
    数据库表设计时候更小的占磁盘空间尽可能使用更小的整数类型,一般来说,数据库中的表越小,在它上面执行的查询也就会越快。
    比如设置合适的数值(tinyint int bigint),要根据实际情况选择
    比如设置合适的字符串类型(char和varchar)char定长效率高,varchar可变长度,效率稍低
    索引优化
    表的主键、外键必须有索引;
    数据量大的表应该有索引;
    经常与其他表进行连接的表,在连接字段上应该建立索引;
    经常出现在Where子句中的字段,特别是大表的字段,应该建立索引;
    索引应该建在选择性高的字段上; (sex 性别这种就不适合)
    索引应该建在小字段上,对于大的文本字段甚至超长字段,不要建索引;
    频繁进行数据操作的表,不要建立太多的索引;
    删除无用的索引,避免对执行计划造成负面影响;
    sql语句优化
    SELECT语句务必指明字段名称(避免直接使用select * )
    SQL语句要避免造成索引失效的写法
    SQL语句中IN包含的值不应过多
    当只需要一条数据的时候,使用limit 1
    如果排序字段没有用到索引,就尽量少排序
    如果限制条件中其他字段没有索引,尽量少用or
    尽量用union all代替union
    避免在where子句中对字段进行null值判断
    不建议使用%前缀模糊查询
    避免在where子句中对字段进行表达式操作
    Join优化 能用innerjoin 就不用left join right join,如必须使用 一定要已小表为驱动
    主从复制、读写分离
    如果数据库的使用场景读的操作比较的时候,为了避免写的操作所造成的性能影响 可以采用读写分离的架构,读写分离,解决的是,数据库的写入,影响了查询的效率。读写分离的基本原理是让主数据库处理事务性增、改、删操作(INSERT、UPDATE、DELETE),而从数据库处理SELECT查询操作。 数据库复制被用来把事务性操作导致的变更同步到集群中的从数据库。
    01.数据库三范式
    1NF 强调列的原子性,确保字段值不可再分。
    2NF 强调消除部分依赖,确保非主键列完全依赖于整个主键。
    3NF 强调消除传递依赖,确保非主键列只依赖于主键,不依赖于其他非主键列。
    02.MySql连表
    INNER JOIN、LEFT JOIN、RIGHT JOIN 和 FULL JOIN
    MySQL 不直接支持 FULL JOIN,但可以通过 UNION 来实现
    03.Sql的执行顺序
    04.MySql的引擎
    InnoDB:支持事务、行级锁,是 MySQL 的默认引擎,适用于大多数场景。
    MyISAM:不支持事务,但读操作快,适合读多写少的场景。
    MEMORY:数据存储在内存中,速度快,适合需要高性能的临时数据存储。
    CSV:数据存储为 CSV 格式,适合数据交换。
    ARCHIVE:适合存储归档数据,具有高压缩性。
    FEDERATED:跨服务器查询的引擎,适用于分布式数据库。
    BLACKHOLE:数据黑洞,引擎适合主从复制和日志记录。
    InnoDB与MyISAM的区别:
    05.索引的分类
    MySQL可以按照四个角度来分类索引:
    按「数据结构」分类:B+tree索引、Hash索引、Full-text索引。
    按「物理存储」分类:聚簇索引(主键索引)、二级索引(辅助索引)。
    按「字段特性」分类:主键索引、唯一索引、普通索引、前缀索引。
    按「字段个数」分类:单列索引、联合索引。
    06.聚簇索引和非聚簇索引
    聚簇索引和非聚簇索引
    07.MySql索引的数据结构
    B+Tree 是一种多叉树,叶子节点才存放数据,非叶子节点只存放索引,而且每个节点里的数据是按主键顺序存放的。每一层父节点的索引值都会出现在下层子节点的索引值中,因此在叶子节点中,包括了所有的索引值信息,并且每一个叶子节点都有两个指针,分别指向下一个叶子节点和上一个叶子节点,形成一个双向链表
  69. NULL 和""的区别
    NULL 代表一个不确定的值,就算是两个 NULL,它俩也不一定相等。例如,SELECT NULL=NULL的结果为 false,但是在我们使用DISTINCT,GROUP BY,ORDER BY时,NULL又被认为是相等的。
    ''的长度是 0,是不占用空间的,而NULL 是需要占用空间的。
    NULL 会影响聚合函数的结果。例如,SUM、AVG、MIN、MAX 等聚合函数会忽略 NULL 值。 COUNT 的处理方式取决于参数的类型。如果参数是 (COUNT()),则会统计所有的记录数,包括 NULL 值;如果参数是某个字段名(COUNT(列名)),则会忽略 NULL 值,只统计非空值的个数。
    查询 NULL 值时,必须使用 IS NULL 或 IS NOT NULLl 来判断,而不能使用 =、!=、 <、> 之类的比较运算符。而''是可以使用这些比较运算符的。
    09.索引创建的原则
    针对于数据量较大,且查询比较频繁的表建立索引
    针对于常作为查询条件(where)、排序(order by)、分组(group by)
    频繁需要排序的字段:索引已经排序,这样查询可以利用索引的排序,加快排序查询时间。
    被经常频繁用于连接的字段:经常用于连接的字段可能是一些外键列,对于外键列并不一定要建立外键,只是说该列涉及到表与表的关系。对于频繁被连接查询的字段,可以考虑建立索引,提高多表连接查询的效率
    尽量选择区分度高的列作为索引,尽量建立唯一索引,区分度越高,使用索引的效率越高。
    如果是字符串类型的字段,字段的长度较长,可以针对于字段的特点,建立前缀索引。
    尽量使用联合索引,减少单列索引,查询时,联合索引很多时候可以覆盖索引,节省存储空间,避免回表,提高查询效率。
    要控制索引的数量,索引并不是多多益善,索引越多,维护索引结构的代价也就越大,会影响增删改的效率。
    不为 NULL 的字段:索引字段的数据应该尽量不为 NULL,因为对于数据为 NULL 的字段,数据库较难优化。如果字段频繁被查询,但又避免不了为 NULL,建议使用 0,1,true,false 这样语义较为清晰的短值或短字符作为替代。
    10.索引失效的场景
    失效场景展开
    不符合最左前缀原则
    使用 OR 关键字
    使用 LIKE 时前置通配符
    数据类型不一致
    在索引列上使用函数操作
    使用 IS NULL 或 IS NOT NULL
    隐式类型转换
    范围查询导致后续列索引失效
    小表不使用索引
    ORDER BY 和 LIMIT 场景下的索引失效
    in 走索引, not in 索引失效
    11.超大分页怎么处理
    使用 LIMIT + 主键(或索引)条件过滤
    通过使用主键或其他唯一索引字段进行过滤,可以避免大部分数据的扫描。这个方法的核心思想是利用索引加速定位,而不是简单地依赖 LIMIT 偏移。
    优化方式:
    这个查询首先通过子查询定位到第10000行的 id,然后通过 id > xxx 进行数据过滤,避免大量的跳过操作。
    适用于有自增主键或索引的表,查询性能会明显改善
    基于ID范围的分段查询
    对于深度分页,可以将偏移量转换为基于ID的范围查询,特别是在有自增主键或有序唯一索引时。
    延迟关联(Deferred Join)
    对于复杂的表结构,涉及多表 JOIN 查询时,先在子查询中获取主键,再通过主键 JOIN 获取完整记录。这样避免了对大量数据的直接操作,提高分页查询效率。
    使用覆盖索引(Covering Index)
    如果查询的数据列全部在索引中,则可以使用覆盖索引,避免回表查询。
    覆盖索引意味着查询的数据直接可以从索引中读取,减少了IO操作。
    12.主从复制 读写分离 同步延时
    读写分离
    读写分离是通过将数据库的读操作和写操作分配到不同的数据库服务器上来减轻单台服务器的负担
    主库(Master)负责处理所有的写操作(INSERT、UPDATE、DELETE等),
    从库(Slave)负责处理所有的读操作(SELECT)。
    主从复制
    Master 主库在事务提交时,会把数据变更记录在二进制日志文件 Binlog 中。
    从库读取主库的二进制日志文件 Binlog ,写入到从库的中继日志 Relay Log 。
    slave重做中继日志中的事件,将改变反映它自己的数据。
    复制模式
    同步延迟
    13.分库分表
    垂直分库 就是把单一数据库按照业务进行划分,不同的业务使用不同的数据库,进而将一个数据库的压力分担到多个数据库。
    水平分库 是把同一个表按一定规则拆分到不同的数据库中,每个库可以位于不同的服务器上,这样就实现了水平扩展,解决了单表的存储和性能瓶颈的问题。
    垂直分表 是对数据表列的拆分,把一张列比较多的表拆分为多张表。
    水平分表 是对数据表行的拆分,把一张行比较多的表拆分为多张表,可以解决单一表数据量过大的问题。
    导致的问题以及解决的方式:
    实现方式: ShardingSphere 支持读写分离和分库分表,还提供分布式事务、数据库治理、影子库、数据加密和脱敏等功能。
    14.慢SQL 定位和分析以及优化
    开启 MySQL 慢查询日志
    MySQL提供了慢查询日志功能,可以记录执行时间超过指定时间阈值的SQL语句。
    使用 EXPLAIN 分析执行计划
    id:查询的执行顺序。
    select_type:查询类型,常见的有 SIMPLE、PRIMARY、SUBQUERY等。
    table:正在访问的表。
    type:连接类型,影响查询效率的重要指标。常见的类型有:
    system:常见于小表查询。
    const:表示通过唯一索引(如主键)定位到单行数据,性能较好。
    ref:表示使用非唯一索引进行查询,性能较好。
    fulltext表示MySQL使用全文索引来进行全文检索
    range表示MySQL通过索引查找某个范围内的数据
    index表示MySQL进行全索引扫描,而不是全表扫描
    ALL:表示全表扫描,性能最差。
    possible_keys:查询可以使用的索引。
    key:实际使用的索引。
    key_len:使用的索引长度。
    rows:预计扫描的行数。
    Extra:执行过程中额外的信息,如 Using where、Using index、Using filesort、Using temporary 等。其中,Using filesort 和 Using temporary 是影响性能的警告标志。
    如果EXPLAIN的Extra字段中显示Using index,并且没有Using where,则说明MySQL使用了覆盖索引,查询数据时不需要回表,性能较优。
    使用 SHOW PROFILE 分析查询性能
    SHOW PROFILE 的输出会显示每个阶段的耗时,例如 Sending data、Copying to tmp table、Sorting result 等。可以据此判断查询的瓶颈是在哪里(如排序、临时表、发送数据等)。
    使用 SHOW STATUS 获取SQL执行状态
    Handler_read_rnd_next:全表扫描的行数。如果该值很大,说明SQL语句可能在做全表扫描。
    Created_tmp_disk_tables:如果该值很大,说明查询过程中频繁创建磁盘临时表,影响性能。
    Select_full_join:没有使用索引的关联查询的次数,值大说明SQL语句可能存在未使用索引的 JOIN 操作。
    15.事务 并发事务 隔离级别
    原子性(Atomicity):事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
    一致性(Consistency):执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的;
    隔离性(Isolation):并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
    持久性(Durability):一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
    并发事务带来的问题

16.MySql中的锁
表级锁(Table Lock)
读锁(共享锁,READ LOCK): 在读锁下,多个事务可以同时读取表,但无法对表进行写入操作。其他事务可以继续获得读锁,但写锁会被阻塞。
写锁(独占锁,WRITE LOCK): 在写锁下,只有获得锁的事务可以对表进行读写操作,其他任何事务(包括读事务)都会被阻塞。
行级锁(Row Lock)
这种锁粒度较细,能够提高数据库的并发处理能力。InnoDB存储引擎支持行级锁。
共享锁(S Lock): 允许多个事务同时读取一行数据,但不能修改。
排他锁(X Lock): 允许对行进行修改,其他事务不能对该行进行读取或修改。
间隙锁(Gap Lock)
间隙锁是InnoDB引擎中引入的一种特殊锁,用于防止幻读(Phantom Read)。它锁定索引记录之间的间隙(甚至包括空的索引位置),避免在间隙中的数据被插入。
优点: 防止幻读
意向锁
为了解决表锁与行锁冲突而引入的一种锁定机制。它通过在表级别声明某个事务打算在行级别加锁,从而让表级锁的兼容性检查变得更高效。常见的意向锁包括意向共享锁(IS)和意向排他锁(IX),它们可以有效地协助并发事务控制,提高数据库的性能和锁管理效率
17.MVCC
MVCC(多版本并发控制,Multiversion Concurrency Control)是一种提高数据库并发性能的机制,广泛应用于InnoDB等支持事务的存储引擎。它通过维护数据的多个版本,实现读写操作的无锁并发,避免了读写冲突,特别适合高并发场景。MVCC主要用于支持两种隔离级别:读已提交(Read Committed)和可重复读(Repeatable Read)。
核心原理:
版本链:在每一行记录中,InnoDB维护两个隐藏字段,分别记录数据的创建版本(事务ID)和删除版本(事务ID)。每次对数据的更新并不会直接覆盖,而是生成一个新版本,并通过事务ID标记其可见性。多个版本通过链表串联,形成“版本链”。
快照读(Snapshot Read):读操作读取的是数据的历史版本,而不是当前最新版本。通过读取与当前事务一致性视图(snapshot)中的版本,确保在执行查询时能够看到一个稳定的结果。这种读取方式不会阻塞写操作。
当前读(Current Read):一些特定的操作,如SELECT FOR UPDATE或UPDATE等,会读取最新版本的数据并加锁,保证其他事务无法修改该数据。
一致性视图(Consistent Read View):每个事务在启动时都会创建一个一致性视图,这个视图决定了当前事务可以看到的数据版本。事务只能看到在其启动之前提交的版本,未提交的更改对当前事务不可见。
MVCC的隔离级别:
读已提交(Read Committed):每次查询都会创建一个新的视图,因此能看到其他事务已提交的数据,但不可重复读。
可重复读(Repeatable Read):事务在开始时创建快照,整个事务期间保持不变,因此同一事务中的多次查询结果一致,避免幻读。
优点:
提高并发性能:读操作不需要加锁,避免了传统锁机制带来的性能开销。
避免死锁:由于读操作不阻塞写操作,死锁发生的概率降低。
redis
01.redis的数据结构
02.redis的持久化策略
RDB:
定期更新,定期将Redis中的数据生成的快照同步到磁盘等介质上,磁盘上保存的就是Redis的内存快照
优点:数据文件的大小相比于aop较小,使用rdb进行数据恢复速度较快
缺点:比较耗时,存在丢失数据的风险
AOF:
将Redis所执行过的所有指令都记录下来,在下次Redis重启时,只需要执行指令就可以了
优点:数据丢失的风险大大降低了
缺点:数据文件的大小相比于rdb较大,使用aop文件进行数据恢复的时候速度较慢
你们的项目中的持久化是如何配置选择的? RDB+AOF
03.redis的主从和集群
主从复制 哨兵模式 分片集群
全量同步:
1.从节点执行replicaof命令,发送自己的replid和offset给主节点
2.主节点判断从节点的replid与自己的是否一致,
3.如果不一致说明是第一次来,需要做全量同步,主节点返回自己的replid给从节点
4.主节点开始执行bgsave,生成rdb文件
5.主节点发送rdb文件给从节点,再发送的过程中
6.从节点接收rdb文件,清空本地数据,加载rdb文件中的数据
7.同步过程中,主节点接收到的新命令写入从节点的写缓冲区(repl_buffer)
8.从节点接收到缓冲区数据后写入本地,并记录最新数据对应的offset
9.后期采用增量同步
增量同步:
1.主节点会不断把自己接收到的命令记录在repl_baklog中,并修改offset
2.从节点向主节点发送psync命令,发送自己的offset和replid
3.主节点判断replid和offset与从节点是否一致
4.如果replid一致,说明是增量同步。然后判断offset是否一致
5.如果从节点offset小于主节点offset,并且在repl_baklog中能找到对应数据则将offset之间相差的数据发送给从节点
6.从节点接收到数据后写入本地,修改自己的offset与主节点一致
分片集群怎么读写:
Redis 集群引入了哈希槽的概念,Redis 集群有 16384 个哈希槽,每个 key通过 CRC16 校验后对 16384 取模来决定放置哪个槽,集群的每个节点负责一部分 hash 槽。
redis的哨兵选举:
主节点故障检测:哨兵节点通过定期 PING 主节点来监控其状态。当某个哨兵节点无法接收到主节点的响应时,会标记主节点为主观下线(SDOWN)。如果多数哨兵都认为主节点不可用,主节点会被标记为客观下线(ODOWN)。
哨兵选举领导者:当主节点被标记为客观下线时,哨兵节点会进行投票选举,选择一个哨兵作为领导者。每个哨兵节点只投一票,获得多数票的哨兵节点将成为领导者,负责执行故障转移。
故障转移:哨兵领导者在选定后,会从当前的从节点中选择一个最新的从节点,将其提升为新的主节点。选择依据通常是数据同步状态、网络延迟等。
集群重新配置:领导者将通知其他从节点重新配置,从新的主节点同步数据。同时,原主节点恢复后将作为从节点继续工作。
redis的集群脑裂:
由于redis master节点和redis salve节点和sentinel处于不同的网络分区,使得sentinel没有能够心跳感知到master,所以通过选举的方式提升了一个salve为master,这样就存在了两个master,就像大脑分裂了一样,这样会导致客户端还在old master那里写入数据,新节点无法同步数据,当网络恢复后,sentinel会将old master降为salve,这时再从新master同步数据,这会导致大量数据丢失。
解决方案:
redis中有两个配置参数:
min-replicas-to-write 1 表示最少的salve节点为1个
min-replicas-max-lag 5 表示数据复制和同步的延迟不能超过5秒
配置了这两个参数:如果发生脑裂:原master会在客户端写入操作的时候拒绝请求。这样可以避免大量数据丢失
04.redis缓存三剑客
缓存穿透:
概述:指查询一个一定不存在的数据,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到 DB 去查询,可能导致 DB 挂掉。
get请求:api/v1/news/13
解决方案:
1、查询返回的数据为空,仍把这个空结果进行缓存,但过期时间会比较短
2、布隆过滤器:将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对DB的查询
缓存击穿
概述:对于设置了过期时间的key,缓存在某个时间点过期的时候,恰好这时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端 DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把 DB 压垮。
解决方案:
使用互斥锁:当缓存失效时,不立即去load db,先使用如 Redis 的 setnx 去设置一个互斥锁,当操作成功返回时再进行 load db的操作并回设缓存,否则重试get缓存的方法
可以设置当前key逻辑过期,大概是思路如下:
①:在设置key的时候,设置一个过期时间字段一块存入缓存中,不给当前key设置过期时间
②:当查询的时候,从redis取出数据后判断时间是否过期
③:如果过期则开通另外一个线程进行数据同步,当前线程正常返回数据,这个数据不是最新
设置热点数据永不过期:对于某些非常热门的 key,直接设置缓存永不过期,避免突然失效的情况
缓存雪崩
概述:设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB 瞬时压力过重雪崩。与缓存击穿的区别:雪崩是很多key,击穿是某一个key缓存。
解决方案:
缓存过期时间随机化:在设置缓存的过期时间时,添加一个随机范围的偏移量,避免所有缓存同时失效。
二级缓存:在缓存层外增加一层本地缓存(如 Guava、Ehcache)作为二级缓存,减少对数据库的访问。
请求限流和降级:在缓存雪崩时,对流量进行限流或降级处理,比如直接返回默认数据或空值,保护数据库不被压垮。
05.redis分布式锁
redis怎么实现分布式锁

  1. 使用 SET 命令设置锁
    通过 Redis 的 SET 命令,结合选项 NX(只在键不存在时设置)和 EX(设置过期时间),可以实现分布式锁的基本功能。
    key:表示锁的名称。
    value:通常是一个唯一标识符,比如 UUID,用来标记谁持有这把锁。
    NX:保证只在 key 不存在时设置成功,这防止多个客户端同时获取锁。
    EX ttl:设置锁的过期时间,避免死锁。如果客户端崩溃,锁不会永久存在。
    示例:
    这行命令表示创建一个名为 lock_key 的锁,值为 unique_value,并且锁会在 10 秒后自动释放(即使没有手动释放),从而避免死锁。
  2. 释放锁
    锁的释放通常需要谨慎处理,因为你需要确保只有持有锁的客户端才能释放它。
    释放锁的正确方式:
    读取锁的值,确认当前持有锁的客户端是自己(通过比对 value)。
    使用 DEL 命令删除该锁。
    由于这两个操作并不是原子性的,可能存在竞态条件(比如在读取值之后锁被另一个客户端重新获取),所以可以使用Lua脚本保证这两个步骤的原子性。
    Lua 脚本释放锁:
    KEYS[1] 是锁的 key。
    ARGV[1] 是锁的 value,即该客户端持有的唯一标识符。
  3. 过期时间与死锁处理
    由于 Redis 的 EX 参数设置了锁的过期时间,即使客户端因为网络或系统崩溃等意外情况未能释放锁,锁也会在 ttl 到期后自动释放。
    要确保过期时间足够长,以避免在正常业务执行期间锁过早过期。常见做法是根据业务复杂性动态调整 ttl,或者在锁即将过期时通过续约机制延长锁的有效期。
  4. Redis分布式锁的经典实现:Redlock算法
    虽然上面的实现能够应对基本的分布式锁场景,但为了在分布式系统中提高锁的可靠性和容错性,Redis作者提出了 Redlock 算法,具体步骤如下:
    客户端向多个(通常是 5 个)独立的 Redis 节点请求锁。
    客户端为每个请求设置超时时间,避免长时间阻塞。
    当客户端能从多数节点(如 3/5 个节点)成功获取锁,并且锁的总时间在合理范围内(需要比锁的过期时间短),认为锁获取成功。
    锁失效后,客户端需要使用相同的方式释放这些节点上的锁。
    这种方法确保即使部分 Redis 节点出现故障,系统仍然能够保持一致性和高可用性。
    总结
    Redis 分布式锁的基本思路是通过 SET NX EX 命令创建锁,并结合 Lua 脚本保证释放锁的原子性。此外,Redlock 是一种更可靠的分布式锁实现,适用于更高容错和可靠性需求的场景
    06.redisson以及实现原理
    redisson的使用介绍和原理
    07.redis的数据过期策略和数据淘汰策略
    数据过期策略:Redis 提供定期删除、惰性删除和主动删除来管理设置了过期时间的数据。它们一起协作,保证 Redis 不会被过期数据填满,同时控制系统的性能开销。
    数据淘汰策略:当内存达到限制时,Redis 会根据配置的淘汰策略(如 LRU、LFU、TTL 等)删除部分数据,确保新数据能够继续写入。不同的淘汰策略适用于不同的应用场景,例如缓存系统可以使用 LRU 或 LFU 策略,而对于需要保证数据不丢失的系统可以选择 noeviction 策略。
    数据过期策略
    数据淘汰策略
    rabbitmq
    01.使用mq的好处
  5. RabbitMQ 如何保证消息不丢失
    生产者端:发布确认机制(Publisher Confirms)确保消息成功投递到 RabbitMQ,必要时可重试发送消息。
    消息队列:消息和队列持久化 以及 高可用机制(镜像队列或仲裁队列),保证 RabbitMQ 重启或节点故障时消息不会丢失。
    消费者端:手动消息确认机制 确保消息被正确消费后才从队列中删除,失败时可以重新投递或发送至死信队列。
    保证消息不丢失
  6. 生产者端保证消息不丢失
    Publisher Confirms(发布确认机制):
    RabbitMQ 提供了发布确认机制来确保消息从生产者发送到 RabbitMQ 服务器并成功被写入队列。生产者发送消息后,会收到 RabbitMQ 的确认或否认(ACK/NACK)响应。
    开启该机制后,生产者会等待 RabbitMQ 返回的确认响应,如果没有收到 ACK,可以重新发送消息。
    事务机制(Transactions):
    RabbitMQ 提供了事务模式,生产者可以在发送消息前开启事务(txSelect),发送消息后提交事务(txCommit)。如果消息发送失败,生产者可以回滚(txRollback)。
    事务机制虽然保证了消息可靠性,但性能开销大,通常推荐使用 发布确认机制 代替事务。
  7. 消息队列保证消息不丢失
    消息持久化(Message Durability):
    队列和消息都需要设置为持久化,以便 RabbitMQ 重启时数据不会丢失。
    队列持久化:创建队列时,将 durable 参数设为 true,使队列在 RabbitMQ 重启后依然存在。
    消息持久化:生产者发送消息时,将消息的 deliveryMode 设置为 2,表示消息应被持久化存储到磁盘。
    注意:消息持久化并不能保证实时性,因为消息写入磁盘是异步的,仍然有极短时间内数据未被同步到磁盘的风险。
    高可用模式(HA)/ 集群模式:
    RabbitMQ 提供了 镜像队列(Mirrored Queues)功能,允许在多个节点上复制队列数据。即使某个节点宕机,其他节点仍然保留完整的数据。
    Quorum Queues(仲裁队列):RabbitMQ 3.8 引入的队列类型,使用 Raft 协议来管理消息和元数据的复制,提供更强的消息可靠性和数据冗余。
  8. 消费者端保证消息不丢失
    手动确认消息(Manual Acknowledgment):
    消费者接收消息时,可以选择手动确认消息处理是否成功。只有当消费者发送确认(ACK)时,RabbitMQ 才会从队列中删除该消息。
    如果消费者没有发送 ACK(比如应用崩溃或消费失败),RabbitMQ 会重新将消息投递给其他消费者,确保消息不丢失。
    死信队列(Dead Letter Queue, DLQ):
    当消息因某种原因(例如消费失败、拒绝消费等)无法被处理时,可以通过设置 死信队列 来存储这些未能被成功处理的消息,以便后续分析或重新处理。
    03.Mq消息重复消费问题
  9. 确保消息处理的幂等性
    幂等性是指某个操作无论执行多少次,最终的结果都是一样的。在应用层面确保操作的幂等性是解决消息重复消费问题的关键。
    幂等操作的示例:
    数据库插入时使用唯一约束(如唯一的订单号或事务 ID)来保证每条数据只被插入一次。
    对已有数据进行更新操作,如 UPDATE 操作,能够多次执行但对同一个对象产生相同的结果。
    使用 upsert(插入或更新)机制,根据消息的唯一标识符进行操作。
    示例: 如果系统需要处理订单,订单 ID 可以作为唯一标识。在接收到重复的订单消息时,系统可以根据订单 ID 判断该订单是否已经处理过,从而避免重复处理。
  10. 消息的唯一性标识(Message Deduplication)
    为每条消息添加一个唯一标识(如 UUID 或业务上的唯一 ID),消费者在处理消息前,检查该唯一标识是否已经处理过。
    如何实现:
    为每条消息生成唯一 ID(比如订单号、事务号)。
    消费者收到消息后,将该 ID 存入数据库或缓存系统(如 Redis)中。
    每次处理消息时,首先检查该 ID 是否已经存在,存在则表明该消息已经处理,直接跳过;否则,处理消息并将 ID 记录下来。
    示例: 在订单系统中,可以为每个订单生成唯一的 order_id。当消息被消费时,首先检查数据库或 Redis 中是否存在这个 order_id,如果已经存在,说明该订单已被处理,避免重复消费。
  11. 消费者端手动 ACK 配置
    RabbitMQ 的消息确认机制可以帮助减少消息重复消费,但无法完全消除重复消费的可能性。为了更好地控制消息的投递和处理流程,通常建议使用 手动确认模式(Manual Acknowledgment):
    手动确认(Manual ACK):消费者在成功处理消息后,调用 basicAck 进行确认,这样 RabbitMQ 才会将消息从队列中移除。如果消息处理失败,RabbitMQ 可以重新将消息投递给其他消费者。
    如果消费失败且不希望消息再次消费,可以通过 basicReject 或 basicNack 命令,并设置 requeue=false,将消息直接丢弃或转发至死信队列(DLQ)。
  12. 避免重复消费的队列设计
    通过适当的队列配置,降低重复消费的可能性。
    设置消息的 TTL(Time To Live):为队列设置消息的 TTL,使得消息在一定时间内失效,避免因为消费失败导致消息长期滞留在队列中,并不断被重新投递。
    死信队列(DLQ):当某条消息无法正常消费时,利用死信队列机制将其转移到特定的队列中,避免其重新投递到主队列。
  13. 分布式锁或事务机制
    在某些场景下,可以使用分布式锁或事务机制来确保同一条消息只被消费一次。
    分布式锁:在处理消息时,通过分布式锁(如 Redis 实现的锁机制),确保同一时间只有一个消费者在处理该消息,防止并发处理同一消息带来的重复问题。
    事务性消息消费:使用事务机制保证消息消费与业务操作的一致性。如果消费失败,回滚事务,避免重复消费。
  14. 使用 RabbitMQ 的 message-id 和 delivery-tag
    RabbitMQ 的每条消息都有一个 message-id 或 delivery-tag(唯一的递送标记),可以结合这些标识进行消息去重处理。
    message-id:应用可以在消息头中指定 message-id,通过检查是否处理过该 message-id 来决定是否重复消费。
    delivery-tag:RabbitMQ 会为每次投递分配一个唯一的 delivery-tag,在确认时可以参考这个标记。
    04.消息堆积怎么解决
    第一:提高消费者的消费能力 ,可以使用多线程消费任务
    第二:增加更多消费者,提高消费速度
          使用工作队列模式, 设置多个消费者消费消费同一个队列中的消息
    
    第三:扩大队列容积,提高堆积上限
    可以使用RabbitMQ惰性队列,惰性队列的好处主要是
    ①接收到消息后直接存入磁盘而非内存
    ②消费者要消费消息时才会从磁盘中读取并加载到内存
    ③支持数百万条的消息存储
    05.如何保证消费的顺序性
    RabbitMQ 本身不保证消息顺序性,但通过 单消费者模式 是最简单的方法,确保顺序。
    为了提升吞吐量,可以结合 消息分区 或 手动 ACK 机制 来平衡顺序性与并发处理。
    对于严格需要全局顺序控制的场景,可以通过 基于数据库表的顺序控制 来确保消息的顺序性。
    通过 Fair Dispatch 和 消息重排机制 也能在多消费者的场景下维持一定的顺序性。
    展开说说
    springCloud
    00.什么是微服务 说一下理解
    将单体服务拆分成一组小型服务。拆分完成之后,每个小型服务都运行在独立的进程中。服务与服务之间采用轻量级的通信机制来进行沟通(Spring Cloud 中是基于Http请求)
    每一个服务都按照具体的业务进行构建,如电商系统中,订单服务,会员服务,支付服务等。这些拆分出来的服务都是独立的应用服务,可以独立的部署到上产环境中。相互之间不会受影响。所以一个微服务项目就可以根据业务场景进行开发。这在单体类项目中是无法实现的。
    优点:松耦合,聚焦单一业务功能,无关开发语言,团队规模降低。在开发中,不需要了解多有业务,只专注于当前功能,便利集中,功能小而精。微服务一个功能受损,对其他功能影响并不是太大,可以快速定位问题。微服务只专注于当前业务逻辑代码,不会和 html、css 或其他界面进行混合。可以灵活搭配技术,独立性比较好。
    缺点:随着服务数量增加,管理复杂,部署复杂,服务器需要增多,服务通信和调用压力增大,运维工程师压力增大,人力资源增多,系统依赖增强,数据一致性,性能监控。
    01.五大组件
    早期我们一般认为的Spring Cloud五大组件是
    Eureka : 注册中心
    Ribbon : 负载均衡
    Feign : 远程调用
    Hystrix : 服务熔断
    Zuul/Gateway : 网关
    随着SpringCloudAlibba在国内兴起 , 我们项目中使用了一些阿里巴巴的组件
    注册中心/配置中心 Nacos
    负载均衡 Ribbon
    服务调用 Feign
    服务保护 sentinel
    服务网关 Gateway
    02.nacos的工作原理
    服务注册:服务启动后向 Nacos 注册自己的信息,包括 IP 地址、端口等。
    服务发现:客户端从 Nacos 获取需要调用的服务实例列表,通过负载均衡算法选择合适的服务实例。
    健康检查:Nacos 对服务实例进行健康检查,保证只有可用的服务被调用。
    配置管理:开发者可以在 Nacos 配置中心管理应用配置,支持实时推送和动态更新。
    03.nacos和eureka的区别
    ① Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式
    ② 临时实例心跳不正常会被剔除,非临时实例则不会被剔除
    ③ Nacos支持服务列表变更的消息推送模式,服务列表更新更及时
    ④ Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式
    04.openfeign具体怎么实现远程调用
    05.负载均衡如何实现
    服务调用过程中的负载均衡一般使用SpringCloud的Ribbon 组件实现 , Feign的底层已经自动集成了Ribbon , 使用起来非常简单
    客户端调用的话一般会通过网关, 通过网关实现请求的路由和负载均衡
    06.Gateway在你们的项目中如何去应用
    Spring Cloud Gateway:是Spring Cloud中所提供的一个服务网关组件,是整个微服务的统一入口,在服务网关中可以实现请求路由、统一的日志记录,流量监控、权限校验等一系列的相关功能!
    项目应用:权限的校验
    具体实现思路:使用Spring Cloud Gateway中的全局过滤器拦截请求(GlobalFilter、Order),从请求头中获取token,然后解析token。如果可以进行正常解析,此时进行放行;如果解析不到直接返回。
    07.项目中如何实现限流的
    我们项目的流量还是比较大的,我们项目中用的令牌桶算法来进行限流的,gateway中进行设置。
    令牌桶是一个存放固定容量令牌的桶,按照固定速率r往桶里添加令牌;桶中最多存放b个令牌,当桶满时,新添加的令牌被丢弃;当一个请求达到时,会尝试从桶中获取令牌;如果有,则继续处理请求;如果没有则排队等待或者直接丢弃;可以发现,漏桶算法的流出速率恒定,而令牌桶算法的流出速率却有可能大于r; 也就说对于突发流量令牌桶也能应付。
    具体使用是,在网关路由中进行过滤器配置,可以设置桶的带下,和固定速率。我们通常也会按照用户访问的ip进行限制,这个令牌需要存入redis,所以也需要集成redis使用。
    08.服务降级和服务熔断
    服务降级
    服务熔断
    juc多线程
    01.线程池
    corePoolSize 核心线程数目
    maximumPoolSize 最大线程数目 = (核心线程+救急线程的最大数目)
    keepAliveTime 生存时间 - 临时线程的生存时间,生存时间内没有新任务,此线程资源会释放
    unit 时间单位 - 临时线程的生存时间单位,如秒、毫秒等
    workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务
    threadFactory 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等
    handler 拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略
    流程
    1.AbortPolicy:直接抛出异常,默认策略;
    2.CallerRunsPolicy:用调用者所在的线程来执行任务;
    3.DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
    4.DiscardPolicy:直接丢弃任务
    IO密集型任务
    一般来说:文件读写、DB读写、网络请求等
    推荐:核心线程数大小设置为2N+1 (N为计算机的CPU核数)
    CPU密集型任务
    一般来说:计算型代码、Bitmap转换、Gson转换等
    推荐:核心线程数大小设置为N+1 (N为计算机的CPU核数)
    Executors 类来创建
    FixedThreadPool:固定数量线程。
    CachedThreadPool:动态调整线程数。
    SingleThreadExecutor:单线程执行任务。
    ScheduledThreadPool:用于定时和周期任务。
    阻塞队列类型
    ArrayBlockingQueue:
    基于数组的有界阻塞队列。
    需要在创建时指定队列的固定大小。
    适合场景:任务数量是可预测且有限的场景。
    LinkedBlockingQueue:
    基于链表的有界或无界阻塞队列(若不指定大小,则为无界)。
    默认大小为Integer.MAX_VALUE,队列容量很大。
    适合场景:任务量较多但又希望能控制任务数量的场景。
    SynchronousQueue:
    不存储任务的队列,每个插入操作必须等待相应的取出操作,反之亦然。
    适合场景:任务提交者和执行者一对一的场景(每个任务都立即分配给线程)。
    PriorityBlockingQueue:
    支持任务优先级排序的无界阻塞队列。
    适合场景:任务需要按优先级顺序执行的场景。
    DelayQueue:
    只有到期的任务才能从队列中取出,任务带有延迟时间。
    适合场景:需要延迟执行或定时任务的场景
    异常复用问题:
    使用execute()时,未捕获异常导致线程终止,线程池创建新线程替代;使用submit()时,异常被封装在Future中,线程继续复用。
  15. synchronized关键字的底层原理和锁升级
    Synchronized 关键字在 Java 中通过锁机制来保证线程安全,底层是基于 JVM 实现的 monitor 锁来工作的。具体原理如下:
    Monitor 锁:每个对象在 JVM 中都有一个 Monitor 对象,synchronized 依赖这个 Monitor 锁。当线程进入同步代码块或方法时,线程需要获取该对象的 Monitor 锁,其他线程只能等待锁释放。
    底层指令:在字节码层面,synchronized 方法会生成 monitorenter 和 monitorexit 指令,用来实现加锁与解锁操作。
    锁优化:为了提升性能,JVM 对 synchronized 进行了多种优化,如偏向锁、轻量级锁和重量级锁,根据不同的线程竞争情况自动调整锁的级别。
    偏向锁:如果没有竞争,锁会偏向于第一个获取它的线程,避免频繁加锁解锁。
    轻量级锁:当有多个线程竞争时,锁会升级为轻量级锁,尽量减少对线程的阻塞。
    重量级锁:在高竞争情况下,锁会进一步升级为重量级锁,通过操作系统的内核锁来阻塞线程,保证线程安全。
    这种分层锁机制可以有效提高性能,在大多数情况下避免不必要的线程阻塞和上下文切换。
    03.线程的状态和生命周期
    NEW: 初始状态,线程被创建出来但没有被调用 start() 。
    RUNNABLE: 运行状态,线程被调用了 start()等待运行的状态。
    BLOCKED:阻塞状态,需要等待锁释放。
    WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
    TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
    TERMINATED:终止状态,表示该线程已经运行完毕。
    04.死锁
    死锁的四个必要条件:
    互斥条件:每个资源要么是空闲的,要么是被某个进程独占,其他进程不能同时使用该资源。
    占有并等待条件:一个进程已经持有一个资源,同时又在等待另一个被其他进程持有的资源。
    不剥夺条件:进程已经获得的资源不能被强制剥夺,只有进程自己释放资源后,资源才能分配给其他进程。
    循环等待条件:系统中存在一个进程链,链中的每个进程都在等待下一个进程持有的资源。即存在进程间的循环等待
  16. 死锁预防
  17. 死锁避免
    通过在进程请求资源时动态分析系统的状态,确保系统不会进入死锁状态。银行家算法是死锁避免的经典算法。
    银行家算法:该算法通过模拟资源分配情况,判断如果满足当前进程的资源请求后,系统是否仍然处于安全状态。如果分配资源后系统仍然安全,则允许分配;否则,拒绝分配请求,避免进入死锁状态。
  18. 死锁检测与恢复
    在无法预防或避免死锁的情况下,可以允许系统发生死锁,然后通过检测和恢复来解决死锁问题。
    死锁检测:通过维护资源分配图或等待图来检测系统是否存在死锁。检测算法会定期检查图中是否存在循环。如果存在循环,则说明系统中有死锁发生。
    死锁恢复:
    终止进程:一旦检测到死锁,可以强制终止某些进程来打破死锁。例如,杀掉一个或多个死锁进程,释放其资源。
    回收资源:强制剥夺进程占有的资源,并将这些资源分配给其他进程,以打破死锁循环。
  19. 死锁忽略
    具体解决死锁的实践建议:
    避免锁的嵌套:尽量避免嵌套锁,即在持有一个锁的情况下再请求另一个锁。如果不可避免,确保多个线程按相同的顺序获取锁,避免循环等待。
    使用超时机制:在请求资源时,可以设定一个超时时间。如果超过该时间还未获得资源,进程/线程将放弃请求并释放已持有的资源。
    使用Try-Lock机制:在获取锁时,使用tryLock()方法,该方法尝试获取锁,如果锁被占用则立即返回,而不是阻塞等待。这可以减少死锁的发生机会。
    减少锁的粒度:尽量减少锁的粒度或锁的持有时间,确保每次锁定的代码块尽量小,从而减少发生死锁的机会
    如何检测死锁
    使用jmap、jstack等命令查看 JVM 线程栈和堆内存的情况。如果有死锁,jstack 的输出中通常会有 Found one Java-level deadlock:的字样,后面会跟着死锁相关的线程信息。另外,实际项目中还可以搭配使用top、df、free等命令查看操作系统的基本情况,出现死锁可能会导致 CPU、内存等资源消耗过高。
    采用 VisualVM、JConsole 等工具进行排查。
    05.synchronized 和ReentrantLock 有什么别?
    两者都是可重入锁
    可重入锁 也叫递归锁,指的是线程可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果是不可重入锁的话,就会造成死锁。
    JDK 提供的所有现成的 Lock 实现类,包括 synchronized 关键字锁都是可重入的。
    相比synchronized,ReentrantLock增加了一些高级功能。主要来说主要有三点:
    等待可中断 : ReentrantLock提供了一种能够中断等待锁的线程的机制,通过 lock.lockInterruptibly() 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
    可实现公平锁 : ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来指定是否是公平的。
    可实现选择性通知(锁可以绑定多个条件): synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制。ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition()方法。
    06.TreadLocal
    通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢?
    JDK 中自带的ThreadLocal类正是为了解决这样的问题。 ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。
    如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal变量名的由来。他们可以使用 get() 和 set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。
    07.AQS
    AQS(AbstractQueuedSynchronizer,抽象队列同步器)
    08.JMM
    Java Memory Model(Java 内存模型)
    jvm
    01.jvm内存划分
    02.jvm的垃圾回收算法
    标记-清除算法:标记存活对象,清除非存活对象,简单但会产生内存碎片。
    复制算法:将存活对象复制到另一块内存区域,适合短生命周期的对象。
    标记-整理算法:整理存活对象,消除碎片,适合老年代。
    分代回收算法:将堆划分为新生代和老年代,结合多种算法提高回收效率,是目前最常用的垃圾回收机
  20. 标记-清除算法(Mark-Sweep)
    描述:这是最基础的垃圾回收算法,分为两个阶段:
    标记阶段:从 GC Roots(垃圾回收根节点,如栈帧中的局部变量、静态变量等)出发,遍历所有可达对象,并将它们标记为存活状态。
    清除阶段:遍历堆中所有对象,回收未被标记为存活的对象,释放相应的内存空间。
    优点:
    实现简单,无需对象移动。
    缺点:
    清除后会产生大量内存碎片,当有大量碎片存在时,可能无法分配大对象,导致性能下降。
  21. 复制算法(Copying)
    描述:复制算法将内存划分为两块等大的区域,每次只使用其中一块。当内存用尽时,将存活的对象复制到另一块区域,并一次性清除当前使用的内存块。
    流程:
    将存活对象从“使用区”复制到“空闲区”,然后清空使用区中的所有对象。
    优点:
    整理后没有内存碎片,内存分配简单。
    缺点:
    需要两块相同大小的内存区域,内存利用率较低。
    应用:这种算法适合存活对象少、对象生命周期短的场景,如新生代的垃圾回收。
  22. 标记-整理算法(Mark-Compact)
    描述:标记-整理算法是标记-清除算法的改进,分为两个阶段:
    标记阶段:与标记-清除算法相同,标记所有存活对象。
    整理阶段:将所有存活的对象向内存一端移动,然后清除边界外的内存。
    优点:
    避免了内存碎片问题,保证内存连续分配。
    缺点:
    对象移动的成本较高,回收效率比标记-清除稍差。
    应用:这种算法适合在老年代使用,因为老年代的对象存活时间长,需要更多的内存整理来保持分配效率。
  23. 分代回收算法(Generational Garbage Collection)
    描述:分代回收是目前 JVM 中最常用的垃圾回收策略。它将堆内存划分为两部分:新生代和老年代,并根据对象的生命周期特点采用不同的垃圾回收算法。
    新生代(Young Generation):大多数对象在此被分配。由于大多数对象生命周期短暂,采用复制算法,高效回收大部分短命对象。新生代通常进一步划分为:
    Eden 区:新对象最初分配的区域。
    Survivor 区:当对象在 Eden 区存活后被移动到 Survivor 区。
    老年代(Old Generation):经过多次新生代垃圾回收后仍然存活的对象会被移动到老年代,采用标记-整理算法或标记-清除算法进行回收。老年代的对象存活时间较长,回收频率较低。
    优点:
    结合了不同算法的优势,根据对象的生命周期采用适合的算法,提高回收效率。
    新生代对象的生命周期短,使用复制算法快速回收;老年代对象存活时间长,采用标记-整理算法避免碎片化问题。
    计网 操作系统
    linux docker git 命令
    常用命令集合
    Linux 常用命令:
    文件操作:ls、cd、rm、cp
    进程管理:ps、kill、top
    网络管理:ping、ifconfig、netstat
    系统管理:df、du、free
    Docker 常用命令:
    镜像管理:docker pull、docker images、docker rmi
    容器管理:docker run、docker ps、docker stop、docker rm
    数据管理:docker volume、docker exec、docker logs
    网络管理:docker network
    git常用命令:
    基础操作:git init、git clone、git add、git commit、git status、git log
    分支管理:git branch、git checkout、git merge、git rebase、git switch
    远程仓库操作:git remote、git fetch、git pull、git push
    查看和撤销操作:git show、git reset、git revert、git stash
    git的使用流程:
    具体流程
    拉取最新develop分支 → 创建feature分支 → 开发和提交代码 → 提交合并请求 → 代码审查 → 合并到develop。
    develop分支测试通过 → 创建release分支 → 最终优化和测试 → 合并到main → 部署生产。
    出现紧急问题时:从main创建hotfix分支 → 修复 → 合并回main和develop。
    如何保证缓存一致性?
    保证缓存一致性需要根据具体的需求来定:
    1、对数据实时性有一定要求
    对数据实时性有一定要求即数据库数据更新需要近实时查询到最新的数据,针对这种情况可采用延迟双删、Canal+MQ异步同步的方式。
    2、对数据实时性要求不高
    使用定时任务的方式定时更新缓存。
    3、对数据实时性要求非常高
    此类场景不适合用缓存,直接使用数据库即可。
    注意:在使用缓存时不论采用哪种方式如果没有特殊要求一定要对key加过期时间,即使一段时间缓存不一致当缓存过期后最终数据是一致的。
    MQ有哪几种交换机模式,你们用的哪种
    Redis数据结构?
    那如果Redis宕机怎么办?
    介绍一个解决过的线上问题,是怎么定位问题?怎么解决?采取什么措施避免以后再发生?
    项目用了多少台机器?
    项目日活
    系统用户量:十万至数五十万左右
    系统日活:几百至几千之间
    QPS:数百至数千之间
    TPS:几十至几百之间
    RT:几毫秒至几百毫秒之间。
    幂等问题
    什么是幂等性问题
    用户对同一操作的一次或多次请求返回的结果是相同的,幂等问题多发生在修改和新增
    为什么会发生幂等性问题
    可能由于网络的问题,用户对一次操作发送了两次请求,比如用户注册,发送了两次,就注册了两次
    我对于幂等问题的看法,其实都是要去看看他有没有被执行过,而且只能执行一次。
    图片加载失败
    以订单为例:
    第一阶段:在进入到提交订单页面之前,需要订单系统根据用户信息向后端发起一个申请Token(可以是UUID)的请求,后端将Token保存到Redis缓存中,key为token,value是用户信息。第二阶段操作使用。
    第二阶段: 订单系统拿着申请到的token发起提交订单请求,后端会检查Redis中是否存在该Token, 如果存在, 表示第一次发起订单提交请求,开始逻辑处理,处理完逻辑后删除Redis中的Token 当有重复请求的时候,检查缓存中Token是否存在。不存在表示非法请求。
    如何保证MQ幂等性?或 如何防止重复消费?
    保证MQ幂等性通常是指保证消费者消费消息的幂等性。
    1、使用数据库的唯一约束去控制。
    比如:添加唯一索引保证添加数据的幂等性
    2、使用token机制
    发送消息时给消息指定一个唯一的ID
    发送消息时将消息ID写入Redis
    消费时根据消息ID查询Redis判断是否已经消费,如果已经消费则不再消费
    暂存问题
    1.分支(代码)管理
    1.1环境
    1.1.1异常管理
    1.1.2提交命令&提交规范
    2.项目开发情况
    2.1怎么进行代码测试
    2.2项目发布专项
    2.2.1你们怎么发布项目
    2.3发布失败你们遇到了吗?怎么处理的?
    2.4关联的下游服务版本回退,怎么处理的?
    3.故障排查专项
    3.1线上故障怎么排查
    1,先分析日志,通常在业务中都会有日志的记录,或者查看系统日志,或者查看日志文件,然后定位问题
    2,远程debug(通常公司的正式环境(生产环境)是不允许远程debug的。一般远程debug都是公司的测试环境,方便调试代码)
    3.1.1线上故障遇到哪些
    3.1.2线上故障怎么处理
    4.需求管理专项
    4.1你们一个需求从立项到落地是怎么实施的
    4.2你的技术方案设计一般都有哪些
    4.3你担任什么角色?主要做什么
    4.4项目日活专项
    纯八股(copy未整理)
    MySQL相关面试题
    面试官:MySQL中,如何定位慢查询?
    候选人:
    嗯~,我们当时做压测的时候有的接口非常的慢,接口的响应时间超过了2秒以上,因为我们当时的系统部署了运维的监控系统Skywalking ,在展示的报表中可以看到是哪一个接口比较慢,并且可以分析这个接口哪部分比较慢,这里可以看到SQL的具体的执行时间,所以可以定位是哪个sql出了问题
    如果,项目中没有这种运维的监控系统,其实在MySQL中也提供了慢日志查询的功能,可以在MySQL的系统配置文件中开启这个慢日志的功能,并且也可以设置SQL执行超过多少时间来记录到一个日志文件中,我记得上一个项目配置的是2秒,只要SQL执行的时间超过了2秒就会记录到日志文件中,我们就可以在日志文件找到执行比较慢的SQL了。
    面试官:那这个SQL语句执行很慢, 如何分析呢? 候选人:如果一条sql执行很慢的话,我们通常会使用mysql自动的执行计划explain来去查看这条sql的执行情况,比如在这里面可以通过key和key_len检查是否命中了索引,如果本身已经添加了索引,也可以判断索引是否有失效的情况,第二个,可以通过type字段查看sql是否有进一步的优化空间,是否存在全索引扫描或全盘扫描,第三个可以通过extra建议来判断,是否出现了回表的情况,如果出现了,可以尝试添加索引或修改返回字段来修复 面试官:了解过索引吗?(什么是索引) 候选人:嗯,索引在项目中还是比较常见的,它是帮助MySQL高效获取数据的数据结构,主要是用来提高数据检索的效率,降低数据库的IO成本,同时通过索引列对数据进行排序,降低数据排序的成本,也能降低了CPU的消耗 面试官:索引的底层数据结构了解过嘛 ? 候选人:MySQL的默认的存储引擎InnoDB采用的B+树的数据结构来存储索引,选择B+树的主要的原因是:第一阶数更多,路径更短,第二个磁盘读写代价B+树更低,非叶子节点只存储指针,叶子阶段存储数据,第三是B+树便于扫库和区间查询,叶子节点是一个双向链表 面试官:B树和B+树的区别是什么呢?
    候选人:第一:在B树中,非叶子节点和叶子节点都会存放数据,而B+树的所有的数据都会出现在叶子节点,在查询的时候,B+树查找效率更加稳定
    第二:在进行范围查询的时候,B+树效率更高,因为B+树都在叶子节点存储,并且叶子节点是一个双向链表
    面试官:什么是聚簇索引什么是非聚簇索引 ?
    候选人:
    好的~,聚簇索引主要是指数据与索引放到一块,B+树的叶子节点保存了整行数据,有且只有一个,一般情况下主键在作为聚簇索引的
    非聚簇索引值的是数据与索引分开存储,B+树的叶子节点保存对应的主键,可以有多个,一般我们自己定义的索引都是非聚簇索引
    面试官:知道什么是回表查询嘛 ? 候选人:嗯,其实跟刚才介绍的聚簇索引和非聚簇索引是有关系的,回表的意思就是通过二级索引找到对应的主键值,然后再通过主键值找到聚集索引中所对应的整行数据,这个过程就是回表
    【备注:如果面试官直接问回表,则需要先介绍聚簇索引和非聚簇索引】
    面试官:知道什么叫覆盖索引嘛 ? 候选人:嗯~,清楚的
    覆盖索引是指select查询语句使用了索引,在返回的列,必须在索引中全部能够找到,如果我们使用id查询,它会直接走聚集索引查询,一次索引扫描,直接返回数据,性能高。
    如果按照二级索引查询数据的时候,返回的列中没有创建索引,有可能会触发回表查询,尽量避免使用select *,尽量在返回的列中都包含添加索引的字段
    面试官:MYSQL超大分页怎么处理 ? 候选人:嗯,超大分页一般都是在数据量比较大时,我们使用了limit分页查询,并且需要对数据进行排序,这个时候效率就很低,我们可以采用覆盖索引和子查询来解决
    先分页查询数据的id字段,确定了id之后,再用子查询来过滤,只查询这个id列表中的数据就可以了
    因为查询id的时候,走的覆盖索引,所以效率可以提升很多
    面试官:索引创建原则有哪些? 候选人:嗯,这个情况有很多,不过都有一个大前提,就是表中的数据要超过10万以上,我们才会创建索引,并且添加索引的字段是查询比较频繁的字段,一般也是像作为查询条件,排序字段或分组的字段这些。
    还有就是,我们通常创建索引的时候都是使用复合索引来创建,一条sql的返回值,尽量使用覆盖索引,如果字段的区分度不高的话,我们也会把它放在组合索引后面的字段。
    如果某一个字段的内容较长,我们会考虑使用前缀索引来使用,当然并不是所有的字段都要添加索引,这个索引的数量也要控制,因为添加索引也会导致新增改的速度变慢。
    面试官:什么情况下索引会失效 ? 候选人:嗯,这个情况比较多,我说一些自己的经验,以前遇到过的
    比如,索引在使用的时候没有遵循最左匹配法则,第二个是,模糊查询,如果%号在前面也会导致索引失效。如果在添加索引的字段上进行了运算操作或者类型转换也都会导致索引失效。
    我们之前还遇到过一个就是,如果使用了复合索引,中间使用了范围查询,右边的条件索引也会失效
    所以,通常情况下,想要判断出这条sql是否有索引失效的情况,可以使用explain执行计划来分析
    面试官:sql的优化的经验 候选人:嗯,这个在项目还是挺常见的,当然如果直说sql优化的话,我们会从这几方面考虑,比如
    建表的时候、使用索引、sql语句的编写、主从复制,读写分离,还有一个是如果量比较大的话,可以考虑分库分表
    面试官:创建表的时候,你们是如何优化的呢? 候选人:这个我们主要参考的阿里出的那个开发手册《嵩山版》,就比如,在定义字段的时候需要结合字段的内容来选择合适的类型,如果是数值的话,像tinyint、int 、bigint这些类型,要根据实际情况选择。如果是字符串类型,也是结合存储的内容来选择char和varchar或者text类型 面试官:那在使用索引的时候,是如何优化呢? 候选人:选择适合作为索引的列。一般来说,选择经常被查询的列作为索引列可以提高查询性能, 我会根据业务需求和查询条件的复杂度来选择合适的组合索引。
    但也有避免过多索引:虽然索引可以提高查询性能,但是过多的索引可能会增加写操作的开销,并且会占用额外的存储空间。
    面试官:你平时对sql语句做了哪些优化呢? 候选人:嗯,这个也有很多,比如SELECT语句务必指明字段名称,不要直接使用select * ,还有就是要注意SQL语句避免造成索引失效的写法;如果是聚合查询,尽量用union all代替union ,union会多一次过滤,效率比较低;如果是表关联的话,尽量使用innerjoin ,不要使用用left join right join,如必须使用 一定要以小表为驱动 面试官:事务的特性是什么?可以详细说一下吗? 候选人:嗯,这个比较清楚,ACID,分别指的是:原子性、一致性、隔离性、持久性;我举个例子:
    A向B转账500,转账成功,A扣除500元,B增加500元,原子操作体现在要么都成功,要么都失败
    在转账的过程中,数据要一致,A扣除了500,B必须增加500
    在转账的过程中,隔离性体现在A像B转账,不能受其他事务干扰
    在转账的过程中,持久性体现在事务提交后,要把数据持久化(可以说是落盘操作)
    面试官:并发事务带来哪些问题?
    候选人:
    我们在项目开发中,多个事务并发进行是经常发生的,并发也是必然的,有可能导致一些问题
    第一是脏读, 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
    第二是不可重复读:比如在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
    第三是幻读(Phantom read):幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。
    面试官:怎么解决这些问题呢?MySQL的默认隔离级别是?
    候选人:解决方案是对事务进行隔离
    MySQL支持四种隔离级别,分别有:
    第一个是,未提交读(read uncommitted)它解决不了刚才提出的所有问题,一般项目中也不用这个。第二个是读已提交(read committed)它能解决脏读的问题的,但是解决不了不可重复读和幻读。第三个是可重复读(repeatable read)它能解决脏读和不可重复读,但是解决不了幻读,这个也是mysql默认的隔离级别。第四个是串行化(serializable)它可以解决刚才提出来的所有问题,但是由于让是事务串行执行的,性能比较低。所以,我们一般使用的都是mysql默认的隔离级别:可重复读
    面试官:undo log和redo log的区别
    候选人:好的,其中redo log日志记录的是数据页的物理变化,服务宕机可用来同步数据,而undo log 不同,它主要记录的是逻辑日志,当事务回滚时,通过逆操作恢复原来的数据,比如我们删除一条数据的时候,就会在undo log日志文件中新增一条delete语句,如果发生回滚就执行逆操作;
    redo log保证了事务的持久性,undo log保证了事务的原子性和一致性
    面试官:事务中的隔离性是如何保证的呢?(你解释一下MVCC)
    候选人:事务的隔离性是由锁和mvcc实现的。
    其中mvcc的意思是多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突,它的底层实现主要是分为了三个部分,第一个是隐藏字段,第二个是undo log日志,第三个是readView读视图
    隐藏字段是指:在mysql中给每个表都设置了隐藏字段,有一个是trx_id(事务id),记录每一次操作的事务id,是自增的;另一个字段是roll_pointer(回滚指针),指向上一个版本的事务版本记录地址
    undo log主要的作用是记录回滚日志,存储老版本数据,在内部会形成一个版本链,在多个事务并行操作某一行记录,记录不同事务修改数据的版本,通过roll_pointer指针形成一个链表
    readView解决的是一个事务查询选择版本的问题,在内部定义了一些匹配规则和当前的一些事务id判断该访问那个版本的数据,不同的隔离级别快照读是不一样的,最终的访问的结果不一样。如果是rc隔离级别,每一次执行快照读时生成ReadView,如果是rr隔离级别仅在事务中第一次执行快照读时生成ReadView,后续复用
    面试官:MySQL主从同步原理
    候选人:MySQL主从复制的核心就是二进制日志(DDL(数据定义语言)语句和 DML(数据操纵语言)语句),它的步骤是这样的:
    第一:主库在事务提交时,会把数据变更记录在二进制日志文件 Binlog 中。
    第二:从库读取主库的二进制日志文件 Binlog ,写入到从库的中继日志 Relay Log 。
    第三:从库重做中继日志中的事件,将改变反映它自己的数据
    面试官:你们项目用过MySQL的分库分表吗?
    候选人:
    嗯,因为我们都是微服务开发,每个微服务对应了一个数据库,是根据业务进行拆分的,这个其实就是垂直拆分。
    面试官:那你之前使用过水平分库吗?
    候选人:
    嗯,这个是使用过的,我们当时的业务是(xxx),一开始,我们也是单库,后来这个业务逐渐发展,业务量上来的很迅速,其中(xx)表已经存放了超过1000万的数据,我们做了很多优化也不好使,性能依然很慢,所以当时就使用了水平分库。
    我们一开始先做了3台服务器对应了3个数据库,由于库多了,需要分片,我们当时采用的mycat来作为数据库的中间件。数据都是按照id(自增)取模的方式来存取的。
    当然一开始的时候,那些旧数据,我们做了一些清洗的工作,我们也是按照id取模规则分别存储到了各个数据库中,好处就是可以让各个数据库分摊存储和读取的压力,解决了我们当时性能的问题
    Redis相关面试题
    面试官:什么是缓存穿透 ? 怎么解决 ?
    候选人:
    ,我想一下
    缓存穿透是指查询一个一定不存在的数据,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到 DB 去查询,可能导致 DB 挂掉。这种情况大概率是遭到了攻击。
    解决方案的话,我们通常都会用布隆过滤器来解决它
    面试官:好的,你能介绍一下布隆过滤器吗?
    候选人:
    嗯,是这样~
    布隆过滤器主要是用于检索一个元素是否在一个集合中。我们当时使用的是redisson实现的布隆过滤器。
    它的底层主要是先去初始化一个比较大数组,里面存放的二进制0或1。在一开始都是0,当一个key来了之后经过3次hash计算,模于数组长度找到数据的下标然后把数组中原来的0改为1,这样的话,三个数组的位置就能标明一个key的存在。查找的过程也是一样的。
    当然是有缺点的,布隆过滤器有可能会产生一定的误判,我们一般可以设置这个误判率,大概不会超过5%,其实这个误判是必然存在的,要不就得增加数组的长度,其实已经算是很划分了,5%以内的误判率一般的项目也能接受,不至于高并发下压倒数据库。
    面试官:什么是缓存击穿 ? 怎么解决 ?
    候选人:
    缓存击穿的意思是对于设置了过期时间的key,缓存在某个时间点过期的时候,恰好这时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端 DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把 DB 压垮。
    解决方案有两种方式:
    第一可以使用互斥锁:当缓存失效时,不立即去load db,先使用如 Redis 的 setnx 去设置一个互斥锁,当操作成功返回时再进行 load db的操作并回设缓存,否则重试get缓存的方法
    第二种方案可以设置当前key逻辑过期,大概是思路如下:
    ①:在设置key的时候,设置一个过期时间字段一块存入缓存中,不给当前key设置过期时间
    ②:当查询的时候,从redis取出数据后判断时间是否过期
    ③:如果过期则开通另外一个线程进行数据同步,当前线程正常返回数据,这个数据不是最新
    当然两种方案各有利弊:
    如果选择数据的强一致性,建议使用分布式锁的方案,性能上可能没那么高,锁需要等,也有可能产生死锁的问题
    如果选择key的逻辑删除,则优先考虑的高可用性,性能比较高,但是数据同步这块做不到强一致。
    面试官:什么是缓存雪崩 ? 怎么解决 ?
    候选人:
    嗯!!
    缓存雪崩意思是设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB 瞬时压力过重雪崩。与缓存击穿的区别:雪崩是很多key,击穿是某一个key缓存。
    解决方案主要是可以将缓存失效时间分散开,比如可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
    面试官:redis做为缓存,mysql的数据如何与redis进行同步呢?(双写一致性)
    候选人:嗯!就说我最近做的这个项目,对账模块里面包括佣金、费用等,这些信息可能需要频繁读取和更新。通过将结算信息缓存到 Redis 中,需要让数据库与redis高度保持一致,因为要求时效性比较高,我们当时采用的读写锁保证的强一致性。
    我们采用的是redisson实现的读写锁,在读的时候添加共享锁,可以保证读读不互斥,读写互斥。当我们更新数据的时候,添加排他锁,它是读写,读读都互斥,这样就能保证在写数据的同时是不会让其他线程读数据的,避免了脏数据。这里面需要注意的是读方法和写方法上需要使用同一把锁才行。
    面试官:那这个排他锁是如何保证读写、读读互斥的呢?
    候选人:其实排他锁底层使用也是setnx,保证了同时只能有一个线程操作锁住的方法
    面试官:你听说过延时双删吗?为什么不用它呢?
    候选人:延迟双删,如果是写操作,我们先把缓存中的数据删除,然后更新数据库,最后再延时删除缓存中的数据,其中这个延时多久不太好确定,在延时的过程中可能会出现脏数据,并不能保证强一致性,所以没有采用它。
    面试官:redis做为缓存,mysql的数据如何与redis进行同步呢?(双写一致性)
    候选人:嗯!就说我最近做的这个项目,对账模块有当保费支付信息发生变更时,如新增、修改或删除支付记录这些变更需要及时更新到对账系统中,以便实时统计保费收入和支出情况。数据同步可以有一定的延时(符合大部分业务)
    我们当时采用的阿里的canal组件实现数据同步:不需要更改业务代码,部署一个canal服务。canal服务把自己伪装成mysql的一个从节点,当mysql数据更新以后,canal会读取binlog数据,然后在通过canal的客户端获取到数据,更新缓存即可。
    面试官:redis做为缓存,数据的持久化是怎么做的?
    候选人:在Redis中提供了两种数据持久化的方式:1、RDB 2、AOF
    面试官:这两种持久化方式有什么区别呢?
    候选人:RDB是一个快照文件,它是把redis内存存储的数据写到磁盘上,当redis实例宕机恢复数据的时候,方便从RDB的快照文件中恢复数据。
    AOF的含义是追加文件,当redis操作写命令的时候,都会存储这个文件中,当redis实例宕机恢复数据的时候,会从这个文件中再次执行一遍命令来恢复数据
    面试官:这两种方式,哪种恢复的比较快呢?
    候选人:RDB因为是二进制文件,在保存的时候体积也是比较小的,它恢复的比较快,但是它有可能会丢数据,我们通常在项目中也会使用AOF来恢复数据,虽然AOF恢复的速度慢一些,但是它丢数据的风险要小很多,在AOF文件中可以设置刷盘策略,我们当时设置的就是每秒批量写入一次命令
    面试官:Redis的数据过期策略有哪些 ?
    候选人:
    嗯~,在redis中提供了两种数据过期删除策略
    第一种是惰性删除,在设置该key过期时间后,我们不去管它,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key。
    第二种是 定期删除,就是说每隔一段时间,我们就对一些key进行检查,删除里面过期的key
    定期清理的两种模式:
    SLOW模式是定时任务,执行频率默认为10hz,每次不超过25ms,以通过修改配置文件redis.conf 的 hz 选项来调整这个次数
    FAST模式执行频率不固定,每次事件循环会尝试执行,但两次间隔不低于2ms,每次耗时不超过1ms
    Redis的过期删除策略:惰性删除 + 定期删除两种策略进行配合使用。
    面试官:Redis的数据淘汰策略有哪些 ?
    候选人:
    嗯,这个在redis中提供了很多种,默认是noeviction,不删除任何数据,内部不足直接报错
    是可以在redis的配置文件中进行设置的,里面有两个非常重要的概念,一个是LRU,另外一个是LFU
    LRU的意思就是最少最近使用,用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。
    LFU的意思是最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高
    我们在项目设置的allkeys-lru,挑选最近最少使用的数据淘汰,把一些经常访问的key留在redis中
    面试官:数据库有1000万数据 ,Redis只能缓存20w数据, 如何保证Redis中的数据都是热点数据 ?
    候选人:
    嗯,我想一下

    可以使用 allkeys-lru (挑选最近最少使用的数据淘汰)淘汰策略,那留下来的都是经常访问的热点数据
    面试官:Redis的内存用完了会发生什么?
    候选人:
    嗯~,这个要看redis的数据淘汰策略是什么,如果是默认的配置,redis内存用完以后则直接报错。我们当时设置的 allkeys-lru 策略。把最近最常访问的数据留在缓存中。
    面试官:Redis分布式锁如何实现 ?
    候选人:嗯,在redis中提供了一个命令setnx(SET if not exists)
    由于redis的单线程的,用了命令之后,只能有一个客户端对某一个key设置值,在没有过期或删除key的时候是其他客户端是不能设置这个key的
    面试官:好的,那你如何控制Redis实现分布式锁有效时长呢?
    候选人:嗯,的确,redis的setnx指令不好控制这个问题,我们当时采用的redis的一个框架redisson实现的。
    在redisson中需要手动加锁,并且可以控制锁的失效时间和等待时间,当锁住的一个业务还没有执行完成的时候,在redisson中引入了一个看门狗机制,就是说每隔一段时间就检查当前业务是否还持有锁,如果持有就增加加锁的持有时间,当业务执行完成之后需要使用释放锁就可以了
    还有一个好处就是,在高并发下,一个业务有可能会执行很快,先客户1持有锁的时候,客户2来了以后并不会马上拒绝,它会自旋不断尝试获取锁,如果客户1释放之后,客户2就可以马上持有锁,性能也得到了提升。
    面试官:好的,redisson实现的分布式锁是可重入的吗?
    候选人:嗯,是可以重入的。这样做是为了避免死锁的产生。这个重入其实在内部就是判断是否是当前线程持有的锁,如果是当前线程持有的锁就会计数,如果释放锁就会在计算上减一。在存储数据的时候采用的hash结构,大key可以按照自己的业务进行定制,其中小key是当前线程的唯一标识,value是当前线程重入的次数
    面试官:redisson实现的分布式锁能解决主从一致性的问题吗
    候选人:这个是不能的,比如,当线程1加锁成功后,master节点数据会异步复制到slave节点,此时当前持有Redis锁的master节点宕机,slave节点被提升为新的master节点,假如现在来了一个线程2,再次加锁,会在新的master节点上加锁成功,这个时候就会出现两个节点同时持有一把锁的问题。
    我们可以利用redisson提供的红锁来解决这个问题,它的主要作用是,不能只在一个redis实例上创建锁,应该是在多个redis实例上创建锁,并且要求在大多数redis节点上都成功创建锁,红锁中要求是redis的节点数量要过半。这样就能避免线程1加锁成功后master节点宕机导致线程2成功加锁到新的master节点上的问题了。
    但是,如果使用了红锁,因为需要同时在多个节点上都添加锁,性能就变的很低了,并且运维维护成本也非常高,所以,我们一般在项目中也不会直接使用红锁,并且官方也暂时废弃了这个红锁
    面试官:好的,如果业务非要保证数据的强一致性,这个该怎么解决呢?
    候选人:**嗯~,redis本身就是支持高可用的,做到强一致性,就非常影响性能,所以,如果有强一致性要求高的业务,建议使用zookeeper实现的分布式锁,它是可以保证强一致性的。
    面试官:Redis集群有哪些方案, 知道嘛 ?
    候选人:嗯,在Redis中提供的集群方案总共有三种:主从复制、哨兵模式、Redis分片集群
    面试官:那你来介绍一下主从同步
    候选人:嗯,是这样的,单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,可以搭建主从集群,实现读写分离。一般都是一主多从,主节点负责写数据,从节点负责读数据,主节点写入数据之后,需要把数据同步到从节点中
    面试官:能说一下,主从同步数据的流程
    候选人:嗯
    ,好!主从同步分为了两个阶段,一个是全量同步,一个是增量同步
    全量同步是指从节点第一次与主节点建立连接的时候使用全量同步,流程是这样的:
    第一:从节点请求主节点同步数据,其中从节点会携带自己的replication id和offset偏移量。
    第二:主节点判断是否是第一次请求,主要判断的依据就是,主节点与从节点是否是同一个replication id,如果不是,就说明是第一次同步,那主节点就会把自己的replication id和offset发送给从节点,让从节点与主节点的信息保持一致。
    第三:在同时主节点会执行bgsave,生成rdb文件后,发送给从节点去执行,从节点先把自己的数据清空,然后执行主节点发送过来的rdb文件,这样就保持了一致
    当然,如果在rdb生成执行期间,依然有请求到了主节点,而主节点会以命令的方式记录到缓冲区,缓冲区是一个日志文件,最后把这个日志文件发送给从节点,这样就能保证主节点与从节点完全一致了,后期再同步数据的时候,都是依赖于这个日志文件,这个就是全量同步
    增量同步指的是,当从节点服务重启之后,数据就不一致了,所以这个时候,从节点会请求主节点同步数据,主节点还是判断不是第一次请求,不是第一次就获取从节点的offset值,然后主节点从命令日志中获取offset值之后的数据,发送给从节点进行数据同步
    面试官:怎么保证Redis的高并发高可用
    候选人:首先可以搭建主从集群,再加上使用redis中的哨兵模式,哨兵模式可以实现主从集群的自动故障恢复,里面就包含了对主从服务的监控、自动故障恢复、通知;如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主;同时Sentinel也充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端,所以一般项目都会采用哨兵的模式来保证redis的高并发高可用
    面试官:你们使用redis是单点还是集群,哪种集群
    候选人:嗯!,我们当时使用的是主从(1主1从)加哨兵。一般单节点不超过10G内存,如果Redis内存不足则可以给不同服务分配独立的Redis主从节点。尽量不做分片集群。因为集群维护起来比较麻烦,并且集群之间的心跳检测和数据通信会消耗大量的网络带宽,也没有办法使用lua脚本和事务
    面试官:redis集群脑裂,该怎么解决呢?
    候选人:嗯! 这个在项目很少见,不过脑裂的问题是这样的,我们现在用的是redis的哨兵模式集群的
    有的时候由于网络等原因可能会出现脑裂的情况,就是说,由于redis master节点和redis salve节点和sentinel处于不同的网络分区,使得sentinel没有能够心跳感知到master,所以通过选举的方式提升了一个salve为master,这样就存在了两个master,就像大脑分裂了一样,这样会导致客户端还在old master那里写入数据,新节点无法同步数据,当网络恢复后,sentinel会将old master降为salve,这时再从新master同步数据,这会导致old master中的大量数据丢失。
    关于解决的话,我记得在redis的配置中可以设置:第一可以设置最少的salve节点个数,比如设置至少要有一个从节点才能同步数据,第二个可以设置主从数据复制和同步的延迟时间,达不到要求就拒绝请求,就可以避免大量的数据丢失
    面试官:redis的分片集群有什么作用
    候选人:分片集群主要解决的是,海量数据存储的问题,集群中有多个master,每个master保存不同数据,并且还可以给每个master设置多个slave节点,就可以继续增大集群的高并发能力。同时每个master之间通过ping监测彼此健康状态,就类似于哨兵模式了。当客户端请求可以访问集群任意节点,最终都会被转发到正确节点
    面试官:Redis分片集群中数据是怎么存储和读取的?
    候选人:
    嗯~,在redis集群中是这样的
    Redis 集群引入了哈希槽的概念,有 16384 个哈希槽,集群中每个主节点绑定了一定范围的哈希槽范围, key通过 CRC16 校验后对 16384 取模来决定放置哪个槽,通过槽找到对应的节点进行存储。
    取值的逻辑是一样的
    面试官:Redis是单线程的,但是为什么还那么快?
    候选人:
    嗯,这个有几个原因吧~
    1、完全基于内存的,C语言编写
    2、采用单线程,避免不必要的上下文切换可竞争条件
    3、使用多路I/O复用模型,非阻塞IO
    例如:bgsave 和 bgrewriteaof 都是在后台执行操作,不影响主线程的正常使用,不会产生阻塞
    面试官:能解释一下I/O多路复用模型?
    候选人:嗯
    ,I/O多路复用是指利用单个线程来同时监听多个Socket ,并在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。目前的I/O多路复用都是采用的epoll模式实现,它会在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间,不需要挨个遍历Socket来判断是否就绪,提升了性能。
    其中Redis的网络模型就是使用I/O多路复用结合事件的处理器来应对多个Socket请求,比如,提供了连接应答处理器、命令回复处理器,命令请求处理器;
    在Redis6.0之后,为了提升更好的性能,在命令回复处理器使用了多线程来处理回复事件,在命令请求处理器中,将命令的转换使用了多线程,增加命令转换速度,在命令执行的时候,依然是单线程
    框架篇面试题(Spring, Mybatis)
    面试官:Spring框架中的单例bean是线程安全的吗?
    候选人:
    不是线程安全的,是这样的
    当多用户同时请求一个服务时,容器会给每一个请求分配一个线程,这是多个线程会并发执行该请求对应的业务逻辑(成员方法),如果该处理逻辑中有对该单列状态的修改(体现为该单例的成员属性),则必须考虑线程同步问题。
    Spring框架并没有对单例bean进行任何多线程的封装处理。关于单例bean的线程安全和并发问题需要开发者自行去搞定。
    比如:我们通常在项目中使用的Spring bean都是不可可变的状态(比如Service类和DAO类),所以在某种程度上说Spring的单例bean是线程安全的。
    如果你的bean有多种状态的话(比如 View Model对象),就需要自行保证线程安全。最浅显的解决办法就是将多态bean的作用由“singleton”变更为“prototype”。
    面试官:什么是AOP
    候选人:
    aop是面向切面编程,在spring中用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取公共模块复用,降低耦合,一般比如可以做为公共日志保存,事务处理等
    面试官:你们项目中有没有使用到AOP
    候选人:
    我们当时在后台管理系统中,就是使用aop来记录了系统的操作日志
    主要思路是这样的,使用aop中的环绕通知+切点表达式,这个表达式就是要找到要记录日志的方法,然后通过环绕通知的参数获取请求方法的参数,比如类信息、方法信息、注解、请求方式等,获取到这些参数以后,保存到数据库
    面试官:Spring中的事务是如何实现的
    候选人:
    spring实现的事务本质就是aop完成,对方法前后进行拦截,在执行方法之前开启事务,在执行完目标方法之后根据执行情况提交或者回滚事务。
    面试官:Spring中事务失效的场景有哪些
    候选人:
    嗯!这个在项目中之前遇到过,我想想啊
    第一个,如果方法上异常捕获处理,自己处理了异常,没有抛出,就会导致事务失效,所以一般处理了异常以后,别忘了跑出去就行了
    第二个,如果方法抛出检查异常,如果报错也会导致事务失效,最后在spring事务的注解上,就是@Transactional上配置rollbackFor属性为Exception,这样别管是什么异常,都会回滚事务
    第三,我之前还遇到过一个,如果方法上不是public修饰的,也会导致事务失效
    嗯,就能想起来那么多
    面试官:Spring的bean的生命周期
    候选人:
    嗯!,这个步骤还是挺多的,我之前看过一些源码,它大概流程是这样的
    首先会通过一个非常重要的类,叫做BeanDefinition获取bean的定义信息,这里面就封装了bean的所有信息,比如,类的全路径,是否是延迟加载,是否是单例等等这些信息
    在创建bean的时候,第一步是调用构造函数实例化bean
    第二步是bean的依赖注入,比如一些set方法注入,像平时开发用的@Autowire都是这一步完成
    第三步是处理Aware接口,如果某一个bean实现了Aware接口就会重写方法执行
    第四步是bean的后置处理器BeanPostProcessor,这个是前置处理器
    第五步是初始化方法,比如实现了接口InitializingBean或者自定义了方法init-method标签或@PostContruct
    第六步是执行了bean的后置处理器BeanPostProcessor,主要是对bean进行增强,有可能在这里产生代理对象
    最后一步是销毁bean
    面试官:Spring中的循环引用
    候选人:
    嗯,好的,我来解释一下
    循环依赖:循环依赖其实就是循环引用,也就是两个或两个以上的bean互相持有对方,最终形成闭环。比如A依赖于B,B依赖于A
    循环依赖在spring中是允许存在,spring框架依据三级缓存已经解决了大部分的循环依赖
    ①一级缓存:单例池,缓存已经经历了完整的生命周期,已经初始化完成的bean对象
    ②二级缓存:缓存早期的bean对象(生命周期还没走完)
    ③三级缓存:缓存的是ObjectFactory,表示对象工厂,用来创建某个对象的
    面试官:那具体解决流程清楚吗?
    候选人:
    第一,先实例A对象,同时会创建ObjectFactory对象存入三级缓存singletonFactories
    第二,A在初始化的时候需要B对象,这个走B的创建的逻辑
    第三,B实例化完成,也会创建ObjectFactory对象存入三级缓存singletonFactories
    第四,B需要注入A,通过三级缓存中获取ObjectFactory来生成一个A的对象同时存入二级缓存,这个是有两种情况,一个是可能是A的普通对象,另外一个是A的代理对象,都可以让ObjectFactory来生产对应的对象,这也是三级缓存的关键
    第五,B通过从通过二级缓存earlySingletonObjects 获得到A的对象后可以正常注入,B创建成功,存入一级缓存singletonObjects
    第六,回到A对象初始化,因为B对象已经创建完成,则可以直接注入B,A创建成功存入一次缓存singletonObjects
    第七,二级缓存中的临时对象A清除
    面试官:构造方法出现了循环依赖怎么解决?
    候选人:
    由于bean的生命周期中构造函数是第一个执行的,spring框架并不能解决构造函数的的依赖注入,可以使用@Lazy懒加载,什么时候需要对象再进行bean对象的创建
    面试官:SpringMVC的执行流程知道嘛
    候选人:
    嗯,这个知道的,它分了好多步骤
    1、用户发送出请求到前端控制器DispatcherServlet,这是一个调度中心
    2、DispatcherServlet收到请求调用HandlerMapping(处理器映射器)。
    3、HandlerMapping找到具体的处理器(可查找xml配置或注解配置),生成处理器对象及处理器拦截器(如果有),再一起返回给DispatcherServlet。
    4、DispatcherServlet调用HandlerAdapter(处理器适配器)。
    5、HandlerAdapter经过适配调用具体的处理器(Handler/Controller)。
    6、Controller执行完成返回ModelAndView对象。
    7、HandlerAdapter将Controller执行结果ModelAndView返回给DispatcherServlet。
    8、DispatcherServlet将ModelAndView传给ViewReslover(视图解析器)。
    9、ViewReslover解析后返回具体View(视图)。
    10、DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。
    11、DispatcherServlet响应用户。
    当然现在的开发,基本都是前后端分离的开发的,并没有视图这些,一般都是handler中使用Response直接结果返回
    面试官:Springboot自动配置原理
    候选人:
    嗯,好的,它是这样的。
    在Spring Boot项目中的引导类上有一个注解@SpringBootApplication,这个注解是对三个注解进行了封装,分别是:
    @SpringBootConfiguration
    @EnableAutoConfiguration
    @ComponentScan
    其中@EnableAutoConfiguration是实现自动化配置的核心注解。
    该注解通过@Import注解导入对应的配置选择器。关键的是内部就是读取了该项目和该项目引用的Jar包的的classpath路径下META-INF/spring.factories文件中的所配置的类的全类名。
    在这些配置类中所定义的Bean会根据条件注解所指定的条件来决定是否需要将其导入到Spring容器中。
    一般条件判断会有像@ConditionalOnClass这样的注解,判断是否有对应的class文件,如果有则加载该类,把这个配置类的所有的Bean放入spring容器中使用。
    面试官:Spring 的常见注解有哪些?
    候选人:
    嗯,这个就很多了
    第一类是:声明bean,有@Component、@Service、@Repository、@Controller
    第二类是:依赖注入相关的,有@Autowired、@Qualifier、@Resourse
    第三类是:设置作用域 @Scope
    第四类是:spring配置相关的,比如@Configuration,@ComponentScan 和 @Bean
    第五类是:跟aop相关做增强的注解 @Aspect,@Before,@After,@Around,@Pointcut
    面试官:SpringMVC常见的注解有哪些?
    候选人:
    嗯,这个也很多的
    有@RequestMapping:用于映射请求路径;
    @RequestBody:注解实现接收http请求的json数据,将json转换为java对象;
    @RequestParam:指定请求参数的名称;
    @PathViriable:从请求路径下中获取请求参数(/user/{id}),传递给方法的形式参数;@ResponseBody:注解实现将controller方法返回对象转化为json对象响应给客户端。@RequestHeader:获取指定的请求头数据,还有像@PostMapping、@GetMapping这些。
    面试官:Springboot常见注解有哪些?
    候选人:
    嗯~~
    Spring Boot的核心注解是@SpringBootApplication , 他由几个注解组成 :
    @SpringBootConfiguration: 组合了- @Configuration注解,实现配置文件的功能;
    @EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项
    @ComponentScan:Spring组件扫描
    面试官:MyBatis执行流程
    候选人:
    好,这个知道的,不过步骤也很多
    ①读取MyBatis配置文件:mybatis-config.xml加载运行环境和映射文件
    ②构造会话工厂SqlSessionFactory,一个项目只需要一个,单例的,一般由spring进行管理
    ③会话工厂创建SqlSession对象,这里面就含了执行SQL语句的所有方法
    ④操作数据库的接口,Executor执行器,同时负责查询缓存的维护
    ⑤Executor接口的执行方法中有一个MappedStatement类型的参数,封装了映射信息
    ⑥输入参数映射
    ⑦输出结果映射
    面试官:Mybatis是否支持延迟加载?
    候选人:
    是支持的~
    延迟加载的意思是:就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据。
    Mybatis支持一对一关联对象和一对多关联集合对象的延迟加载
    在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false,默认是关闭的
    面试官:延迟加载的底层原理知道吗?
    候选人:
    嗯,我想想啊
    延迟加载在底层主要使用的CGLIB动态代理完成的
    第一是,使用CGLIB创建目标对象的代理对象,这里的目标对象就是开启了延迟加载的mapper
    第二个是当调用目标方法时,进入拦截器invoke方法,发现目标方法是null值,再执行sql查询
    第三个是获取数据以后,调用set方法设置属性值,再继续查询目标方法,就有值了
    面试官:Mybatis的一级、二级缓存用过吗?
    候选人:
    嗯~~,用过的~
    mybatis的一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当Session进行flush或close之后,该Session中的所有Cache就将清空,默认打开一级缓存
    关于二级缓存需要单独开启
    二级缓存是基于namespace和mapper的作用域起作用的,不是依赖于SQL session,默认也是采用 PerpetualCache,HashMap 存储。
    如果想要开启二级缓存需要在全局配置文件和映射文件中开启配置才行。
    面试官:Mybatis的二级缓存什么时候会清理缓存中的数据
    候选人:
    当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了新增、修改、删除操作后,默认该作用域下所有 select 中的缓存将被 clear。
相关文章
|
2月前
|
数据采集 人工智能 自然语言处理
2025数字人竞争力榜单发布:实时交互数字人全面进化
在数字经济迅速发展的背景下,2025年中国数字人企业的崛起为各行业带来了新的机遇与挑战。本文将深入分析不同数字人企业的特点与全栈技术的应用,提供选型指南,帮助企业识别合适的合作伙伴,从而提升市场竞争力,实现数字化转型与创新发展。
123 8
|
28天前
|
人工智能 算法 测试技术
人工智能测试工程师,需要掌握哪些真正「能落地」的技能?
AI时代,测试工程师正面临能力重构。AI未取代测试,却重塑其核心:从验证功能到保障不确定系统的稳定性与可信性。真正的AI测试需具备三层能力:理解模型逻辑、以数据驱动测试设计、构建智能化自动化体系。转型关键不在知识碎片,而在工程闭环实践。未来属于能让AI系统可靠落地的测试人。
|
4月前
|
人工智能 编解码 自然语言处理
2025年数字人平台如何选?这份排名与推荐指南帮你精准定位
在2025年数字人爆发之际,必火AI凭借全链路智能创作平台脱颖而出。其以极速克隆、AI文案、智能剪辑三大引擎,实现从素材到成片的一站式生成,支持4K超清、40语种、情感化音色,大幅降低制作门槛与成本。评测显示,该平台在效率、质量与易用性上全面领先,广泛适用于短视频营销、个人IP、企业培训及跨境出海等场景,成为企业与创作者数字化转型的优选工具。
647 4
|
2月前
|
人工智能 自然语言处理 文字识别
别再手动对账了!rpa财务机器人软件如何实现“易用、实用、好用”?
RPA财务机器人软件正重塑财务工作,通过自动化处理重复、规则明确的任务,如对账、报税、报销审核等,大幅提升效率与准确性。它非物理机器人,而是一套模拟人工操作的程序,可7×24小时运行,助力财务从“手工时代”迈向“智能时代”。尤其适合高频、稳定、标准化流程,已成为企业降本增效的核心工具。
224 1
|
9月前
|
人工智能 搜索推荐 程序员
程序员圈爆火,狂揽2.4K星!1秒内AI语音双向对话,支持个性化发音和多端适配,颠覆你的交互想象!
RealtimeVoiceChat是一款基于现代Web技术的开源实时语音对话工具,无需下载任何软件,打开浏览器即可与AI实时语音互动。其核心亮点包括零安装体验、超低延迟、高度可定制化以及跨平台兼容等特性。通过Web Speech API实现毫秒级语音合成,支持多参数精细控制(如音色、语速、音调等),并提供隐私安全保障。项目适用于无障碍辅助、语言学习、智能客服及内容创作等多个场景。开发者可快速集成GPT/Claude等大模型,扩展为企业级应用。此外,随着Web Speech API普及率提升,该项目有望推动语音交互在教育、智能家居等领域的发展
937 4
|
存储 算法
Leetcode第三题(无重复字符的最长子串)
这篇文章介绍了解决LeetCode第三题“无重复字符的最长子串”的算法,使用滑动窗口技术来找出给定字符串中最长的不含重复字符的子串,并提供了详细的代码实现和解释。
651 0
Leetcode第三题(无重复字符的最长子串)
|
存储 关系型数据库 MySQL
“COUNT(*) MyISAM比InnoDB更快”是误解
在我印象中,MyISAM的查询速度比InnoDB快,但根据MySQL官网文章,从5.7版本开始,InnoDB性能大幅提升,在8.0中持续优化。InnoDB提供更好的性能、可靠性和可扩展性,支持ACID事务、行级锁定、崩溃恢复等特性,成为现代应用的默认选择。尤其在高可用性和灾难恢复方面,InnoDB是唯一选择。云服务也普遍不支持MyISAM。因此,建议使用MyISAM的用户尽早迁移到InnoDB以获得更佳性能和可靠性。
262 11
|
缓存 Java 测试技术
谷粒商城笔记+踩坑(11)——性能压测和调优,JMeter压力测试+jvisualvm监控性能+资源动静分离+修改堆内存
使用JMeter对项目各个接口进行压力测试,并对前端进行动静分离优化,优化三级分类查询接口的性能
737 10
谷粒商城笔记+踩坑(11)——性能压测和调优,JMeter压力测试+jvisualvm监控性能+资源动静分离+修改堆内存
|
数据采集 存储 JavaScript
(2024)豆瓣电影TOP250爬虫详细讲解和代码
这是一个关于如何用Python爬取2024年豆瓣电影Top250的详细教程。教程涵盖了生成分页URL列表和解析页面以获取电影信息的函数。`getAllPageUrl()` 生成前10页的链接,而`getMoiveListByUrl()` 使用PyQuery解析HTML,提取电影标题、封面、评价数和评分。代码示例展示了测试这些函数的方法,输出包括电影详情的字典列表。
1462 3
|
监控 供应链 安全
构建高效微服务架构:API网关与服务熔断策略
【7月更文挑战第38天】随着现代应用程序向微服务架构的转型,系统的稳定性和效率成为了开发团队关注的焦点。本文将探讨在微服务环境中实现系统可靠性的关键组件——API网关,以及如何在服务间通讯时采用熔断机制来防止故障蔓延。通过分析API网关的核心功能和设计原则,并结合熔断策略的最佳实践,我们旨在提供一套提高分布式系统弹性的策略。

热门文章

最新文章