• 问:现在聊点基础的吧,我现在new两个value为1的Integer对象用==比较返回什么

呃,返回false 用valueOf创建的话==会返回true。Integer,long 这几个封装类型里面会维护一个缓存。用来缓存比较小的值,默认缓存是-128到127这个范围的值。

  • 问:我看你做过订单系统。你这个系统里面的金额用的什么类型啊?

代码里面用BigDecimal,mysql 里面用Decimal

  • 问:为什么不用double 呢?

因为double 会丢精度呃会出现1.00001这种不精确的值。

  • 问:知道为什么会出现这种值吗?

这是因为十进制小数和二进制转化的问题啊。你比如0.8转化成二进制的话,小数部分就是11001100这种无限循环。计算机就只会取前面一部分值进行存储啊,后面循环的部分就丢了,再转化成十进制的时候就丢精度了。

  • 问:那为什么BigDecimal 能表示精准的浮点数呢?

BigDecimal里面说白了就是浮点数转化成整数存储,然后记录小数点的位置在哪里。这个转化成的整数一般都是Long范围内的,所以直接用Long存就完事儿了。要是超出了Long的范围,就要用BigInteger存,BigInteger 是用数组来存大数的。然后BigDecimal 里面还有一个scare 字段,用来记录小数点后的位置。呃,还有就是用precision 字段用来记录整个小数和整数部分的总长度。这样的话就知道小数点位置了。

  • 问:好,那用equals 比较两个BigDecimal值会有什么问题呢?

BigDecimal 的 equal 的实现里面会比较scare 和precision 两个字段啊,就会导致1.0和1这种精度不同的值返回false。实际上呢是相等的。所以用compareTo方法才行。

  • 问:你还知道什么情况下,依靠此会返回false 呢?

我自己覆盖了一个equals 啊,equals 始终返回false 这种算吗?

  • 问:算呃,我们说一下正常的情况啊。

呃,用两个类加载器加载同一个类啊,得到两个不同的class 对象啊,然后用这两个class 生成两个对象。调equals的话会返回false。

  • 问:那你们的服务器用的是什么呀?

呃,用的tomcat

  • 问:有没有尝试过对tomcat 进行一个调优?

嗯,大概知道些,主要就是把IO模式改成NIO,然后再调整一下最大连接数上限。连接池里面现成的上限数,呃后再调整一下JVM的大小啊,GC的参数就可以了。我们之前做监控的时候,是在中间件里面发HTTP请求到监控服务器。但是在做了一次迁移之后,业务说会莫名其妙的出现TCP连接超时的问题。好,先说结论吧。我们排查之后发现tomcat里面backlog 参数太小了,然后原理是这样的。TCP三次握手的时候会涉及到两个队列。刚开始握手的时候,连接先进行sync队列,然后握手完了之后进accept队列。当时我们迁移私有云之后。各个业务实例都变多了啊,然后我们监控的实例个数没有扩容,然后导致accept 不过来了,就会导致在那个accept 队列里面堆积了啊,就会出现连接超时。

  • 问:再来Spring吧。你们在开发的时候用了Spring的哪些功能呢?

主要用 Spring 的IOC和AOP两个功能,

  • 问:谈谈你对IOC的理解吧。

IOC的话是控制反转,正常情况下java 要创建一个对象的话,是我们需要用new 关键词去创建的。然后使用IOC的话,是把管理这些对象的权限全部交给了spring 容器。然后spring 容器帮你创建好这些对象。然后跟IOC相关的一个概念是依赖注入,它就是在你需要对象的时候,通过注解或者是XML配置的方式,注入到你的代码里面去。那这样的话就通过配置的方式接入了接口和实现。嗯。我们之前有一个项目没有用Spring ,但这里面用了大量的工厂方法模式啊,这样的话就非常痛苦。基本上定义了一个接口就会跟出一个工厂啊,就很头疼啊,类的数量直接爆炸。

  • 问:了解过Spring IOC实现的原理吗?

