字节跳动二面(消息队列+分布式+CAS+ThreadLocal)

蓦然回首自己做开发已经十年了,这十年中我获得了很多,技术能力、培训、出国、大公司的经历,还有很多很好的朋友。但再仔细一想,这十年中我至少浪费了五年时间,这五年可以足够让自己成长为一个优秀的程序员,可惜我错过了,我用这五年时间和很多程序员一样在困惑和迷茫中找不到出路!

路其实一直都在那里,只是我们看不到而已!

以前我一直被公司和技术牵着走,并不是自己在选择技术,而是不自觉地被推到了这个位置上。想想有多少人对于自己将来要从事的职业和技术类型进行过深入思考和比较呢?当我跳出编码后,我开始思考和程序及程序员职业生涯相关的问题,最后发现,影响我们走入今天的困局的竟然是一些我们常常挂在嘴边的话。

面试前

字节跳动的面试是最专业的,每次面试前有专门的HR和你约时间,确定OK后再进行面试。每次都是通过视频面试,因为都是之前都是电话面或现场面,所以视频面试还是有点不自然。也有人觉得视频面试体验很赞,当然萝卜青菜各有所爱。最坑的二面的时候对方面试官的网络老是掉线,最后很冤枉的挂了(当然有一些点答得不好也是原因之一)。所以还是有点遗憾的。

一面

先自我介绍下

  • 聊项目,逆向系统是什么意思
  • 聊项目,逆向系统用了哪些技术
  • 线程池的线程数怎么确定?
  • 如果是IO操作为主怎么确定?
  • 如果计算型操作又怎么确定?
  • Redis熟悉么,了解哪些数据结构
  • 说了zset
  • zset底层怎么实现的
  • 跳表
  • 跳表的查询过程是怎么样的,查询和插入的时间复杂度
  • 说了先从第一层查找,不满足就下沉到第二层找,因为每一层都是有序的,写入和插入的时间复杂度都是O(logN)
  • 红黑树了解么,时间复杂度
  • 说了是N叉平衡树,O(logN)
  • 既然两个数据结构时间复杂度都是O(logN),zset为什么不用红黑树
  • 跳表实现简单,踩坑成本低,红黑树每次插入都要通过旋转以维持平衡,实现复杂
  • 点了点头,说下Dubbo的原理
  • 说了服务注册与发布以及消费者调用的过程
  • 踩过什么坑没有
  • 说了dubbo异常处理的和打印accesslog的问题
  • CAS了解么
  • 说了CAS的实现
  • 还了解其他同步机制么
  • 说了synchronize以及两者的区别,一个乐观锁,一个悲观锁
  • 那我们做一道题吧,数组A,2*n个元素,n个奇数、n个偶数,设计一个算法,使得数组奇数下标位置放置的都是奇数,偶数下标位置放置的都是偶数
  • 先说下你的思路
  • 从0下标开始遍历,如果是奇数下标判断该元素是否奇数,是则跳过,否则从该位置寻找下一个奇数
  • 下一个奇数?怎么找?
  • 有点懵逼,思考中。。。
  • 有思路么?
  • 仍然是先遍历一次数组,并对下标进行判断,如果下标属性和该位置元素不匹配从当前下标的下一个遍历数组元素,然后替换
  • 你这样时间复杂度有点高,如果要求O(N)要怎么做
  • 思考一会,答道“定义两个指针,分别从下标0和1开始遍历,遇见奇数位是是偶数和偶数位是奇数就停下,交换内容”
  • 时间差不多了,先到这吧。你有什么想问我的?

