1.整理华子面经--1

细说项目中遇到的难点?

C++项目:

撤销恢复模块:

拥有三个属性:

    QQueue<T> memo;//用于存储中间的逻辑的双向队列
	int memoIndex;//用于找到当前位置前一个状态的索引
	int capacity;//用于约束双向队列的行为,容量

主要函数:

  • 有参初始化指定容量,无参初始化指定容量为Integer_Max;
  • 获取上一个状态,获取当前状态
  • 查看index是否在左右端点处
  • 当传入一个新的备忘录对象的时候,当前索引之后的存储在队列中的对象,然后加入该对象
  • 清空备忘录

底层模型数据结构设计,级联更新

以一个六面体网格为例,他需要保存的网格信息有,网格节点,网格线,网格面,网格单元,部件信息,这四者之间的关系是:

网格节点/网格线/网格面拥有它被哪些网格单元共有的信息,节点索引信息

网格单元/网格面/网格线有构成自己的网格节点顺序索引

网格节点拥有位置信息,是以上结构的基础

Java项目:

遇到的困难:人人开源返回类R的问题,Feign远程调用丢失请求头问题

项目经典逻辑:

合并购物车功能

背景:购物车分为离线购物车和在线购物车,离线购物车可以购买商品,在用户登录以后,离线购物车会被清空,所有商品被转移到在线购物车

  • 存储:购物车数据放在redis中做持久化策略,因为经常要进行写操作,所以不适合放在mysql中
  • ThreadLocal和Session和拦截器的使用:购物车服务中配置cookie,扩大cookie的作用域,使用springSession的redis类型作为session的存储,
  • 在拦截器的preHandler方法中对Session中的用户对象进行处理(在登录模块放入),如果对象不为空,则为已经登录的用户,标记为可以合并购物车,从请求中拿到cookie区分用户,处理成购物车服务需要使用的对象Vo,放入ThreadLocal中避免对象多次跨方法传输
  • 在拦截器的postHandler方法中获取到threadlocal中的对象,通过其属性值判断是否是临时用户或者第一次使用购物车的用户,此类用户需要设置cookie(常量key,用户id),在方法的最后remove对象

定时关单逻辑+自动解锁库存逻辑

秒杀逻辑

定时上架商品:定时任务扫描mysql将商品信息预热到redis中,考虑到分布式幂等性的问题,使用redisson分布式锁来实现

存储结构:一共有两块信息

  • <场次+id,随机码>,随机码的作用是防止链接泄漏,提前拿到链接进行秒杀
  • <随机码,商品数量>,使用redisson的分布式信号量来防止超卖

用户排重:用户如果已经进行了秒杀,使用一个key去redis中进行占位,以此来防止重复秒杀

异步削峰处理:在用户秒杀成功后,给rabbitMq发送消息,rabbitMq监听接口在拿到秒杀信息后,在数据库保存订单相关的结构

提交订单按钮的幂等性保证

在转到订单页的时候,在页面上存储一个token并且在redis中也存放一个token,在点击按钮的时候尝试删除这个token,如果删除成功就执行后续的操作,否则快速失败,其中检验token和删除token使用LUA脚本来保证原子操作

线程的优先级?操作系统线程和JVM线程?

Java中优先级分为1-10个级别,优先级高的线程得到的cpu资源多,线程优先级有继承特性,如果a线程启动了b线程,那么b线程将继承a线程的优先级

现在java中线程的本质其实就是操作系统中的线程,操作系统中进程的状态分为:创建,就绪,运行,等待和销毁,java的线程状态有创建,运行,等待,超时等待,阻塞和销毁

CAS怎么进行交换?

当要对变量进行修改的时候,会先将内存位置的值与预期的变量原值进行比较,如果一致就将内存位置更新为新值,否则不做操作,Java中的CAS的底层是Unsafe类和自旋锁,unsafe类中方法都是native修饰,不属于java代码,最后映射到操作系统的指令是cmpxchg;

数据库执行一条查询语句的过程?

  • mysql执行器根据执行计划调用存储引擎的api查询数据
  • 存储引擎先从缓存池buffer pool中查询数据,如果没有再去磁盘中查询,如果查询到了就将其放入缓存池中
  • 在数据加载到buffer poll中的同时,会将这条数据的原始记录保存到undo log日志文件中
  • innodb会在buffer poll中执行更新操作,更新后的数据会记录在redo log buffer中
  • 提交事务在提交的同时会做三件事:将redo log buffer中的数据刷入redo log文件,将本次操作记录写入到bin log文件,将bin log文件名字和更新内容在bin log的位置记录到redo log中,同时在redo log最后添加commit标记
  • 使用一个后台线程,它会在某个时机将我们buffer pool中的更新后的数据刷到mysql数据库中,这样保证内存和数据的统一

如何设计单点登录?

同域名下的单点登录:cookie的domain设置为当前父域,并且父域的cookie会被子域共享

不同域名下的单点登录:

方案一:由于不同域下的cookie不共享,可以部署一个认证中心,用于专门处理登录请求的独立web服务,用户在认证中心登录成功后,认证中心记录其登录状态,创建授权令牌,然后将授权令牌作为参数发送给各个子系统,应用系统通过检查当前请求有无token来了解用户登录状态;

方案二:在前后端分离框架中,将SessionId或者token保存到LocalStorage中,让前端每次向后端发送请求,主动将localStorage的数据传递给服务器,localstorage是html5的新特性,主要用于本地存储

 最快最稳定的排序算法