简单了解过Spring IOC的话,基本上就是通过工厂模式,还有反射实现的。在Spring IOC初始化的时候,先去加载配置文件,然后解析配置文件,Spring 就知道扫哪些路径。然后就会扫描到这些类,Spring 会把这些类加载成bean definition,然后再生成Spring Bean,生成Spring Bean的时候还会触发一些生命周期方法。比如说post constructor 方法啊,做些初始化之类的。生成完Spring Bean 之后呢,就开始进行装配。Spring 之前不是扫描得到了一些Bean definition 啊,就知道哪些字段需要注入哪些Spring Bean。然后就根据Bean的名称或者是Bean的类型,把相应的Bean注入到相应的字段里面。其实就是通过反射调用相应的setter 方法或者是构造函数,设置Spring Bean,大概就是这样了。

  • 问:Spring bean 都是单例的,对吧?

呃,默认是单例,呃,也可以配置成原型模式等。

  • 问:为什么Spring Bean 要搞成单例的呢?

单例的话创建的对象少,对这些友好,每次都走反射创建并的话,性能肯定是没有单例好。单例的话直接缓存这个单例对象就行,用的时候直接拿就行。缺点就是Spring Bean 线程不安全啊,这个有很多方式来处理,也不是什么很严重的缺点。

  • 问:而Spring Bean之间相互依赖啊,Spring 是怎么解决这种循环依赖的问题呢?

Spring 使用三层缓存解决单例模式下的循环依赖。第一层缓存存的是完全初始化好了的Bean。第二层缓存存的是创建好了但是没有初始化好的Bean,第三层缓存存的是创建Bean的工厂对象。Spring 首先从一层缓存查Bean,查到了就直接返回,查不到就会去二层缓存查,查到了就会返回这个半成品的Bean。要是还是查不到的话就用三层缓存里面的工厂去创建一个半成品,并放到二层缓存里面,然后就可以用了。我举个例子哈。A,B两个Bean相互依赖,呃,加载A 的时候,呃,一二层缓存没有啊,到了三层缓存的工厂拿到一个半成品啊,然后放到二层缓存。然后呢初始化A的时候,发现要初始化B,B 也是到了三层缓存的工厂里面啊,拿到一个半成品放到二层缓存。然后Spring 会优先初始化被依赖的Bean,那就会去初始化B 啊,发现需要A 在二层缓存里面能找到半成品A 啊,就可以直接调用setter 方法把A 注入到B里面,B就变成成品了啊,会放到一层缓存里面。好,然后再回到初始化A 的流程。这个时候呢可以把B注入到A里面啊,A 也就可以进一层缓存了啊,这其实就是一个递归的感觉。嗯,单例这么可以解啊,原型模式不行,解不了。

  • 问:Spring AOP你主要用在什么场景?

嗯,主要用来管理事务和权限验证。

  • 问:哎,说说Spring AOP的原理吧

目标类实现了接口的时候,走JDK动态代理生成一个实现了接口的代理类。目标类要是没实现任何接口,就走cglib 生成个子类作为单体类。

  • 问:代理模式你在项目中用过吗?

呃,基本没用过。但是我使用的很多框架都用到了。比如说Dubbo这种RPC框架都有一层代理,基本都会用到代理模式。就拿Dubbo 来说,他会给我们的业务service 创建一个代理类代理里面完成远程调用。我记得Dubbo是用的javassist生成的代理类。

  • 问:mysql 里面的索引了解吗?

呃,了解,所以是个B+树。

  • 问:我是想问Mysql里面见索引多了会有什么问题吗?

嗯,主要是会降低写入性能吧。Innodb里面存数据是按页存储的,每个页16K,索引也是按页存的。索引在页里面是按索引的顺序存储的,在插入或者是修改的时候,要是一个页里面的数据量超过了阈值,就会进行页分裂。这就涉及到索引数据的拷贝。拷贝的话就要把数据从磁盘加载到内存然后再写回磁盘,性能就会下降。是删除数据的话,就可能导致数据页的合并也是一样的。然后就是Innodb写的时候,是走Change buffer之类的缓冲的。要是修改到唯一索引的话,就没有办法走Change buffer 了。需要先读出唯一索引判重,然后刷盘就会产生更多的脏页。而且就算没走到唯一索引,其他的索引页被读取到内存之后,也会很快变成脏页。还要浪费资源,把脏页刷盘mysql

  • 问:你们线上用的什么拓扑结构

用的主从,然后分库分表,然后在基础框架里面自动做分库分表的路由储存同步里面。

  • 问:主从同步里面,你们的binlog格式用的是啥

Mixed 模式

  • 问:为什么用这种格式呢?