二面

  • 面试官和蔼很多,你先介绍下自己吧
  • 你对服务治理怎么理解的?
  • 项目中的限流怎么实现的?
  • Guava ratelimiter,令牌桶算法
  • 具体怎么实现的?
  • 要点是固定速率且令牌数有限
  • 如果突然很多线程同时请求令牌,有什么问题?
  • 导致很多请求积压,线程阻塞
  • 怎么解决呢?
  • 可以把积压的请求放到消息队列,然后异步处理
  • 如果不用消息队列怎么解决?
  • 说了RateLimiter预消费的策略
  • 分布式追踪的上下文是怎么存储和传递的?
  • ThreadLocal + spanId,当前节点的spanId作为下个节点的父spanId
  • Dubbo的RpcContext是怎么传递的?
  • ThreadLocal
  • 主线程的ThreadLocal怎么传递到线程池?
  • 说了先在主线程通过ThreadLocal的get方法拿到上下文信息,在线程池创建新的ThreadLocal并把之前获取的上下文信息设置到ThreadLocal中。这里要注意的线程池创建的ThreadLocal要在finally中手动remove,不然会有内存泄漏的问题
  • 你说的内存泄漏具体是怎么产生的?
  • 说了ThreadLocal的结构,主要分两种场景:主线程仍然对ThreadLocal有引用和主线程不存在对ThreadLocal的引用。第一种场景因为主线程仍然在运行,所以还是有对ThreadLocal的引用,那么ThreadLocal变量的引用和value是不会被回收的。第二种场景虽然主线程不存在对ThreadLocal的引用,且该引用是弱引用,所以会在gc的时候被回收,但是对用的value不是弱引用,不会被内存回收,仍然会造成内存泄漏
  • 线程池的线程是不是必须手动remove才可以回收value?
  • 是的,因为线程池的核心线程是一直存在的,如果不清理,那么核心线程的threadLocals变量会一直持有ThreadLocal变量
  • 那你说的内存泄漏是指主线程还是线程池?
  • 主线程
  • 可是主线程不是都退出了,引用的对象不应该会主动回收么?
  • (面试官和内存泄漏杠上了),沉默了一会。。。
  • 那你说下SpringMVC不同用户登录的信息怎么保证线程安全的?
  • 刚才解释的有点懵逼,一下没反应过来,居然回答成锁了(大脑有点晕了,此时已经一个小时过去了,感觉情况不妙。。。)
  • 这个直接用ThreadLocal不就可以么,你见过SpringMVC有锁实现的代码么
  • 有点晕菜。。。
  • 我们聊聊mysql吧,说下索引结构
  • 说了B+树
  • 为什么使用B+树?
  • 说了查询效率高,O(logN),可以充分利用磁盘预读的特性,多叉树,深度小,叶子结点有序且存储数据
  • 什么是索引覆盖?
  • 忘记了。。。
  • Java为什么要设计双亲委派模型?
  • 什么时候需要自定义类加载器?
  • 我们做一道题吧,手写一个对象池
  • 有什么想问我的么

总结

二面技术面之后,就是HR,这个就不多说了。整体字节跳动的面试感受,面试官很专业,要求基础知识很熟悉,面试之前一定要准备后再去面试。LeetCode题目也要刷。

切记面试前一定要刷题和准备,简历上的项目不熟悉的千万不要写上去,写上去的项目备好2-3个技术方案。

我个人认为,作为技术人就要保持终生学习的态度,让学习力成为核心竞争力,才能不被时代所淘汰,而高效的时间支配能让你变得更加优秀,所以,我在这里将这份耗时两个月整理出来的核心技能知识点,送给有需要的人,希望这份资料能对大家有所帮助

动态数据源切换可以通过AOP(面向切面编程)和ThreadLocal来实现。其中,ThreadLocalJava中的一个线程级别的变量,可以在同一个线程中共享数据。自定义注解可以用来标记需要切换数据源的方法。 实现步骤如下: 1. 定义数据源切换注解 ```java @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface DataSource { String value() default "primary"; } ``` 2. 定义数据源切换切面 ```java @Aspect @Component public class DataSourceAspect { @Pointcut("@annotation(com.example.demo.annotation.DataSource)") public void dataSourcePointCut() { } @Before("dataSourcePointCut()") public void before(JoinPoint joinPoint) { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); DataSource dataSource = signature.getMethod().getAnnotation(DataSource.class); if (dataSource != null) { DynamicDataSourceContextHolder.setDataSource(dataSource.value()); } else { DynamicDataSourceContextHolder.setDataSource("primary"); } } @After("dataSourcePointCut()") public void after(JoinPoint joinPoint) { DynamicDataSourceContextHolder.clearDataSource(); } } ``` 3. 定义ThreadLocal存储数据源信息 ```java public class DynamicDataSourceContextHolder { private static final ThreadLocal<String> contextHolder = new ThreadLocal<>(); public static void setDataSource(String dataSource) { contextHolder.set(dataSource); } public static String getDataSource() { return contextHolder.get(); } public static void clearDataSource() { contextHolder.remove(); } } ``` 4. 在数据源配置文件中定义多个数据源 ```java @Configuration public class DataSourceConfig { @Bean @ConfigurationProperties("datasource.primary") public DataSource primaryDataSource() { return DataSourceBuilder.create().build(); } @Bean @ConfigurationProperties("datasource.secondary") public DataSource secondaryDataSource() { return DataSourceBuilder.create().build(); } @Bean @Primary public DynamicDataSource dataSource() { DynamicDataSource dynamicDataSource = new DynamicDataSource(); Map<Object, Object> dataSourceMap = new HashMap<>(2); dataSourceMap.put("primary", primaryDataSource()); dataSourceMap.put("secondary", secondaryDataSource()); dynamicDataSource.setTargetDataSources(dataSourceMap); dynamicDataSource.setDefaultTargetDataSource(primaryDataSource()); return dynamicDataSource; } } ``` 5. 在需要切换数据源的方法上添加@DataSource注解 ```java @Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Override @DataSource("primary") public List<User> listPrimaryUsers() { return userMapper.listUsers(); } @Override @DataSource("secondary") public List<User> listSecondaryUsers() { return userMapper.listUsers(); } } ``` 这样,在调用listPrimaryUsers和listSecondaryUsers方法时,就会自动切换到对应的数据源。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值