# 面试题汇总 **Repository Path**: bottomhuang/summary-of-interview-questions ## Basic Information - **Project Name**: 面试题汇总 - **Description**: 面试资料 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2024-06-24 - **Last Updated**: 2024-07-20 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 一、登录模块 ## 登录的时候为什么异步消息kafka处理用户初始化 这个是我们的业务操作,如果用同步处理导致登录等待时间过长,会影响用户体验 ## kafka怎么保证消息不丢失的 **副本机制(Replication)**:Kafka的每个分区(Partition)都有多个副本(Replica)。其中一个副本是主副本(Leader),其余的是从副本(Follower)。当生产者发送消息时,消息首先写入主副本,然后由主副本同步到从副本。如果主副本出现故障,一个从副本可以被提升为新的主副本。 **确认机制(Acknowledgement)**:生产者在发送消息时可以指定不同的确认级别(acks)。有三种确认级别: - `acks=0`:生产者不等待任何确认,消息发送后即认为成功。这种方式效率最高但可靠性最低。 - `acks=1`:生产者等待主副本确认消息已收到,这种方式在主副本故障时可能会丢失数据。 - `acks=all`(或`acks=-1`):生产者等待所有副本都确认收到消息,这种方式可靠性最高,但延迟也最大。 **ISR机制(In-Sync Replicas)**:Kafka维护一个同步副本集(ISR),其中包含所有与主副本保持同步的从副本。只有在所有ISR中的副本都确认收到消息后,才会向生产者确认消息写入成功。 **消息持久化(Durability)**:Kafka在写入消息之前会将其写入磁盘日志,并且在消息写入日志后才会确认消息的写入。这种方式确保即使服务器重启或崩溃,消息仍然可以从磁盘恢复。 **生产者重试机制(Producer Retries)**:生产者可以配置重试次数(retries)和重试间隔(retry.backoff.ms)。如果消息发送失败,生产者会自动进行重试,以确保消息最终能够成功发送。 **事务(Transactions)**:Kafka支持事务,可以保证一组消息的原子性。即要么这组消息全部成功,要么全部失败。这对于需要保证消息顺序和一致性的场景非常有用。 ![image-2.png](assets/image-2.png) `bootstrap-servers`:Kafka集群的地址,格式为`host:port`。生产者和消费者都会通过这个地址连接到Kafka集群。 `retries: 3`:生产者在发送消息失败时重试的次数,设置为3次。如果消息发送失败,生产者会自动重试,以提高消息发送的可靠性。 `acks: 1`:生产者在收到主副本确认后认为消息发送成功。这种方式在主副本故障时可能会丢失数据,但性能较高。 `batch-size: 16384`:生产者批量发送消息的大小,单位是字节。生产者会尝试将多条消息放入一个批次中,以提高吞吐量。 `buffer-memory: 33554432`:生产者用于缓存消息的总内存大小,单位是字节。如果缓冲区满了,生产者会阻塞或抛出异常。 `key-serializer` 和 `value-serializer`:指定消息键和值的序列化器,这里使用的是字符串序列化器。 `group-id: service-album`:消费者组的ID,Kafka通过消费者组管理消息的消费。属于同一组的消费者协调分配分区,以实现负载均衡和容错。 `enable-auto-commit: true`:消费者在处理完消息后自动提交偏移量。这种方式简单但不够灵活,如果消息处理失败可能会丢失数据。 `auto-offset-reset: earliest`:当消费者组首次启动或找不到当前偏移量时,从分区的最早位置开始消费消息。 `key-deserializer` 和 `value-deserializer`:指定消息键和值的反序列化器,这里使用的是字符串反序列化器。 ## 登录的时候你是怎么获取请求上下文的? **RequestContextHolder**是Spring框架提供的一个用于管理请求上下文的工具类 ## threadlocad 里面是数据在不同微服务模块是可以共享的吗,那把用户ID放入 threadlocal 有什么作用呢 不可以,ThreadLocal为每个线程提供了独立的变量副本,使得每个线程都可以独立地操作自己的变量副本,而不会影响其他线程。 一个用户的一个请求就有一个threadlocad对象,不同服务线程不可以共享,可以通过重新登陆拦截放入该模块的threadlocal ## 登录的时候你存入redis的token的键和值分别是什么 - key RedisConstant.USER_LOGIN_KEY_PREFIX + token user:login:15157318448451 - values UserInfoVo 用户id,微信openId,nickname,主播用户头像图片,用户是否为VIP会员 0:普通用户 1:VIP会员,当前VIP到期时间,即失效时间 ## 登录注解 @GuiguLogin 里面设置了true 和false分别是什么 - true:认证拦截 创作者中心,即上传声音界面需要用户登录 - false:认证不拦截 专辑展示界面,我们可以让用户免费试听,即没登录也可以试听前几个声音,此时使用登录注解只是获取用户ID,如果有者可以判断是否有声音观看权限 ## 登录aop里面为什么要使用环绕通知 - 因为前置通知时我们要获取用户请求的上下文 - 且方法结束后的时候要将ThreaLocal中数据手动清理避免OOM出现 ## 为什么选择注解+aop实现登录拦截 因为在项目中的比如首页的声音有些是不需要登录就可以看的,而在作者中心用户可以上传声音这些功能是需要登录才可以的,因此在我们的拦截规则是具体到方法上的,网关层一般都是拦截某一模块,过滤器,拦截器这些也可以实现登录拦截,都不符合我们项目,我们拦截的细粒度更低,为方法级别,因此选择aop+注解实 ## 那为什么不选择springscurity实现登录拦截 springscurity是一个比较重量级的安全权限框架,适合大型项目且配置繁多,aop+注解实力登录拦截比较轻量级,且我们可以自定义拦截规则 ## 单点登录是怎么实现的 token + redis ## 登录的过程涉及表是什么?字段是什么 user_info id wx_open_id nickname avatar_url (头像地址) is_vip vip_expire_time gender 性别 birthday certification_type(主播认证类型) certification_status(认证状态) ## 你是怎么使用openfegin远程调用的?步骤是什么 1.添加依赖 : 导入openfein pom文件 2.启用openFeign :在Spring Boot应用的主类上添加`@EnableFeignClients`注解来启用OpenFeign 3.定义Feign接口:使用`@FeignClient`注解来标识这是一个Feign客户端 ```java @FeignClient(name = "服务名", url = "服务URL(可选)") public interface MyFeignClient { // 定义HTTP服务接口的方法 @GetMapping("/endpoint") String sendMessage(); } ``` 4.使用Feign接口 :,通过`@Autowired`注解注入Feign客户端接口 ```java @Autowired private MyFeignClient myFeignClient; ``` 原理:OpenFeign是一个声明式RESTful网络请求客户端,通过动态代理技术、注解解析、请求构建与发送、响应解码等步骤,实现了对远程HTTP服务的声明式调用 OpenFeign是一个基于Java的HTTP客户端框架,底层实现的原理主要是利用**Java的反射机制和动态代理技术**。在使用OpenFeign时,用户只需要定义接口,并通过注解的方式描述HTTP请求的信息,然后OpenFeign会根据这些接口和注解信息动态生成代理类,最终实现对HTTP请求的调用。 具体来讲,OpenFeign会根据用户定义的接口和注解信息生成**一个动态代理类**,该代理类会在方法调用时根据注解信息构建HTTP**请求并发送到对应的服务端**。OpenFeign还支持负载均衡和服务发现等功能,可以通过配置不同的负载均衡策略和服务发现机制来实现对服务端的调用。 # 二、专辑详情优化模块 ## 为什么要实现缓存注解,实现步骤是什么? 专辑详情中多项数据接口需要查询缓存,那么分布式锁的业务逻辑代码就会出现大量的重复,因此用缓存注解 key prefix+paramVal(参数) album:1:00 lockKey(锁的key) prefix+paramVal+":lock" 1.定义注解--标记作用 2.定义切面类 guiGuCache 3.编写切点表达式 @Around("@annotation(guiGuCache)") 带有@guiGuCache注解都能直接拦截 4.编写实现业务--缓存注解 大致就是获取参数 用注解前缀+参数作为key去查询redis,有则直接返回 没有先用redisson获取lock解决缓存击穿问题,二次查询redis(因为如果redisson是阻塞获取锁的,这期间可能redis更新了),没有则去mysql查询,如有有数据则直接缓存,没有则要通过放射构造空对象存入(直接存入null 会导致逻辑异常直接查询数据库,缓存就失去作用了),最后设置兜底方案去查询myql(防止redis挂掉) ## redisson怎么实现分布式锁的 ``` RLock lock = redissonClient.getLock(lockKey); lock.lock(); lock.unlock(); ``` 第一次会加锁,是通过setnx判断key存不存在,第二次访问只有等第一个锁释放才能再次获得锁 ## redissson的看门狗机制是什么 Redisson 的看门狗机制(Watchdog)是一种用于自动延长分布式锁过期时间的机制。它可以防止因为意外情况导致锁被提前释放的问题,由于看门狗机制的存在,Redisson 会自动延长锁的过期时间,使得锁在持有期间不会被释放,从而保证了长时间操作的安全性。 **看门狗机制的工作原理** 1. **获取锁时启动看门狗**: 当一个客户端获取到分布式锁时,Redisson 会启动一个看门狗(Watchdog)线程。这个线程会定期检查锁的持有状态。 2. **自动延长锁的过期时间**: 看门狗线程每隔一段时间(默认是锁的有效期的 1/3)会自动为锁延长过期时间。默认的锁过期时间是 30 秒,但看门狗会在 10 秒(30 秒的 1/3)后自动将锁的过期时间延长到 30 秒,以确保锁在客户端持有期间不会被其他客户端获取。 3. **释放锁时停止看门狗**: 当客户端显式释放锁时,看门狗线程会停止运行,不再延长锁的过期时间。 ## 怎么用定时任务更新redis排行榜,vip到期时间?步骤是什么? 1. 下载 XXL-JOB 源码 :从 [GitHub 仓库](https://github.com/xuxueli/xxl-job) 下载 XXL-JOB 的源码或直接下载编译好的包。 2. 配置数据库 :在数据库中创建 XXL-JOB 所需的表,可以使用 `doc/db/tables_xxl_job.sql` 脚本来初始化数据库。 3. 启动 XXL-JOB 管理平台 :部署到服务器上 4. 添加执行器 : 在 XXL-JOB 管理平台上,添加新的执行器,配置好执行器的基本信息。 5. 编写 Job 代码 : 在你的项目中添加 XXL-JOB 的依赖 6. 执行器配置 : ```java # log config xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin xxl.job.accessToken=default_token xxl.job.executor.appname=xxl-job-executor-sample xxl.job.executor.address= xxl.job.executor.ip= xxl.job.executor.port=9999 xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler xxl.job.executor.logretentiondays=30 ``` 7. 执行器组件配置: 注入一个配置类XxlJobConfig,获取xxl-job账号密码等信息 8. 实现业务 ```java /** * 定时执行热门专辑更新 */ @XxlJob("updateHotAlbumJob") public void updateHotAlbumJob() { log.info("定时执行热门专辑更新"); searchFeignClient.updateLatelyAlbumRanking(); } ``` 9. 配置任务 **每1小时执行1次的Cron表达式:0 0 0/1 * * ?** **JobHandler:updateLatelyAlbumRankingJob** ![image-123.png](assets/image-123.png) # 三、订单模块 ## 订单涉及的表有哪些,字段呢 | order_info(订单信息) | 用户ID | | -------------------- | ---------------------------------------------- | | | 订单标题 | | | 订单号 | | | 订单状态:0901-未支付 0902-已支付 0903-已取消 | | | 订单原始金额 | | | 减免总金额 | | | 订单总价 | | | 付款项目类型: 1001-专辑 1002-声音 1003-vip会员 | | order_detail(订单明细) | 订单id | | ---------------------- | --------------- | | | 付费项目id | | | 付费项目名称 | | | 付费项目图片url | | | 付费项目价格 | | order_derate(订单减免表) | 订单id | | ------------------------ | ----------------------------------------- | | | 订单减免类型 1405-专辑折扣 1406-VIP服务折 | | | 减免金额 | | | 备注 | | user_account(用户账户) | id | | ---------------------- | ---------- | | | 用户id | | decimal | 账户总金额 | | | 锁定金额 | | | 可用金额 | | | 总收入 | | | 总支出 | | | 创建时间 | | user_account_detail (用户账户明细) | id | | ----------------------------------- | ----------------------------------------------------- | | | 交易标题 | | | 交易类型(1201-充值 1202-锁定 1203-解锁 1204-消费 ) | | | 订单编号 | | | 创建时间 | | | 用户id | ## 为什么选择选择seata作为分布式事务的解决方案 我们项目微服务解决方案是用springcloudalibaba那一套,seata本身就是alibba提供的一套分布式事务解决方案 ## 你们防止订单篡改的步骤是什么? 主要使用了MD5+时间戳进行加密 ```java //5.3.1 将订单VO转为Map-将VO中支付方式null值去掉 Map paramsMap = BeanUtil.beanToMap(orderInfoVo, false, true); //5.3.2 调用签名API对现有订单所有数据进行签名 String sign = SignHelper.getSign(paramsMap); orderInfoVo.setSign(sign); ``` ## Feign远程调用请求头丢失解决 ```java @Component public class FeignInterceptor implements RequestInterceptor { public void apply(RequestTemplate requestTemplate){ // 获取请求对象 RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); //异步编排 与 MQ消费者端 为 null 避免出现空指针 if(null != requestAttributes) { ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes)requestAttributes; HttpServletRequest request = servletRequestAttributes.getRequest(); String token = request.getHeader("token"); requestTemplate.header("token", token); } } } ``` ## 你们专辑的价格,和声音的价格字段在哪些表中 在专辑表album_info中,里面有付费类型: 0101-免费、0102-vip免费、0103-付费 折扣的字段为discount、vip_discount:0.1-9.9 不打折为-1 ## 订单下单成功之后你们的操作是什么,涉及的表字段有什么 user_info (用户表) is_vip: 用户是否为VIP会员 0:普通用户 1:VIP会员 vip_expire_time:当前VIP到期时间,即失效时间 user_vip_service(用户vip服务记录表) 开始生效日期 到期时间 是否自动续费 下次自动续费时间 user_paid_album(用户已付款专辑) user_paid_track() ## 订单延时关单的具体流程 延迟关单功能的实现我们使用redisson的是延迟队列,它的原理就是redis的zset集合,zset有一个评分,当延时时间到时(当前时间+延时时间)就会被排到第一位,然后根据redis的发布订阅模式,将消息发送到阻塞队列当中,因为我们要一直到阻塞队列拿到消息,所以我们要根据单例线程池创建一个线程来一直循环从阻塞队列拿到消息里面的订单ID,拿到了说明延迟时间到了,就根据订单号去判断支付状态修改,并关闭订单。 ```java /** * 项目启动后开启线程监听阻塞队列中消息 */ @PostConstruct public void orderCancal() { log.info("开启线程监听延迟消息:"); //1.创建阻塞队列(当队列内元素超过上限,继续队列发送消息,进入阻塞状态/当队列中元素为空,继续拉取消息,进入阻塞状态) RBlockingQueue blockingQueue = redissonClient.getBlockingQueue(KafkaConstant.QUEUE_ORDER_CANCEL); //2.开启线程监听阻塞队列中消息 只需要单一核心线程线程池对象即可 ExecutorService executorService = Executors.newSingleThreadExecutor(); executorService.submit(()->{ while (true) { String take = null; try { take = blockingQueue.take(); } catch (InterruptedException e) { throw new RuntimeException(e); } if (StringUtils.isNotBlank(take)) { log.info("监听到延迟关单消息:{}", take); //查询订单状态,关闭订单 orderInfoService.orderCanncal(Long.valueOf(take)); } } }); } } ``` ## 你们金额是用什么类型来接收的,有遇到什么坑吗 BigDecimal 禁止使用构造方法 BigDecimal(double) 的方式把 double 值转化为 BigDecimal 对象。 正例:优先推荐入参为 String 的构造方法,或使用 BigDecimal的 valueOf方法,此方法内部其实执行了 Double 的toString,而 Double 的 toString 按 double 的实际能表达的精度对尾数进行了截断。 BigDecimal recommend1 = new BigDecimal("0.1"); BigDecimal recommend2 = BigDecimal.valueOf(0.1); 两个比较金额应使用compareTo() 方法。equals()方法会比较值和精度(1.0与 1.00返回结果为 false),而compareTo() 则会忽略精度 我们有一个自己封装好的工具类来处理金额类型的BigDecimal ```java package com.atguigu.interview2.utils; import java.math.BigDecimal; /**用于高精确处理常用的数学运算 * @auther zzyy * @create 2024-05-02 17:21 */ public class ArithmeticUtils { //默认除法运算精度 private static final int DEF_DIV_SCALE = 10; /** * 提供精确的加法运算 * * @param v1 被加数 * @param v2 加数 * @return 两个参数的和 */ public static double add(double v1, double v2) { BigDecimal b1 = new BigDecimal(Double.toString(v1)); BigDecimal b2 = new BigDecimal(Double.toString(v2)); return b1.add(b2).doubleValue(); } /** * 提供精确的加法运算 * * @param v1 被加数 * @param v2 加数 * @return 两个参数的和 */ public static BigDecimal add(String v1, String v2) { BigDecimal b1 = new BigDecimal(v1); BigDecimal b2 = new BigDecimal(v2); return b1.add(b2); } /** * 提供精确的加法运算 * * @param v1 被加数 * @param v2 加数 * @param scale 保留scale 位小数 * @return 两个参数的和 */ public static String add(String v1, String v2, int scale) { if (scale < 0) { throw new IllegalArgumentException( "The scale must be a positive integer or zero"); } BigDecimal b1 = new BigDecimal(v1); BigDecimal b2 = new BigDecimal(v2); return b1.add(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).toString(); } /** * 提供精确的减法运算 * * @param v1 被减数 * @param v2 减数 * @return 两个参数的差 */ public static double sub(double v1, double v2) { BigDecimal b1 = new BigDecimal(Double.toString(v1)); BigDecimal b2 = new BigDecimal(Double.toString(v2)); return b1.subtract(b2).doubleValue(); } /** * 提供精确的减法运算。 * * @param v1 被减数 * @param v2 减数 * @return 两个参数的差 */ public static BigDecimal sub(String v1, String v2) { BigDecimal b1 = new BigDecimal(v1); BigDecimal b2 = new BigDecimal(v2); return b1.subtract(b2); } /** * 提供精确的减法运算 * * @param v1 被减数 * @param v2 减数 * @param scale 保留scale 位小数 * @return 两个参数的差 */ public static String sub(String v1, String v2, int scale) { if (scale < 0) { throw new IllegalArgumentException( "The scale must be a positive integer or zero"); } BigDecimal b1 = new BigDecimal(v1); BigDecimal b2 = new BigDecimal(v2); return b1.subtract(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).toString(); } /** * 提供精确的乘法运算 * * @param v1 被乘数 * @param v2 乘数 * @return 两个参数的积 */ public static double mul(double v1, double v2) { BigDecimal b1 = new BigDecimal(Double.toString(v1)); BigDecimal b2 = new BigDecimal(Double.toString(v2)); return b1.multiply(b2).doubleValue(); } /** * 提供精确的乘法运算 * * @param v1 被乘数 * @param v2 乘数 * @return 两个参数的积 */ public static BigDecimal mul(String v1, String v2) { BigDecimal b1 = new BigDecimal(v1); BigDecimal b2 = new BigDecimal(v2); return b1.multiply(b2); } /** * 提供精确的乘法运算 * * @param v1 被乘数 * @param v2 乘数 * @param scale 保留scale 位小数 * @return 两个参数的积 */ public static double mul(double v1, double v2, int scale) { BigDecimal b1 = new BigDecimal(Double.toString(v1)); BigDecimal b2 = new BigDecimal(Double.toString(v2)); return round(b1.multiply(b2).doubleValue(), scale); } /** * 提供精确的乘法运算 * * @param v1 被乘数 * @param v2 乘数 * @param scale 保留scale 位小数 * @return 两个参数的积 */ public static String mul(String v1, String v2, int scale) { if (scale < 0) { throw new IllegalArgumentException( "The scale must be a positive integer or zero"); } BigDecimal b1 = new BigDecimal(v1); BigDecimal b2 = new BigDecimal(v2); return b1.multiply(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).toString(); } /** * 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到 * 小数点以后10位,以后的数字四舍五入 * * @param v1 被除数 * @param v2 除数 * @return 两个参数的商 */ public static double div(double v1, double v2) { return div(v1, v2, DEF_DIV_SCALE); } /** * 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指 * 定精度,以后的数字四舍五入 * * @param v1 被除数 * @param v2 除数 * @param scale 表示表示需要精确到小数点以后几位。 * @return 两个参数的商 */ public static double div(double v1, double v2, int scale) { if (scale < 0) { throw new IllegalArgumentException("The scale must be a positive integer or zero"); } BigDecimal b1 = new BigDecimal(Double.toString(v1)); BigDecimal b2 = new BigDecimal(Double.toString(v2)); return b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP).doubleValue(); } /** * 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指 * 定精度,以后的数字四舍五入 * * @param v1 被除数 * @param v2 除数 * @param scale 表示需要精确到小数点以后几位 * @return 两个参数的商 */ public static String div(String v1, String v2, int scale) { if (scale < 0) { throw new IllegalArgumentException("The scale must be a positive integer or zero"); } BigDecimal b1 = new BigDecimal(v1); BigDecimal b2 = new BigDecimal(v1); return b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP).toString(); } /** * 提供精确的小数位四舍五入处理 * * @param v 需要四舍五入的数字 * @param scale 小数点后保留几位 * @return 四舍五入后的结果 */ public static double round(double v, int scale) { if (scale < 0) { throw new IllegalArgumentException("The scale must be a positive integer or zero"); } BigDecimal b = new BigDecimal(Double.toString(v)); return b.setScale(scale, BigDecimal.ROUND_HALF_UP).doubleValue(); } /** * 提供精确的小数位四舍五入处理 * * @param v 需要四舍五入的数字 * @param scale 小数点后保留几位 * @return 四舍五入后的结果 */ public static String round(String v, int scale) { if (scale < 0) { throw new IllegalArgumentException("The scale must be a positive integer or zero"); } BigDecimal b = new BigDecimal(v); return b.setScale(scale, BigDecimal.ROUND_HALF_UP).toString(); } /** * 取余数 * * @param v1 被除数 * @param v2 除数 * @param scale 小数点后保留几位 * @return 余数 */ public static String remainder(String v1, String v2, int scale) { if (scale < 0) { throw new IllegalArgumentException( "The scale must be a positive integer or zero"); } BigDecimal b1 = new BigDecimal(v1); BigDecimal b2 = new BigDecimal(v2); return b1.remainder(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).toString(); } /** * 取余数 BigDecimal * * @param v1 被除数 * @param v2 除数 * @param scale 小数点后保留几位 * @return 余数 */ public static BigDecimal remainder(BigDecimal v1, BigDecimal v2, int scale) { if (scale < 0) { throw new IllegalArgumentException("The scale must be a positive integer or zero"); } return v1.remainder(v2).setScale(scale, BigDecimal.ROUND_HALF_UP); } /** * 比较大小 * * @param v1 被比较数 * @param v2 比较数 * @return 如果v1 大于v2 则 返回true 否则false */ public static boolean compare(String v1, String v2) { BigDecimal b1 = new BigDecimal(v1); BigDecimal b2 = new BigDecimal(v2); int bj = b1.compareTo(b2); boolean res; if (bj > 0) res = true; else res = false; return res; } } ``` ## 听书的系统架构 ![image-178.png](assets/image-178.png) ## 为什么选择Kafaka不选择Rabbitmq 1. **业务需求**: - **RabbitMQ**:适用于需要可靠消息传递、高度灵活性的消息模型以及与其他应用集成的场景。例如,金融系统的支付、订单处理等需要可靠消息传递的业务,以及需要消息路由、动态队列等高度灵活性的消息模型的业务。 - **Kafka**:适用于需要高吞吐量和低延迟的实时数据处理场景,如日志收集、事件驱动架构等。Kafka的发布/订阅模式以及高效的批量读写操作使其成为流处理和数据管道的理想选择。 - **RocketMQ**:适用于需要处理大规模消息流和高并发场景,如电子商务和金融交易系统。RocketMQ的高吞吐量和低延迟特性使其能够应对高负载情况。 2. **性能需求**: - **Kafka**:单机写入TPS极高,适用于百万级吞吐量的场景。 - **RocketMQ**:同样具有高吞吐量和低延迟的特点,但具体性能可能因部署和配置而异。 - **RabbitMQ**:虽然也具有良好的性能,但在处理超大规模数据流时可能稍逊于Kafka和RocketMQ。 3. **可靠性需求**: - **RabbitMQ**:具有稳定、可靠的消息传递机制,通过持久化、确认、回退等机制确保消息的稳定传递。 - **RocketMQ**:支持异步实时刷盘、同步刷盘、同步复制等功能,保证了数据的可靠性,特别是在单机可靠性上表现优异。 - **Kafka**:通过异步复制和同步复制机制确保数据可靠性,但可能在某些情况下略逊于RocketMQ。 4. **社区和生态系统**: - **RabbitMQ**:拥有庞大的开发者社区和丰富的文档资源,用户在遇到问题时能够得到及时的帮助和支持。 - **Kafka**:在大数据和流处理领域拥有广泛的生态系统,与许多大数据工具集成良好。 - **RocketMQ**:社区规模相对较小,可能在寻找解决方案或支持时面临一定挑战。 5. **部署和配置**: - **RabbitMQ**:相对易于安装和部署,适用于中小规模系统。 - **Kafka**:部署和配置相对简单,但需要一定的专业知识。 - **RocketMQ**:部署和配置可能较为复杂,需要对集群和网络进行合理规划。 ![image-20220625232225254.png](assets/image-20220625232225254.png) ## 你们消息怎么保证不丢失的? ### 1. **设置正确的副本数和复制因子** Kafka 中每个主题的分区都可以配置多个副本(Replica)。设置适当的副本数可以提高数据的可靠性和容错能力。通常建议每个分区配置至少 3 个副本,确保即使一个 Broker 节点宕机,数据仍然可用。 ``` properties复制代码# 设置副本数为3 replication.factor=3 ``` ### 2. **消息确认机制** Kafka 的生产者可以配置消息的确认机制(acknowledgment),确保消息成功写入到 Broker 中。常见的确认机制有以下几种: - **acks=0**:生产者不等待来自服务器的任何确认,可能导致消息丢失。 - **acks=1**:生产者在 Leader 副本确认消息后发送成功确认,可能导致消息在网络分区或 Leader 节点宕机时丢失。 - **acks=all**:生产者在 Leader 和 ISR 中的所有副本都确认消息后发送成功确认,确保了最高的消息可靠性。 ``` properties复制代码# 设置确认机制为all acks=all ``` ### 3. **数据持久化** Kafka 使用日志(Log)来持久化消息,即使在消息被消费之前,消息也会被持久化到磁盘上。这保证了即使在生产者发送消息后但尚未被消费之前,消息仍然安全地存储在磁盘上。 ### 4. **Leader 和 ISR 机制** Kafka 使用 Leader 和 ISR(In-Sync Replica)机制来保证数据的一致性和可靠性。只有在 ISR 中的副本才能参与消息的读写操作。当 Leader 副本宕机或者出现网络分区时,不在 ISR 中的副本会被移除,确保消息的可靠传输。 ### 5. **配置合适的消息过期时间** 可以通过配置消息的过期时间来控制消息在 Broker 中的存储时间。超过过期时间的消息将被删除,避免消息在系统中无限积累导致资源浪费。 ``` properties复制代码# 设置消息过期时间为7天 message.max.age.ms=604800000 ``` ### 6. **备份和监控** 确保 Kafka 集群有良好的备份策略,并通过监控系统监控集群的健康状态。及时发现并解决可能导致消息丢失的问题,如磁盘空间不足、网络故障等。 ### 7. **持久性配置** 在 Kafka 的配置文件中,还可以通过配置项来提高消息的持久性和可靠性,例如: - `log.retention.ms`:日志文件保留时间。 - `log.segment.bytes`:日志段文件大小。 - `unclean.leader.election.enable`:是否允许不同步的副本成为 Leader。 ## 怎么保证消息的消费顺序 ## rabbitmq怎么处理消息堆积问题 ## nginx怎么配置负载均衡,反向代理 **下载安装nginx** - yum install 安装nginx - yum install 安装perl库、安装zlib库 - 编译安装 - cd /usr/local/nginx/sbin/ ./nginx 启动nginx 编辑nginx.conf ```java http { upstream myapp { //upstream myapp块定义了三个后端服务器的IP地址和权重 server 192.168.1.2 weight=3; server 192.168.1.3 weight=1; server 192.168.1.4 weight=1; } server { server块定义了一个监听80端口的服务器 listen 80; location / { //location /块配置了将所有请求转发到上游定义的backend_server。 proxy_pass http://myapp; proxy_pass指令指定了后端服务器。 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; //配置缓存头 proxy_set_header X-Forwarded-Proto $scheme; } } } ``` ## 你是怎么用定时任务的?你说了更新redis排行榜 在专辑模块中定了这样的一个接口,把专辑排好序的专辑(热度,播放量,订阅量,购买量,评论量)统计好放入redis的hash中,然后再定时模块中用openfein调用这个接口,配置好执行器名称,然后再xxl-job调度中心配置每天凌晨2点执行这个更新排行榜任务 ## docker怎么部署项目的 ## 项目的挑战,有什么难点?怎么解决的 缓存注解 分布式锁删除其他锁的问题,这个场景 策略模式 ## 面试不会的地方 这个确实在工作的时候没有考虑到,如果工作需要的话我会花时间补上来 ## Redisson的功能 # 就业面试 ## 简历制作 ### word简历 word名字 :黄根源_Java开发工程师 _3年 标题:黄根源_Java开发工程师 _3年 基本信息: - 姓名:黄根源 - 性别:男 - 年龄:26 - 籍贯:广东汕头 - 学历:本科 - 专业: 软件工程 - 毕业时间:22届 - 现居地址(可无):宝安碧头地铁站 - 联系电话:18476157184 - 电子邮箱:18476157184@163.com 求职意向: - 期望职位:Java开发工程师 - 到岗时间:随时到岗 - 期望地点:深圳 - 目前状态:已离职 - 期望薪资:面议 - 工作性质:全职 专业技能: 1.熟练 2.熟悉 3.掌握 4.了解 工作经历: ![image-89.png](assets/image-89.png) - 时间:2022 .01 - 2024 .07.15 - 公司:学一个没有官网,没有风险,成立时间比自己工作时间久,科技类 - 工作职责 项目经验: 教育经历: 个人亮点: 逻辑思维、热爱学习、抗压能力、适应能力、性格随和、融入团队 ### 网上简历 ### 投递时间及注意时间 ![image-156.png](assets/image-156.png) ![image-188.png](assets/image-188.png) ![image-184.png](assets/image-184.png) ![image-166.png](assets/image-166.png) ## 面试技巧 面试前工作 - 了解公司 业务地点,业务,营业执照,看准网 - 面试准备 公司地址、联系方式、面试时间、证件 准备面试行程备忘录 - 面试前注意事项 应早到10-15分钟,到达先去洗手间整理 手机调成飞行模式,录音 敲门,不要打断面试官 面试中注意 主动面试时候给结果 我现在单身,没有任何家庭负担,可以全身心的投入工作。同时,我也会提高工作效率,减少不必要的加班。 hr 问什么时候出结果 组长 技术栈 公司业务 项目进程 开发人员组成 # --------------------------------------------------------------- # ----------------------------------------------------------------- # 简历问题-------------------------------------------------- ## 什么是倒排索引 - **正排索引**:唯一 ID 作为索引,以文档内容作为记录的结构。 - **倒排索引**:将文档内容中的单词作为索引,将包含该词的文档 ID 作为记录的结构。 **索引过程:** 1. 把词条进行分词,同时记录文档索引位置 2. 把词条相同的数据化合并 3. 把词条进行排序 ## 什么是OOP编程思想,你在项目怎么用这个 面向对象编程是一种编程范式,它的核心概念有封装,继承,多态 封装是将数据和方法组合在一起,并隐藏对象的内部细节。外部只能通过对象的方法来访问这些数据 继承允许一个类继承另一个类的属性和方法,从而实现代码重用和扩展。子类(派生类)继承父类(基类)的特性,可以添加新的属性和方法或重写父类的方法 多态 :父类的应用指向子类的对象,多态允许不同的类以相同的接口来实现不同的功能,就比如 List arrayList = new ArrayLsit ## 类的加载机制 面向对象编程中,类的加载机制是指将类的字节码从文件系统或网络等位置加载到内存中,并进行链接和初始化的过程, 1. **加载(Loading)**: - **通过类名定位类文件**:类加载器(ClassLoader)通过类名在文件系统、JAR文件或网络等位置查找类的字节码文件(通常是`.class`文件)。 - **读取类文件**:将类文件的字节码读入内存中。 - **生成Class对象**:将字节码转换为对应的Class对象,表示类的结构信息。 2. **验证(Verification)**: - 验证字节码的正确性和安全性,确保它不违反Java语言规范和字节码规范。 - 验证内容包括:文件格式验证、元数据验证、字节码验证和符号引用验证。 3. **准备(Preparation)**: - 为类的静态变量分配内存,并将其初始化为默认值。此时不包括静态代码块和静态变量的初始化表达式。 4. **解析(Resolution)**: - 将常量池中的符号引用转换为直接引用。符号引用是用一组符号来描述所引用的目标,如类名、字段名和方法名;直接引用是实际指向目标的指针、偏移量或句柄。 5. **初始化(Initialization)**: - 执行类的初始化代码,包括静态变量的初始化和静态代码块。初始化是按照类的代码顺序执行的。 ## 类的加载器 将运行的Java类动态加载到JVM内存中 1. 主启动类加载器:加载Java核心库类 2. 拓展类加载器:加载拓展库中的类 3. 应用程序类加载器:加载用户路径的类和Jar包,是大部分的Java程序的默认类加载器 4. 用户自定义的类加载器: - 隔离加载类 - 修改类加载的方式 :可以打破双亲委派机制 - 扩展加载源 :比如从数据库、网络、甚至是电视机机顶盒进行加载 - 防止源码泄漏 ## 双亲委派模型 当一个类加载器加载类时,先把请求委派给父类加载器,父类加载器再逐级向上委派,直到启动类加载器。只有当父类加载器无法加载该类时,子类加载器才会尝试加载。 这种机制保证了Java类加载的安全性和一致性,避免了重复加载同一个类的问题 ## 打破双亲委派模式是为了什么?有什么例子 比如JDBC连接例子当中,如给主启动类要主动加载下层类加载器的内容,各个厂商的JDBC模板驱动,这时候就要打破双亲委派机制 **实现**:线程上下文类加载器直接去加载,就可以在上层类加载器加载下层类的API ![image-225232254452.png](assets/image-225232254452.png) ----------------------------------------------------------------------------------------------------------------------------------------------------------- tomcat 违背了java 推荐的双亲委派模型,tomcat 为了实现隔离性,没有遵守这个约定,每个webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器。 ![image-2022062521.png](assets/image-2022062521.png) ## Java的集合 ### List 接口 `List` 是一个有序集合,允许重复元素。常用的实现类有: - **ArrayList**:基于动态数组实现,支持快速随机访问。适用于频繁读取和遍历的场景。 - **LinkedList**:基于双向链表实现,支持快速插入和删除操作。适用于频繁插入、删除操作的场景。 - **Vector**:与 `ArrayList` 类似,但线程安全(已不常用) ### Set 接口 `Set` 是一个不允许重复元素的集合。常用的实现类有: - **HashSet**:基于哈希表实现,支持快速查找和插入操作。元素无序。 - **LinkedHashSet**:继承自 `HashSet`,具有可预测的迭代顺序(插入顺序)。 - **TreeSet**:基于红黑树实现,元素有序(自然顺序或自定义顺序) ### Map 接口 `Map` 是一个键值对(key-value)映射的集合,不属于 `Collection` 接口的子接口。常用的实现类有: - **HashMap**:基于哈希表实现,支持快速查找和插入操作。键值对无序。 - **LinkedHashMap**:继承自 `HashMap`,具有可预测的迭代顺序(插入顺序)。 - **TreeMap**:基于红黑树实现,键值对有序(按键的自然顺序或自定义顺序)。 - **Hashtable**:与 `HashMap` 类似,但线程安全(已不常用)。 - **ConcurrentHashMap**:线程安全的哈希表,支持高效的并发操作 ## IO流 | 字节流 | 字符流 | 标准IO流 | 对象流 用于读写Java对象,实现对象的序列化和反序列化 | | ------------------------------------------------------------ | ------------------------------------------------------------ | ---------------------------------------------------- | ------------------------------------------------------------ | | 输入流(InputStream) | 读取器(Reader) | **System.in**:标准输入流,通常从键盘读取输入 | **ObjectInputStream**:从流中读取对象 | | **FileInputStream**:用于从文件中读取字节 | **FileReader**:用于从文件中读取字符 | **System.out**:标准输出流,通常向控制台输出数据 | **ObjectOutputStream**:将对象写入流中 | | **ByteArrayInputStream**:用于在内存中创建字节数组 | **BufferedReader**:为另一个字符输入流添加缓冲功能,提高读取效率 | **System.err**:标准错误流,通常向控制台输出错误信息 | | | **BufferedInputStream**:为另一个输入流添加缓冲功能,提高读取效率 | 写入器(Writer) | | | | 输出流(OutputStream) | **FileWriter**:用于将字符写入文件 | | | | **FileOutputStream**:用于将字节写入文件 | **BufferedWriter**:为另一个字符输出流添加缓冲功能,提高写入效率 | | | | **ByteArrayOutputStream**:在内存中创建字节数组 | | | | | **BufferedOutputStream**:为另一个输出流添加缓冲功能,提高写入效率 | | | | ## 什么是反射 指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法;并且对于任意一个对象,都能够调用它的任意一个方法; 获取 Class 对象的 3 种方法 : **调用某个对象的 getClass()方法** Person p=new Person(); Class clazz=p.getClass(); **调用某个类的 class 属性来获取该类对应的 Class 对象** Class clazz=Person.class; **使用 Class 类中的 forName()静态方法(最安全/性能最好)** Class clazz=Class.forName("类的全路径"); (最常用) ## 什么是泛型 在没有泛型之前,Java程序员使用Object类型来处理集合中的元素,这导致了代码中充斥着大量的类型转换操作,而且容易出错。例如,如果你将一个错误类型的对象添加到集合中,那么在运行时可能会引发`ClassCastException`异常 主要目的是提供编译时类型检查和消除类型转换,使得代码更加安全、清晰和易于维护 ## Java常用API的使用 | String | Math | Random | Collection | | ----------------------------------------------- | --------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------- | | **length()** 字符串的长度 | `abs(double a)`:返回double值的绝对值。 | `Random()`:创建一个新的随机数生成器 | `add(E e)`:把给定的对象添加到当前集合中 | | **charAt()** 截取一个字符 | `ceil(double a)`:向上取整 | `int nextInt(int n)`:返回一个伪随机数,范围在0(包括)和指定值n(不包括)之间的int值 | `clear()` :清空集合中所有的元素 | | **getChars()** 截取多个字符 | `floor(double a)`:向下取整 | | `remove(E e)`:把给定的对象在当前集合中删除 | | **equals()和equalsIgnoreCase() 比较两个字符串** | `max(int a, int b)` :返回a与b中较大值 | | `contains(E e)`:判断当前集合中是否包含给定的对象 | | **concat()** 连接两个字符串 | `min(int a, int b)`:返回a与b中较小值 | | `isEmpty()`:判断当前集合是否为空 | | **replace()** 替换 | `pow(double a, doubl b)` :a^b | | `int size()`:返回集合中元素的个数 | | **trim()** 去掉起始和结尾的空格 | | | `toArray()`:把集合中的元素,存储到数组中 | | **toLowerCase()** 转换为小写 | | | | | **toUpperCase()** 转换为大写 | | | | ## Java8的新特性 1. Lambda表达式 Lambada允许把函数作为一个方法的参数 2. 方法运用 方法引用允许直接引用已有Java类或对象的方法或者构造 3. 函数接口 有且仅有一个抽象方法的接口叫做函数式接口,函数式接口可以被隐式转换为Lambda 表达式 4. 接口允许定义默认方法和静态方法 5. Stream API 把集合看作一种流,流在管道中传输,并且可以在管道的节点上进行处理,比如筛选,排序,聚合等。 6. 日期/时间类改进 这些类都在 java.time 包下,LocalDate/LocalTime/LocalDateTime。 7. Optional 类 Optional 类是一个可以为 null 的容器对象。如果值存在则 isPresent()方法会返回 true,调用 get()方法会返回该对象。 8. Java8 Base64 实现 Java 8 内置了 Base64 编码的编码器和解码器。 ## JUC 并发工具包 并发工具包主要是提供在多线程的情况下,来保证变量安全的一些工具类我大概熟悉的有 `ConcurrentHashMap`:一种线程安全的哈希表实现,支持高效的并发读写操作。 `AtomicInteger`、`AtomicLong`、`AtomicReference`等:提供了一组用于操作单个变量的线程安全类,利用底层的原子操作。 `ReentrantLock`:一种可重入的互斥锁,提供了与`synchronized`关键字相同的功能,但更灵活。 `ReadWriteLock`:允许多个读线程同时访问,但写线程访问时独占锁 ## 线程的创建方式,生命周期 1.继承 Thread 类并重写 run 方法创建线程,实现简单但不可以继承其他类 2.实现 Runnable 接口并重写 run 方法。避免了单继承局限性,编程更加灵活,实 现解耦。 3..实现 Callable 接口并重写 call 方法,创建线程。可以获取线程执行结果的返回 值,并且可以抛出异常。 4.使用线程池创建(使用 java.util.concurrent.Executor 接口) **线程的生命周期** 1. **新建状态(New)**:当线程对象被创建但尚未调用 `start()` 方法时,处于新建状态。 2. **就绪状态(Runnable)**:当线程对象调用了 `start()` 方法后,线程进入就绪状态,等待获取 CPU 的执行时间片段。 3. **运行状态(Running)**:当线程获得 CPU 时间片段后,开始执行 `run()` 方法中的任务,处于运行状态。 4. **阻塞状态(Blocked)**:线程在运行过程中,由于某些原因(如等待某个资源、睡眠、等待 I/O 完成等),暂时放弃 CPU 的执行权限,进入阻塞状态。 5. **等待状态(Waiting)**:线程调用 `wait()` 方法进入等待状态,等待其他线程调用相同对象的 `notify()` 或 `notifyAll()` 方法唤醒它。 6. **超时等待状态(Timed Waiting)**:线程调用带有超时参数的 `sleep()`、`join()` 或 `wait()` 方法时,进入超时等待状态,等待一段时间后自动唤醒。 7. **终止状态(Terminated)**:线程的 `run()` 方法执行完毕或者调用 `stop()` 方法强制终止线程后,线程进入终止状态。 ## 线程的相关基本方法 **线程相关的基本方法有 wait,notify,notifyAll,sleep,join,yield 等** **1.线程等待(wait)** 调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中 断才会返回,需要注意的是调用 wait()方法后,会释放对象的锁。因此,wait 方 法一般用在同步方法或同步代码块中。 **2.线程睡眠(sleep)** sleep 导致当前线程休眠,与 wait 方法不同的是 sleep 不会释放当前占 有的锁,sleep(long)会导致线程进入 TIMED-WATING 状态,而 wait()方法 会导致当前线程进入 WATING 状态. **3.线程让步(yield)** yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争 CPU 时间片。一般情况下,优先级高的线程有更大的可能性成功竞争得到 CPU 时间片,但这又不是绝对的,有的操作系统对 线程优先级并不敏感。 **4.线程中断(interrupt)** 中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的 一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等) **5.Join 等待其他线程终止** join() 方法,等待其他线程终止,在当前线程中调用一个线程的 join() 方 法,则当前线程转为阻塞状态,回到另一个线程结束,当前线程再由阻塞状态变 为就绪状态,等待 cpu 的宠幸. **6.线程唤醒(notify)** Object 类中的 notify() 方法,唤醒在此对象监视器上等待的单个线程,如 果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的,并 在对实现做出决定时发生,线程通过调用其中一个 wait() 方法,在对象的监视 器上等待,直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程, 被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。类 似的方法还有 notifyAll() ,唤醒再次监视器上等待的所有线程 ## 线程池的作用 在实际使用中,线程是很占用系统资源的,如果对线程管理不完善的话很容易导致 系统问题。因此,在大多数并发框架中都会使用线程池来管理线程,使用线程池管理线程主 要有如下好处: 1、使用线程池可以重复利用已有的线程继续执行任务,避免线程在创建销毁时造成的消耗 2、由于没有线程创建和销毁时的消耗,可以提高系统响应速度 3、通过线程可以对线程进行合理的管理,根据系统的承受能力调整可运行线程数量的大小等 ## 线程池分类 ![image-88.png](assets/image-88.png) - 创建一个可进行缓存重复利用的线程池 - 创建一个可重用固定线程数的线程池 - 创 建 一 个 使 用单线程的线程池,以无界队列方式来运行该线程。之后提交的线程将会排在队列中以此执行 - 创建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期执行 - 创建一个线程池,它可安排在给定延迟后运行命令或者定期的执行 - 创建一个带并行级别的线程池,并行级别决定了同一时刻最多有多少个线程在执行,如不传并行级别参数,将默认为当前系统的 CPU 个数 ## 线程池的参数,以及工作原理 corePoolSize:核心线程池的大小 maximumPoolSize:线程池能创建线程的最大个数 keepAliveTime:空闲线程存活时间 unit:时间单位,为 keepAliveTime 指定时间单位 workQueue:阻塞队列,用于保存任务的阻塞队列 threadFactory:创建线程的工程类 handler:饱和策略(拒绝策略) **工作流程** 线程池首先判断核心线程池里的线程是否已经满了。如果不是,则创建一个新的工作线 程来执行任务。否则进入 2. 2. 判断工作队列是否已经满了,倘若还没有满,将线程放入工作队列。否则进入 3. 3. 判断线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行。 如果线程池满了,则交给饱和策略来处理任务。 ## 拒绝策略 - 默认:丢弃任务并抛出 异常,让你感知到任务被拒绝了,我们可以根据业务逻辑选择重试或者放弃提交等策略 - 也是丢弃任务,但是不抛出异常,相对而言存在一定的风险 - 丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程),通常是存活时间最长的任务 - 既不抛弃任务也不抛出异常,而是将某些任务回退到调用者,让调用者去执行它 ## Java的原子类有哪些? | 基本类型原子类 | 数组类型原子类 | 引用类型原子类 | 对象的属性修改原子类 | 原子操作增强类原理深度解析 | | -------------- | -------------------- | ------------------------------------------- | ------------------------------------------------------------ | -------------------------- | | AtomicInteger | AtomicIntegerArray | AtomicReference | AtomicIntegerFieldUpdater | DoubleAccumulator | | AtomicBoolean | AtomicLongArray | AtomicStampedReference | AtomicLongFieldUpdater | DoubleAdder | | AtomicLong | AtomicReferenceArray | AtomicMarkableReference | AtomicReferenceFieldUpdater | LongAccumulator | | | | 携带版本号的引用类型原子类,可以解决ABA问题 | 以一种线程安全的方式操作非线程安全对象内的某些字段 更新的对象属性必须使用 public volatile 修饰符。 | LongAdder | 常用API简介 int get() //获取当前的值 getAndSet(int newValue) 获取当前的值,并设置新的值 getAndIncrement() 获取当前的值,并自增 getAndDecrement() 获取当前的值,并自减 getAndAdd(int delta) 获取当前的值,并加上预期的值 boolean compareAndSet(int expect, int update) 如果输入的数值等于预期值,则以原子方式将该值设置为输入值 面试官问你:你在哪里用了volatile(内存屏障保证可见性和有序性) AtomicReferenceFieldUpdater 以一种线程安全的方式操作非线程安全对象内的某些字段 更新的对象属性必须使用 public volatile 修饰符。 ## JVM的主要组成部分 1. 类加载器classloader子系统 将运行时的Java类动态加载到JVM内存中 2. 运行时数据区 用于存储数据和状态信息的内存区域 3. 执行性引擎 将代码编译/解释为本地机器指令,并执行 4. 本地方法库 用Java调用操作系统的本地方法 ## JVM的运行数据区有什么东西 ![image-2278941.png](assets/image-2278941.png) 主要分为线程私有,和线程共享 线程私有有:程序计数器、本地方法栈、虚拟机栈 线程共享的有:方法区和堆 程序计数器 :存储指向下一条指令的地址,也即将要执行的指令代码 本地方法栈:用于管理和支持本地方法(native methods)的调用和执行 虚拟机栈:一个个栈帧(方法) **栈的深度和长度在字节码的时候就可以确定下来了** **不存在**:GC问题 存在:OOM问题 一般大小为512K-1024K 如果设置过大,同等内存先能创建的线程数量就变小了 - 局部变量表:(基本数据类型,引用数据类型地址) - 操作数栈:主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间 - 动态链接:指向运行时常量池中该方法的引用、 - 方法返回地址 **+** 一些附加信息:对程序调试提供支持的信息 堆 :对象实例 方法区:存储已被加载的类信息、静态变量、常量、静态变量 ## JVM的发现垃圾和回收垃圾 **如何发现垃圾** **1.引用计数算法** 该算法很古老(了解即可)。核心思想是,堆中的对象每被引用一次,则计数器加 1, 每减少一个引用就减 1,当对象的引用计数器为 0 时可以被当作垃圾收集。 优点:快。 缺点:无法检测出循环引用。如两个对象互相引用时,他们的引用计数永远不可能为 0。 24**2.根搜索算法(也叫可达性分析)** 根搜索算法是把所有的引用关系看作一张图,从一个节点 GC ROOT 开始,寻找对应的 引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕 之后,剩余的节点则被认为是没有被引用到的节点,即可以当作垃圾。 Java 中可作为 GC Root 的对象有 1.虚拟机栈中引用的对象 2.本地方法栈引用的对象 2.方法区中静态属性引用的对象 3.方法区中常量引用的对象 **如何回收垃圾** **1. 标记-清除算法(mark and sweep)** 分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成之后 统一回收掉所有被标记的对象。 缺点:首先,效率问题,标记和清除效率都不高。其次,标记清除之后会产生大量的不 连续的内存碎片。 **2. 标记-整理算法** 是在标记-清除算法基础上做了改进,标记阶段是相同的,但标记完成之后不是直接对 可回收对象进行清理,而是让所有存活的对象都向一端移动,在移动过程中清理掉可回收的 对象,这个过程叫做整理。 优点:内存被整理后不会产生大量不连续内存碎片。 **3. 复制算法(copying)** 将可用内存按容量分成大小相等的两块,每次只使用其中一块,当这块内存使用完了, 就将还存活的对象复制到另一块内存上去,然后把使用过的内存空间一次清理掉。 缺点:可使用的内存只有原来一半。 **4. 分代收集算法(generation)** 当前主流 JVM 都采用分代收集(Generational Collection)算法, 这种算法会根据对象 存活周期的不同将内存划分为年轻代、年老代、永久代,不同生命周期的对象可以采取不同 的回收算法,以便提高回收效率。 堆内存 划分为年轻代,老年代 和元空间 年轻代又划分为 **Eden 区**:大部分新对象在这里分配。 当 Eden 区满时,触发 Minor GC **两个 Survivor 区(S0 和 S1)** 存活的对象从 Eden 区和当前的 From 区(S0 或 S1)复制到 To r区(S1 或 S0,空闲的那个)。 老年代:存放的是大对象,以及如果对象在经过15次 Minor GC 后仍然存活,它们可能会被提升(promoted)到老年代: 年老代的垃圾收集( Full GC)发生频率较低但通常耗时较长 -- 涉及整个堆(年轻代和年老代)的扫描和整理,所以要尽量减少full gc的发生 ## JVM的调优 万物皆对象嘛,对象实例是存在堆当中的,平时写代码是不用考虑这些对象回收问题的,JVM会帮我们处理,但是如果出现OOM或者频繁的FULL GC,我们就要进行适当的调优 ```lisp ①调整堆大小提高服务的吞吐量-->增大了初始化内存和最大内存之后,我们的FullGC次数有一个明显的减少,吞吐量提升,服务器的性能有一个明显的提升 通常会将 -Xms 和-Xmx两个参数配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。 ②JTT优化:JVM默认开启逃逸分析 ③合理配置堆内存: Java整个堆大小设置,Xmx 和 Xms设置为老年代存活对象的3-4倍,即FullGC之后的老年代内存占用的3-4倍。 方法区(永久代 PermSize和MaxPermSize 或 元空间 MetaspaceSize 和 MaxMetaspaceSize)设置为老年代存活对象的1.2-1.5倍。 年轻代Xmn的设置为老年代存活对象的1-1.5倍。 老年代的内存大小设置为老年代存活对象的2-3倍。 老年代存活对象怎么去判定?--->查看日志:查看FullGc老年代存活对象的大小(推荐/比较稳) 强制触发Full GC:没有发生FullGc(影响线上服务,慎用) ``` ## 说一下IOC和aop sprigIoc,全称控制反转,就是在传统的java程序开发中,我们只能通过new关键字来创建对象,这种现象导致对象的的依赖关系比较复杂,耦合度较高,而IOC的主要作用是实现了对象的管理,也就是我们把设计好的对象交给了ioc容器控制,然后在需要用到的目标对象的时候,直接去容器中去获取。 我们在开发中经常把一些工具类,或者在mvc架构中的mapper层或者service层注入到ioc容器中,然后要用到的时候把它从容器拿到就可以了 aop,一般成为面向切面编程,作为面向对象的一种补充,把我们的代码中注入一些跟业务无关代码的一些操作,减少系统中的重复代码,减低了模块间的耦合度,提高系统的可维护行, 在登录模块中,为什么就写了一个aop切面类,主要是获取请求头里面的token 是否存在redis当中来判断用户是否登录 ## 静态代理和动态代理的区别 静态代理是在编译时期就已经确定代理类的实现方式 动态代理是在运行时动态生成代理类的方式,代理类不是在编译期间生成的,而是在运行时根据需要动态生成,通过反射机制实现,代理对象在运行时创建 ## spring中有两种动态代理模式 spring中有两种代理模式分别为JDK动态代理和CGLIB 动态代理 **适用性**: - JDK 动态代理适用于代理实现了接口的目标类,因为 JDK 代理要求目标类实现接口。 - CGLIB 动态代理适用于代理没有实现接口的类,它可以直接继承目标类生成代理对象。 **性能**: - JDK 动态代理相对于 CGLIB 动态代理,生成代理对象的速度更快,但在运行时的性能可能稍逊于 CGLIB,因为它基于接口调用,存在额外的方法调用开销。 - CGLIB 动态代理在生成代理对象时需要使用字节码技术动态生成目标类的子类,因此速度略慢,但在运行时性能较好,因为它直接继承目标类。 ## springmcv的执行流程是什么 ![image-202206252322252789.png](assets/image-202206252322252789.png) 1. 用户发送请求到前端控制器(DispatcherServlet) 2. 前 端 控 制 器 ( DispatcherServlet ) 收 到 请 求 调 用 处 理 器 映 射 器 (HandlerMapping),去查找处理器(Handler) 3. 处理器映射器(HandlerMapping)找到具体的处理器(可以根据 xml 配置、注解进行 查找),生成处理器对象及处理器拦截器(如果有则生成)一并返回给 DispatcherServlet。 4. 前端控制器(DispatcherServlet)调用处理器映射器(HandlerMapping) 5. 处理器适配器(HandlerAdapter)去调用自定义的处理器类(Controller,也叫 后端控制器)。 6.自定义的处理器类(Controller,也叫后端控制器)将得到的参数进行处理并返回结 果给处理器映射器(HandlerMapping) 7. 处 理 器 适 配 器 ( HandlerAdapter ) 将 得 到 的 结 果 返 回 给 前 端 控 制 器 (DispatcherServlet) 8. DispatcherServlet( 前 端 控 制 器 ) 将 ModelAndView 传 给 视 图 解 析 器 (ViewReslover) 9. 视图解析器(ViewReslover)将得到的参数从逻辑视图转换为物理视图并返回给 前端控制器(DispatcherServlet)10. 前端控制器(DispatcherServlet)调用物理视图进行渲染并返回 11. 前端控制器(DispatcherServlet) ## mybaits怎么防止sql注入 一种利用应用程序未正确过滤用户输入数据,导致恶意SQL代码被执行的安全漏洞 \#{} 是占位符,预编译处理,${}是字符串替换。 ## SpringCloud Alibaba常用组件有哪些?有什么作用 nacos 配置中心和注册中心 gateway 网关 sentinel 熔断限流 openfein 远程调用,服务降级 ## nacos作为配置中心,如何实现热更新? - 通过@Value注解注入,结合@RefreshScope来刷新 - 通过@ConfigurationProperties注入,自动刷新 ## 什么是服务的熔断?什么是服务的降级? - **`服务熔断`**的作用类似于我们家用的保险丝,当某服务出现不可用或响应超时的情况时,为了防止整个系统出现雪崩,暂时停止对该服务的调用。 - **`服务降级`**是从整个系统的负荷情况出发和考虑的,对某些负荷会比较高的情况,为了预防某些功能(业务场景)出现负荷过载或者响应慢的情况,在其内部暂时舍弃对一些非核心的接口和数据的请求,而直接返回一个提前准备好的fallback(退路)错误处理信息。这样,虽然提供的是一个有损的服务,但却保证了整个系统的稳定性和可用性。 ## nacos的服务注册和发现是怎么样的 | 服务注册流程 | 服务发现流程 | | ------------------------------------------------------------ | ------------------------------------------------------------ | | **启动与注册请求**:当一个服务实例启动时,它向Nacos注册中心发送注册请求,这个请求包含了服务的基本信息,如服务名、IP地址、端口等 | **服务查询**:当一个服务消费者需要调用其他服务时,它会向Nacos注册中心发起服务查询请求,询问所需服务的可用实例信息 | | **注册表存储**:Nacos注册中心收到注册请求后,会在其内部的注册表中记录这些信息。这种注册表是Nacos用来跟踪和管理所有服务实例的关键数据结构 | **返回实例列表**:Nacos注册中心从其注册表中检索相应服务的当前可用实例列表,并将其返回给服务消费者。这一列表考虑了各种因素,如网络延迟和负载情况,以实现负载均衡和高可用性 | | **心跳维护**:注册成功后,服务实例会定期发送心跳给Nacos注册中心以表明自己还处于活跃状态。默认情况下,心跳间隔为5秒,如果15秒内无心跳且健康检查失败,则该实例会被认定为不健康 | **服务调用**:服务消费者根据返回的实例信息,选择一个合适的服务实例直接进行API调用,完成服务的消费过程 | | **健康检查**:Nacos服务器会定期对注册的服务实例进行健康检查,确保它们能够处理请求。如果健康检查连续失败,则该服务实例会被从注册表中移除 | **动态更新**:服务消费者可以通过订阅模式实时感知服务提供者的变更,一旦服务提供者的状态发生变化,Nacos会立即通知相关的服务消费者,确保服务调用的及时性和准确性 | ## SpringBoot的自动装配原理 在Spring Boot项目中有一个注解@SpringBootApplication,这个注解是对三个注解进行了封装:@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan 其中@EnableAutoConfiguration是实现自动化配置的核心注解。 该注解通过@Import注解导入AutoConfigurationImportSelector,这个类实现了一个导入器接口ImportSelector。在该接口中存在一个方法selectImports, 该方法的返回值是一个数组,数组中存储的就是要被导入到spring容器中的类的全类名。在AutoConfigurationImportSelector类中重写了这个方法, 该方法内部就是读取了项目的classpath路径下META-INF/spring.factories文件中的所配置的类的全类名。 在这些配置类中所定义的Bean会根据条件注解所指定的条件来决定是否需要将其导入到Spring容器中。 ## Spring的传播行为/特性 - 如果当前没有事务,就新建一个事务。如果当前存在事务,则加入这个事务。这是默认的传播行为 - 支持当前事务,如果当前没有事务,就以非事务方式执行。 - 强制要求当前存在事务,如果不存在则抛出异常 - 新建一个事务,如果当前存在事务,则将当前事务挂起。 - 以非事务方式执行操作,如果当前存在事务,则将当前事务挂起 - 以非事务方式执行操作,如果当前存在事务,则抛出异常 - 如果当前存在事务,则在嵌套事务内执行。嵌套事务是一个单独的事务,它有自己的保存点(Savepoint),可以独立提交或回滚。如果外部事务提交,嵌套事务也会提交;如果外部事务回滚,嵌套事务可以回滚到它的保存点。 ## spring事务失效的情况 Spring事务的原理是AOP,进行了切面增强,那么失效的根本原因是这个**AOP**不起作用了!常见情况有如下几种 1. 发生自调用,类里面使用this调用本类的方法(this通常省略),此时这个this对象不是代理类,而是该类的实例对象本身! 解决方法很简单,让那个this变成该类的的代理类实例对象即可! 2. 方法不是public的 3. 数据库不支持事务 4. 没有被Spring管理 5. 异常被吃掉,事务不会回滚(或者抛出的异常没有被定义,默认为RuntimeException) ## Rabbitmq的架构是怎么样的 1. **生产者(Producer)**: - 生产者是消息的发送者,将消息发送到 RabbitMQ 的交换机(Exchange)。 2. **交换机(Exchange)**: - 交换机是 RabbitMQ 接收消息的地方,负责将消息路由到一个或多个队列。 - 根据不同的路由规则(Exchange Type),交换机可以将消息发送到指定的队列,或者直接发送给绑定的消费者。 3. **队列(Queue)**: - 队列是消息的缓冲区,存储生产者发送的消息,直到消费者准备好接收并处理它们。 - 消息进入队列后,等待消费者来获取并处理。 4. **绑定(Binding)**: - 绑定是交换机和队列之间的关系,它定义了如何路由消息到特定的队列。 - 绑定可以根据路由键(Routing Key)将交换机的消息发送到匹配的队列。 5. **消费者(Consumer)**: - 消费者是从队列中接收消息并进行处理的应用程序或服务。 ![image-136.png](assets/image-136.png) ### 工作流程 RabbitMQ 的工作流程通常包括以下步骤: 1. **生产者发送消息**: - 生产者将消息发布到 RabbitMQ 的交换机。 - 在发布消息时,指定交换机的名称和可能的路由键。 2. **交换机处理消息**: - 根据交换机的类型(如直接交换机、主题交换机、扇出交换机等),交换机将消息路由到一个或多个队列。 - 每个队列可能有一个或多个绑定,用于指定如何将消息路由到该队列。 3. **队列存储消息**: - 消息被存储在队列中,等待消费者来获取和处理。 4. **消费者接收消息**: - 消费者订阅并从队列中获取消息。 - 处理完消息后,消费者可以发送确认(ACK)给 RabbitMQ,表示消息已经被成功处理。 5. **消息确认**: - RabbitMQ 可以根据配置要求消费者发送确认,以确保消息被可靠地处理和传递。 - 如果消费者处理消息失败或者出现问题,消息可以重新排队或者根据策略进行死信处理(Dead Letter Exchanges)。 ## Kafaka的架构是怎么样的 ![image-187.png](assets/image-187.png) 1. **Broker(代理)**: - Kafka 集群中的每个服务器节点被称为 Broker。 - 每个 Broker 负责管理一部分数据的存储和处理请求。 - Broker 之间可以水平扩展,组成一个 Kafka 集群,共同处理数据流和保证高可用性。 2. **Topic(主题)**: - 每条发布到 Kafka 集群的消息都有一个特定的类别,称为 Topic。 - Topic 是消息的逻辑容器,用于分类和分区消息。 - 生产者将消息发布到 Topic,而消费者订阅 Topic 并从中获取消息。 3. **Partition(分区)**: - 每个 Topic 可以被分成一个或多个 Partition,每个 Partition 是一个有序的、不可变的消息序列。 - Partition 使得 Kafka 集群能够水平扩展,因为每个 Partition 可以分布在不同的 Broker 上。 - 每个 Partition 在物理上对应一个目录,其中包含该 Partition 的所有消息和索引文件。 4. **Producer(生产者)**: - 生产者负责向 Kafka Broker 发布消息到指定的 Topic。 - 生产者将消息发送到指定 Topic 的一个或多个 Partition 中,根据消息的 Key 进行负载均衡。 - 生产者可以选择同步或异步方式发送消息,并且可以配置消息的持久性和传递语义。 5. **Consumer(消费者)**: - 消费者订阅一个或多个 Topic,并从 Broker 中拉取(pull)消息。 - 消费者组可以由一个或多个消费者实例组成,每个消费者实例可以处理一个或多个 Partition 中的消息。 - 消费者可以控制消息的消费速率,并跟踪每个 Partition 中已经消费的消息的偏移量(offset)。 6. **Offset(偏移量)**: - 每个 Partition 中的消息都会分配一个唯一的、顺序递增的偏移量。 - 消费者使用偏移量来标识它们已经处理的消息位置,以便在需要时能够恢复和重新处理消息。 ### 工作原理 Kafka 的工作流程包括以下步骤: 1. **生产者发布消息**: - 生产者将消息发送到指定的 Topic 中,根据 Partition 的分配策略将消息分布到对应的 Partition 中。 2. **消息存储和分发**: - 消息被存储在 Broker 的 Partition 中,每个消息都带有偏移量,保证了消息在 Partition 中的顺序性。 3. **消费者订阅和处理消息**: - 消费者订阅感兴趣的 Topic,并从 Broker 拉取消息。 - 消费者通过管理偏移量来跟踪自己消费到的位置,并根据需要处理消息。 4. **水平扩展和容错**: - Kafka 集群可以水平扩展,增加 Broker 来增强处理能力和提高容错性。 - 分区和复制机制确保了消息的高可用性和持久性,即使其中一些 Broker 失效也不会丢失消息。 5. **持久性和可靠性**: - Kafka 提供了配置选项来控制消息的持久性和传递保证,如消息复制和持久化设置。 - 消息一旦被提交,就会被持久化到磁盘,保证了消息不会因为 Broker 失败而丢失。 ## Rabbitmq的工作模式有哪些 | | 工作模式 | | ------------------------- | ------------------------------------------------------------ | | 简单模式 | 在简单模式下,消息生产者(P)将消息发送到默认消息队列(queue),消息消费者从队列中获取并消费消息 | | 工作队列模式 | 多个消费者可以订阅同一个队列,会按照平均或公平的方式将消息分发给各个消费者 | | 发布/订阅模式(广播模式) | 将消息发送到交换机,多个队列可以绑定到同一个交换机,并以广播的形式接收生产者发布的消息。所有订阅了该交换机的队列都能接收到相同的消息,实现了一对多的消息传递 | | 路由模式 | 交换机根据消息的路由键(routing key)来决定将消息发送到哪个队列。只有与路由键匹配的队列才能接收到消息 | | 主题模式 | 允许使用通配符来匹配路由键,“*.log”可以匹配所有以“.log”结尾的路由键 | ## RabbitMQ怎么处理消息堆积的 1. 消息堆积的流程一般是 先进行正常投递,投递失败后进入重试队列,重试队列绑定正常队列,过30秒后投递到正常队列,达到重试次数后进入失败队列,重试次数根据消息头的次数来判断,进入失败队列后手动就行消费即开多线程消费,再失败后进入死信队列, 2. 消息失败重发的 消息投递失败后进入队列,队列达到一定数量后批量入库,开定时器来投递这些失败消息,先用redis的分布式锁进行锁定保证定时器只处理一次,然后利用一个时间字段来保证幂等信 保证不会重复消费, 3. 先查询出两百条,在一条条投递,投递使用trycatch,失败后自动重试处理下一条消息,并修改投递失败的状态,最后再批量删除投递成功的数据 ## RabbitMQ怎么保证消息不丢失 ## RabbitMQ怎么保证消息的消费顺序 ## Kafaka怎么保证消息不丢失 ## Kafaka怎么保证实现的消费顺序 ## InnoDB 和MyISAM 的区别 1.InnoDB 存储引擎 **特点和原理:** - **事务支持:** InnoDB 是 MySQL 中唯一支持事务(ACID特性)的存储引擎。它使用 MVCC(多版本并发控制)来管理事务的并发访问,可以实现较高的并发性和隔离级别。 - **行级锁:** InnoDB 使用行级锁,可以在并发访问时只锁定需要的行,而不是锁定整个表,从而提高了并发性和性能。 - **外键约束:** 支持外键约束,保证数据的完整性和一致性,可以在数据层面实现关系数据库的特性。 - **聚簇索引:** InnoDB 的主键索引(或者首个唯一索引)实际上是数据行的物理存储顺序,因此称为聚簇索引。这种设计有利于范围查询和按主键顺序存取数据,但也可能导致插入更新操作的性能损失。 - **崩溃恢复:** 支持崩溃恢复和事务回滚,能够保证数据的一致性和持久性。 - **适用场景:** 适合于需要事务支持、并发性能要求高、需要外键约束和崩溃恢复的应用场景,如事务处理系统、OLTP(联机事务处理)系统等。 2. MyISAM 存储引擎 **特点和原理:** - **非事务性:** MyISAM 不支持事务和回滚功能,它的操作是原子性的,不支持崩溃恢复和事务的持久性。 - **表级锁:** MyISAM 使用表级锁,对整张表进行锁定,这会导致在高并发环境下性能受限。 - **索引结构:** MyISAM 使用 B+ 树索引结构,主键索引和数据行分离存储,主键索引指向数据行的物理位置,非主键索引直接存储数据行的地址。 - **不支持外键约束:** 不支持外键约束,无法保证数据的完整性和一致性,关系完整性需在应用层控制。 - **适用场景:** 适合于读密集的应用场景,如数据仓库、日志分析等,对事务要求不高、并发访问较少的应用。 ## 数据库表设计注意是什么 1、注意选择存储引擎,如果要支持事务需要选择InnoDB。 2、注意字段类型的选择,对于日期类型如果要记录时分秒建议使用datetime,只记录年月日使用date类型,对于字符类型的选择,固定长度字段选择char,不固定长度的字段选择varchar,varchart比char节省空间但速度没有char快;对于内容介绍类的长广文本字段使用ext或longtext类型;如果存储图片等二进制数据使用blob或longblob类型;对金额字段建议使用DECIMAL;对于数值类型的字段在确保取值范围足够的前提下尽量使用占用空间较小的类型, 3、主键字段建议使用自然主键,不要有业务意义,建议使用int unsigned类型,特殊场景使用bigint类型。 4、如果要存储text、blob字段建议单独建—张表,使用外键关联。 5、尽量不要定义外键,保证表的独立性,可以存在外键意义的字段。6、设置字段默认值,比如:状态、创建时间等。 7、每个字段写清楚注释。 8、注意字段的约束,比如:非空、唯一、主键等。 ## Mysql 的调优 首先应该考虑进行sql优化 定位慢sql,要开启慢查询日志,用show proflie查询慢sql语句 用explain分析索引命中情况 建立合适的索引 - 字段的数值有唯一性的限制:建立唯一索引/主键索引-->学号 - 频繁作为 WHERE 查询条件的字段 - 经常 GROUP BY 和 ORDER BY 的列,都有则可以建立联合索引 - UPDATE、DELETE 的 WHERE 条件列 - DISTINCT 字段需要创建索引 - 多表 JOIN 连接操作时,对用于连接的字段创建索引 - 使用列的类型小的创建索引 - 使用字符串前缀创建索引(一般选择前20个字符) - 区分度高(散列性高)的列适合作为索引 - 使用最频繁的列放到联合索引的左侧 避免索引失效 1. 计算或函数 2. LIKE以%开头 3. 不等号!= <> 4. is not null 和is null 5. 类型转换 6. 索引中范围条件右边列失效 7. or语句中某条件未使用索引 尽量选择合适的列查询,避免使用select * 能用单表就用单表,选择用小表驱动大表,能用关联查询的不要用子查询,不能使用笛卡尔积查询 其次才是考虑加redis缓存,硬件优化,分库分表 ## Redis常用的数据结构 | | 数据结构 | | ---------------------- | ----------------------------------------- | | 字符串(String) | 存储key -value 存储登录的token和用户信息 | | 哈希表(Hash) | | | 列表(List) | | | 集合(Set) | | | 有序集合(Sorted Set) | | | 位图(Bitmap) | | | HyperLogLog | | | | | | | | ## Redis的持久化 Redis 提供了两种持久化的方式,分别是 RDB(Redis DataBase)和 AOF(AppendOnly File)。 RDB,简而言之,就是在不同的时间点,将 redis 存储的数据生成快照并存储到磁盘等介质上。 AOF,则是换了一个角度来实现持久化,那就是将 redis 执行过的所有写指令记录下 来,在下次 redis 重新启动时,只要把这些写指令从前到后再重复执行一遍,就可以实现数据恢复了。 RDB 和 AOF 两种方式也可以同时使用,在这种情况下,如果 redis 重启的话,则会 优先采用 AOF 方式来进行数据恢复,这是因为 AOF 方式的数据恢复完整度更高 ## 什么是Redis的集群 它通过将数据分散存储在多个Redis节点上,并利用主从复制和自动故障转移等机制,确保在节点故障或网络分区的情况下,系统仍能持续提供服务 , Redis集群中,整个键空间被划分为**16384**个哈希槽,而不是直接对节点进行哈希。每个键都通过一个**CRC16校验**后,再对16384取模来确定它属于哪个哈希槽。随后,集群中的每个Redis节点都会被分配一部分哈希槽来负责管理 **为什么是16384个槽位** 当槽位数量为65536时,myslots数组的大小会达到8KB,这会使得每次发送的心跳包变得非常庞大,从而消耗大量的网络带宽 ## Redis的主从复制 Redis 的主从复制是一种数据复制机制,用于在多个 Redis 服务器之间实现数据同步。 - master 以写为主,Slave 以读为主 - 当 master 数据变化的时候,自动将新的数据异步同步到其它 slave 数据库 搭建步骤: 1. 开启daemonize yes 2. 注释掉bind 127.0.0.1 3. protected-mode no 4. 指定端口 5. 指定当前工作目录,dir 6. pid文件名字,pidfile 7. log文件名字,logfile 8. requirepass 123456 9. dump.rdb名字 10. 从机的配置添加主机ip 11. 从机配置添加主机密码 ## 主从复制的四种模式 | | 主从复制 | | -------- | ------------------------------------------------------------ | | 一主二仆 | 主机复制写,从机复制读,从机不论是掉队还是重启都可以拉取到主机的所有数据 | | 改换门庭 | 三个都是主机,可以临时设置谁为主机,谁为从机 | | 薪火相传 | 比如A是B的主机,B是C的主机,那么A的数据也会同步到C上面 | | 自立为王 | 通过 SLAVEOF no one 设立自己为主机 | ## 主从复制的缺点、Redis的哨兵机制 当主机挂掉之后,如果不马上进行重启,那么redis集群就只能读而不能进行写了,因此需要哨兵来选举一个从节点当作主节点,哨兵也需要搭建集群来投票觉得主机是否是挂掉了,如果挂掉了就再投票选举一个从节点为新的主节点 **配置哨兵的步骤** 1. 在多个服务器配置 sentinel.conf 2. 启动哨兵集群./redis-sentinel /root/myredis/sentinel26379.conf --sentinel ## 什么是分布式锁,怎么用redis解决分布式锁 **分布式锁**:分布式系统内,跨网络的多个节点上的多个线程并发访问互斥资源,分布式锁用于保证资源被互斥访问,协调线程间的访问顺序 **用的redisson步骤:** - redisson.getLock("myLock"); - lock.tryLock(100, 10, TimeUnit.SECONDS) - lock.unlock(); Redisson 可以自动续期锁的持有时间,防止因为长时间任务导致锁自 --看门狗机制 Redisson 的看门狗机制(Watchdog)是一种用于自动延长分布式锁过期时间的机制。它可以防止因为意外情况导致锁被提前释放的问题,由于看门狗机制的存在,Redisson 会自动延长锁的过期时间,使得锁在持有期间不会被释放,从而保证了长时间操作的安全性。 **看门狗机制的工作原理** 1. **获取锁时启动看门狗**: 当一个客户端获取到分布式锁时,Redisson 会启动一个看门狗(Watchdog)线程。这个线程会定期检查锁的持有状态。 2. **自动延长锁的过期时间**: 看门狗线程每隔一段时间(默认是锁的有效期的 1/3)会自动为锁延长过期时间。默认的锁过期时间是 30 秒,但看门狗会在 10 秒(30 秒的 1/3)后自动将锁的过期时间延长到 30 秒,以确保锁在客户端持有期间不会被其他客户端获取。 3. **释放锁时停止看门狗**: 当客户端显式释放锁时,看门狗线程会停止运行,不再延长锁的过期时间。 ## 什么是缓存穿透,缓存雪崩、缓存击穿问题?你是怎么解决的 | | 缓存穿透 | 缓存雪崩 | 缓存击穿 | | ---- | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | | 现象 | 缓存穿透是指恶意请求或者大量的无效查询导致缓存中不存在的数据被频繁请求,从而绕过缓存直接访问数据库 | 缓存雪崩是指在某个时间段,缓存集中过期失效,导致大量请求直接打到后端数据库 | 缓存击穿是指针对一个热点数据的大量并发访问,导致缓存失效,每个请求都直接访问后端数据库 | | 解决 | **布隆过滤器(Bloom Filter)**:在缓存层面使用布隆过滤器来过滤掉不存在的键,减少无效查询对后端的访问压力。**缓存空值** | 缓存失效时间随机化,热点数据永不过期或者通过监控手段提前刷新缓存,限流和降级 | **加锁**:对热点数据的访问进行加锁,保证只有一个请求可以访问后端服务,其他请求等待结果 Redssion解决缓存击穿 | ## 什么是正向代理?什么是方向代理 **正向代理**(隐藏了真实的客户端),就是一个位于客户端和原始服务器之前的服务器,为了从原始服务器取得内容,客户端向代理发送一个请求并且指定目标(原始服务器),然后代理向原始服务器转交请求并将获得的内容返回给客户端,客户端才能使用正向代理 ![image-991.png](assets/image-991.png) 假设A和B是同学,但平时并不是很熟,A向B借钱,被B拒绝了。 此时A联系了C,C是A的好朋友,C和B也很熟。 C向B借了钱并且给了A。 此时A拿到了B的钱,但B并不知道他的钱借给了A。 这时B同学扮演了一个非常关键的角色,就是代理,也可以说是正向代理 **反向代理**(隐藏真实服务端) 反向代理服务器位于用户与目标服务器之间,但是对于用户而言,反向代理服务器就相当于目标服务器,即用户直接访问反向代理服务器就可以获得目标服务器的资源。同时,用户不需要知道目标服务器的地址,也无须在用户端作任何设定。反向代理服务器通常可用来作为Web加速,即使用反向代理作为Web服务器的前置机来降低网络和服务器的负载,提高访问效率 ![image-198.png](assets/image-198.png) ## 怎么用nginx实现方向代理 首先,需要在 Nginx 配置文件中设置负载均衡。这通常是在 `http` 块中定义一个 `upstream` 块,列出后端服务器,然后在 `server` 块中使用该 `upstream` 进行代理 ,**server** 配置端口号 **proxy_pass** 配置反向代理 地址 **proxy_set_header** 配置方向代理头等 **负载均衡有** | 轮询 | | | ----------------- | ------------------------------------------------------------ | | 权重 | 权重越高的服务器接收的请求越多 | | 最少连接 | 将请求分配给连接数最少的服务器 | | IP 哈希 (IP Hash) | 根据客户端 IP 地址分配请求,确保同一 IP 地址的请求总是发送到同一台服务器 | | URL 哈希 | 根据请求的 URL 分配请求,确保相同的 URL 请求总是发送到同一台服务器 | ```nginx http { upstream backend { //配置负载均衡 server backend1.example.com; server backend2.example.com; server backend3.example.com; } server { //配置端口 listen 80; location / { proxy_pass http://backend; //配置方向代理地址 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } } ``` ## Jenkins是如何搭建持续集成方案的 CI/CD 持续集成和持续交付/持续部署,简单来说就是通过Jenkins和git将提交的代码直接部署到测试环境当中,节省了每次开发完成之后需要部署项目的繁琐步骤 **如何搭建** 1. 安装Jenkins 通过yum安装即可 2. 配置 Jenkins 启动jenkins并初始化配置好内容,包括git配置,和maven打成jar,配置shell脚本实现自动构建出发 ## Linux的常用命令是哪些 | **文件和目录操作** | **文本文件处理** | **系统管理** | **权限管理** | **包管理和软件安装** | **其他常用命令** | | ---------------------------------- | ------------------------------------------------------- | ------------------------------------------ | -------------------------------- | ----------------------------------------------- | ---------------------------- | | **ls -l** 列出详细信息 | **cat** 查看文件内容 | **ps aux** 显示详细进程信息 | **chmod** 修改文件或目录的权限 | sudo apt-get install 用于安装、升级和删除软件包 | **wget** 下载文件 | | **cd** 切换当前工作目录 | **less** 以交互方式查看文件内容,支持向前和向后滚动查看 | **top** 实时显示系统资源占用情况和进程信息 | **chown** 修改文件或目录的所有者 | yum 用于安装、升级和删除软件包 | **tar** 压缩和解压缩文件 | | **pwd** 显示当前工作目录的绝对路径 | **head** 显示文件头部内容,默认显示前 10 行 | **kill** 终止指定进程。 | **chgrp** 修改文件或目录的所属组 | | **ssh** 远程登录到其他计算机 | | **mkdir** 创建新目录 | **tail** 显示文件尾部内容,默认显示末尾 10 行 | **df** 显示磁盘空间使用情况 | | | | | **rm** 删除文件或目录 | **grep** 在文件中搜索指定模式的字符串 | **du** 显示目录或文件的磁盘使用情况 | | | | | **cp** 复制文件或目录 | | **ifconfig** 显示和配置网络接口信息 | | | | | **mv** 移动文件或目录 | | **ping** 测试与主机的连通性 | | | | | **touch** 创建空文件或更新文件 | | | | | | ## docker怎么进行部署的 1. 首先如果部署开发环境如Mysql 2. 拉取 MySQL 镜像 docker pull mysql:latest 3. 启动 MySQL 容器 docker run 后面加一些参数 如 - `-d`:表示以后台模式运行容器。 - `--name my-mysql-container`:给容器指定一个名称。 - `-e MYSQL_ROOT_PASSWORD=my-secret-pw`:设置 MySQL 的 root 用户的密码。 - `-p 3306:3306` - ` -v /path/on/host:/var/lib/mysql `将容器里面的数据挂载在linux/path/on/host上 **docker如何部署springboot项目** 1. 编写 Dockerfile ```dockerfile # 使用官方的 OpenJDK 11 镜像作为基础镜像 FROM openjdk:11-jdk-slim # 设置工作目录 WORKDIR /app # 将打包好的 JAR 文件复制到工作目录 COPY target/my-spring-boot-app.jar /app/app.jar # 暴露应用端口 EXPOSE 8080 # 定义容器启动时运行的命令 CMD ["java", "-jar", "app.jar"] ``` 2. 构建 Docker 镜像 `docker build -t my-spring-boot-app .` 3. 运行 Spring Boot 应用程序容 `docker run -d -p 8080:8080 my-spring-boot-app` ## Docker之componse的作用和常用使用命令?有什么缺点 Docker Compose 是一个用于定义和运行多容器 Docker 应用程序的工具。它通过一个 YAML 文件来配置应用的服务 **常用使用命令:** - `docker-compose up`:启动应用程序的所有服务。 - `docker-compose down`:停止并移除应用程序的所有服务。 - `docker-compose build`:构建或重新构建服务。 - `docker-compose start`:启动已经创建的服务。 - `docker-compose stop`:停止已经创建的服务。 - `docker-compose restart`:重启应用程序的所有服务。 - `docker-compose exec `:在运行中的服务中执行命令。 - `docker-compose logs`:查看服务的日志输出 **缺点:** - 随着服务的增多和复杂性的提升,Compose 文件可能变得冗长和复杂 - Docker Compose 在大规模和高负载场景下可能会有性能瓶颈,不适合于大规模的部署和高并发场景 - 在使用 Docker Compose 进行开发和测试时,有时可能无法完全模拟生产环境的复杂性和隔离性 - Docker Compose 默认是单节点管理,如果主机或者 Docker 守护进程出现故障,可能会导致整个应用程序不可用 ## HTML的标签有哪些 | | | | | | | | -------------- | ---------------------------------- | ---------------------------------------------------- | ------------------------------------------------------------ | ----------------------------- | ------------------------------------ | | 结构标签 | **``**:声明文档类型。 | **``**:定义 HTML 文档的根元素。 | **``**:包含页面的元数据,如标题、字符集声明、外部样式表和脚本 | **``**:定义文档的标题 | **`<body>`**:包含可见的页面内容 | | 文本格式化标签 | <p>:定义段落 | **`<h1>` to `<h6>`**:定义标题,级别从大到小逐级减小 | **`<br/>`**:换行 | **`<hr>`**:水平线 | | | 列表标签 | 无序列表 (`<ul>`, `<li>`) | 有序列表 (`<ol>`, `<li>`) | | | | | 链接和图像标签 | <a>:定义超链接 | <img>:定义图像 | | | | | 表格标签 | <table>:定义表格 | **`<tr>`**:定义表格行 | **`<td>`**:定义表格数据单元格。 | **`<th>`**:定义表头单元格。 | | | 表单标签 | **`<form>`**:定义表单 | **`<input>`**:定义输入字段 | **`<textarea>`**:定义文本域 | **`<button>`**:定义按钮 | **`<option>`**:定义下拉列表中的选项 | | 多媒体标签 | <audio>:定义音频内容 | <video>:定义视频内容 | | | | | 语义化标签 | **`<header>`**:定义文档的头部区域 | **`<footer>`**:定义文档的尾部区域 | **`<nav>`**:定义导航链接 | <div>:定义文档中的分区或区块 | **`<span>`**:定义行内文本的分区 | ## VUE的生命周期 1. **beforeCreate** - 在实例初始化之后,数据观测 (`data` 属性和 `methods` 方法) 和事件配置 (`events` 选项) 之前调用。 - 此时实例的属性和方法还未初始化。 2. **created** - 实例已经创建完成,此时已经完成数据观测 (`data` 属性和 `methods` 方法) 和事件配置 (`events` 选项)。 - 可以在这里进行数据获取、初始化操作,但是挂载阶段还未开始,DOM 未渲染。 3. **beforeMount** - 在挂载开始之前被调用:相关的 render 函数首次被调用。 - 可以在这里修改 render 的返回内容,或者在渲染之前再次更改数据。 4. **mounted** - el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。 - 可以在这里访问到 vm 实例以及 mounted 后的 DOM。 5. **beforeUpdate** - 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。 - 可以在这里进一步地更改数据,但不会触发附加的重渲染过程。 6. **updated** - 由于数据更改导致的虚拟 DOM 重新渲染和打补丁后调用。 - 通常用于更新 DOM 依赖于数据的状态。 7. **beforeDestroy** - 实例销毁之前调用。在这一步,实例仍然完全可用。 - 可以在这里进行一些清理工作,比如清除定时器、解绑全局事件等。 8. **destroyed** - 实例销毁后调用。在这一步,Vue 实例的所有指令和事件监听器都已被移除,所有子实例也已被销毁。 - 可以在这里进行一些清理工作,比如清空数据、销毁非 Vue 插件实例等。 # --------------------------------------------------------------- # ----------------------------------------------------------------- # 人事面试问题--------------------------------------------- ## 你上家公司是哪个?做什么业务的 广东俊辰大数据科技有限公司 ,有大数据服务监控平台的,那个是大数据部门的,我那个部门是项目开发部,主要是用Java语言开发,会接一些外包项目,像最近做的畅听天下 ## 那你为什么不去大数据部门的监控平台呢 主要我去的时候那个大数据部分成员已经满,而且那个监控平台也开发好了,不需要人了,公司要拓展业务,就有这样的项目开发部来接外包项目 ## 地点哪里,附近有什么好玩的 - 东莞市南城街道莞太路南城段金融大厦 那里跟我的大学很久,平时也没怎么去哪,放假的话可能会去植物园和观音山啥的逛逛 ## 你为什么选择这家公司 因为那就公司有我学长,然后就内推我过去,我当时也有其他公司的offer但是,学长说那个项目氛围很好,对我技术成长也有帮助,然后就选择了这家 ## 为什么离职 我挺喜欢我那个项目开发部的,毕竟呆了三年了,主要是现在经济效益不太好,公司也接不到什么项目,然后我们老板就让我们出去外面找找机会,当然也给了赔偿,整个项目开发部都解散了,产品经理就没 ## 项目的组成人员有哪些?多少人 产品经理 1 前端 2个 后端 4个 运维 1个 一共 8人 ## 你对加班的看法 我现在单身,没有任何家庭负担,可以全身心的投入工作。同时,我也会提高工作效率,减少不必要的加班。 ## 你工作的时候学到了什么 ## 你遇到很难的问题是怎么解决的