嗯,statement 格式的话是原封不动的把那个SQL语句复制到从库会有不一致的风险。比如主库执行个now 延迟几秒的从库了,时间就变了。再比如主库用了limit 限制条件啊,结果从库limit 条件走了另外一个索引limit 出来的行就不一样了。row格式的话是同步每行的修改,日志量比较大。比如执行一个delete 语句啊,在主库删了一万行,到了从库就是一万条delete 语句啊,每条删除一行。mix 的格式的话是row和statement 混合。我理解是能走statement 就走statement。要是有一致性问题就走row啊,数据一致性比较重要。

  • 问:嗯,主从有没有碰到过延迟的问题

呃,碰到过,之前有一次删历史数据的时候,我们写了一条delete 语句。where条件里面只写了create_time的条件,结果那个时间点之前的历史数据有两千多万条啊,这种大事物会一次产生非常多的binlog 日志。然后就主从延迟了呃,当时延迟了五个小时

  • 问:那当时你是怎么处理的,这个延迟啊啊也不能一直等着,对吧?

嗯,当时mysql 前面挡了一层redis ,读请求基本都落在缓存上了,只有少量读请求穿透到了mysql 库。然后我就直接强制读主库了。这样的话业务就感知不到延迟了。

  • 问:呃,了解过mysql 主从复制的原理吗?

大概流程是这样。主库先写本地的binlog 日志啊,然后从库跟主库之间有一个长连接。从库有个IO线程会把bin log 从主库复制到从库,然后写入到从库的中继日志里面。然后重复还有一个SQL线程,这个线程会重放中继日志里面的binlog 应用到从库上。这样的话主从就可以保持数据一致了。

  • 问:嗯,从库的IO线程和SQL线程是单线程的吗?

IO线程始终是单线程的,SQL线程应该是在mysql5.5版本的时候是单线程的。到了5.6的时候就变成了多线程,多加了几个worker 线程。SQL线程负责分发binlog ,worker线程负责在从库上并行回放

  • 问:研究过并行复制的原理吗?

呃,大家了解过,我记得是5,6版本的时候还是按库并行复制的。比较鸡肋。我们生产环境的库基本都是业务隔离的,一个mysql 实例一个库。5.7的方案是用事务分组实现并行复制。在事务提交的时候,会先写prepare 状态的redo log 啊,这个时候并发的事务在主库能并发啊就是一组事务啊,那就说明这些事务没有什么交集啊,也不会争抢锁之类的。那这些事务在从库上也能并发执行啊,大概是这么个思路,具体实现的话我没看过,只是知道大概有个逻辑时钟的概念。记在binlog 里面用来标识事务是不是一组的。binlog同步到从库之后,从库就按照这个逻辑时钟的顺序啊,一组一组的执行事务。一组的事务是可以分给多个worker 线程并发执行的。5.7这个版本的并行复制问题是组与组之间不能并行,那就可能因为一个比较大的事务阻塞后面所有的阻塞。后来mysql8的时候,就开始使用write set的方式来检查事务能不能并行,原理大概是这样。mysql 维护了一个全局的Map,里面放了事务的编号啊,以及这个事务修改的行的哈希值啊,这些行的哈希值组成了这个事务的write set集合。write set 没有交集的话,就是没有修改到同一个行。那这些事务也就可以并发执行了。然后一行的哈希值是通过库名加表名加所有唯一索引的名称加列值计算出来的。要是唯一索引多了,计算write set也会受影响啊,所以还是少建唯一索引。

  • 问:你在生产环境里面有没有执行过alert 操作,执行过alert 表会有什么风险吗?

alert 表会锁表。我们当时有gh-ost 和percona两个方案,选的是gh-os。大概原理是先创建影子表,在影子表上执行alert 语句,然后影子表会批量拷贝原表的数据啊,然后再消费bin log, 让影子表和原表同步。最后锁表进行rename,用影子表替换原表。最后这个锁表的时间非常短,在低峰期的话,也就是一秒以内,整个业务没啥感知。就是拷贝 数据和消费binlog追平的时间非常久。当时alert 一张一亿数据的表执行了三四天。percona的那个online change 的方案是基于触发器的。其实我们的场景也是可行的。但是触发器这个东西我们用的比较少,没人能hold 住,所以就有点怵啊。DBA他们就选了个比较有信心的。