Timsort结合了插入排序和归并排序,在数据量较少(<64)的情况下使用插入排序,否则使用归并排序+二分搜索来提高排序效率

 Stream流式编程

  • filter(T->boolean):保留boolean为true的元素
  • distinct():去除重复元素,这个方法通过类的equals方法来判断元素相等,所以需要重写equals方法
  • sorted((T,T)->int):排序
  • limit(long n):返回前n个元素
  • skip(long n):去除前n个元素
  • map(T->R):把流中的每个元素T映射为R
  • flatMap(T->stream):将流中的元素T映射为一个流
  • boolean anyMatch/allMatch/noneMatch(T->boolean):流中是否有一个/所有/没有元素满足boolean条件
  • reduce(T(初始值,可以没有),(T,T)->T):用于计算求和,最大值等
  • count():返回流中元素个数,结果为long类型
  • collect():收集器接口
  • forEach:遍历

例题:筛选出年龄小于30并且id为偶数的用户名并且倒序输出

public static void main(String[] args) {
        ArrayList<Person> people = new ArrayList<>();
        people.add(new Person(1,10,"Charles"));
        people.add(new Person(2,20,"Caarles"));
        people.add(new Person(3,22,"Bharles"));
        people.add(new Person(4,27,"Kharles"));
        people.add(new Person(5,36,"Chazles"));
        List<String> collect = people.stream().filter(a -> a.age < 30).filter(a -> a.id % 2 != 1).sorted((a, b) -> {
            return b.name.compareTo(a.name);
        }).map((a) -> a.name).collect(Collectors.toList());
        for (String s : collect) {
            System.out.println(s.toString());
        }
        //Kharles
        //Caarles
    }

Fork/Join是什么?

可以将一个大的任务拆分成多个子任务进行并行处理,以下是一个大量数字相加的简单的例子:

/**
 * 这是一个简单的Join/Fork计算过程,将1—1001数字相加
 */
public class TestForkJoinPool {
 
    private static final Integer MAX = 200;
 
    static class MyForkJoinTask extends RecursiveTask<Integer> {
        // 子任务开始计算的值
        private Integer startValue;
 
        // 子任务结束计算的值
        private Integer endValue;
 
        public MyForkJoinTask(Integer startValue , Integer endValue) {
            this.startValue = startValue;
            this.endValue = endValue;
        }
 
        @Override
        protected Integer compute() {
            // 如果条件成立,说明这个任务所需要计算的数值分为足够小了
            // 可以正式进行累加计算了
            if(endValue - startValue < MAX) {
                System.out.println("开始计算的部分:startValue = " + startValue + ";endValue = " + endValue);
                Integer totalValue = 0;
                for(int index = this.startValue ; index <= this.endValue  ; index++) {
                    totalValue += index;
                }
                return totalValue;
            }
            // 否则再进行任务拆分,拆分成两个任务
            else {
                MyForkJoinTask subTask1 = new MyForkJoinTask(startValue, (startValue + endValue) / 2);
                subTask1.fork();
                MyForkJoinTask subTask2 = new MyForkJoinTask((startValue + endValue) / 2 + 1 , endValue);
                subTask2.fork();
                return subTask1.join() + subTask2.join();
            }
        }
    }
 
    public static void main(String[] args) {
        // 这是Fork/Join框架的线程池
        ForkJoinPool pool = new ForkJoinPool();
        ForkJoinTask<Integer> taskFuture =  pool.submit(new MyForkJoinTask(1,1001));
        try {
            Integer result = taskFuture.get();
            System.out.println("result = " + result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace(System.out);
        }
    }
}

其底层有多个工作队列,当通过invoke或者submit方法提交任务的时候,forkjoinpool根据一定的路由规则把任务提交到每个工作队列中,如果在任务执行过程中出现子任务,那么子任务会提交到工作线程对应的工作队列中,每个线程都维护着一个工作队列,这是一个双端队列,每个工作线程在处理自己的工作队列的同时,会尝试窃取一个任务,窃取的任务位于其他线程的工作队列的base,而新任务会放在工作队列的top,也就是说窃取时使用FIFO,处理自己队列时使用的是LIFO,在遇到join的时候,如果需要join的任务尚未完成,会先处理其他的任务并且等待其完成

Java默认有几个线程?

每当使用java命令执行一个类的时候,实际都会启用一个JVM,所以每个java运行的时候至少会启动两个线程,一个main线程,一个垃圾回收线程

二进制补码,反码?

  • 反码:将二进制数按照位取反
  • 补码:在反码的基础上+1得到补码

正数的源码,补码和反码都是一致的,要得到一个负数的二进制,首先拿到正数的二进制,取得反码后再得到补码

洗牌算法?

打乱一个没有重复元素的数组,在每次迭代中,生成一个范围在当下下标到数组末尾元素下标之间的随机整数,将当前元素与随机选出的下标所指定的元素进行交换,元素可以与自己进行交换

证明:设一个元素m被放入第i个位置的概率为P=前i-1个位置选择元素时没有选中m的概率*第i个位置选中m的概率

class Solution {
    int[] array;
    int[] original;
    private Random random = new Random();
	
	public Solution(int[] nums){
        array = nums;
        original = nums.clone();
    }

    public int[] reset(){
        array = original;
        original = original.clone();
        return array;
    }

    public int[] shuffle(){
        for(int i=0;i<array.length;i++){
            int pickIdx = random.nextInt(array.length-i) + i;
            int tmp = array[pickIdx];
            array[pickIdx] = array[i];
            array[i] = tmp; 
        }
        return array;
    }
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值