mysql方面
问:说下你在项目中如何使用mysql/如何编写sq语句的
答:写sql比较多的方面在于数据报表。位于平台的监管系统分别按照生产企业,药品采购价,医疗机构这三个订单维度进行统计
问:说下你怎么在报表里编写sql的?遇到哪些问题?怎么处理的?
我们的统计基于订单明细表进行统计,由于订单明细表的数据量较大(4000万条)。 所以将这张订单明细表按照生产企业,药品采购价,医疗机构,以及一些跟订单表相关的业务属性进行分组,且去掉了订单提交时间。归为一个定时任务 由于订单明细是由医疗机构向配送企业以日为单位进行下单的,所以定时任务按天查询进行数据同步.避免一次性因为数据查询区间过大,造成全表扫描,从而导致IO次数变多 因为去掉了订单时间,所以我们在定时任务按照时间段查询并按照上述维度进行分组时。保留该提交时间所属的年,月,日,半年。方便对于年度月度等进行分组 对于生产企业而言。每一家生产企业的汇总记录需要包含他的同比和环比。如果分两次查询统计数据的话必然需要维护,已汇总的关键字段与需要统计环比的映射关系。 但这样做需要先汇总统计完其他字段,如订单金额和订单数量.再进行数据的查询,会增加磁盘IO次数。 而且如果此时定时任务恰好位于凌晨,那么在定时任务定时同步数据到快照表,与方法开始执行同比环比统计时,存在一定的时间差。 所以需要汇总的订单明细中的业务属性字段需要与同比环比在同一个数据行内,我们先用case when对子查询做一次处理。得到根据查询条件筛选出来的数据
但筛选条件分为三种,一种是关联其他表才能获取到的业务属性,一种是订单明细的业务属性。一种是订单明细分组后所属的时间段(年月季半年)属性 而同比的结算方式是当前时间段订单价/(上一时间段订单价-当前时间段订单价)*100%。这意味着我们要在一次处理进行时间段查询的时候,要将当前时间段订单价与上一个时间段订单价在同一个数据行进行返回。
二次处理先对生产企业进行分组.此时会有很多不在本时间段订单价的生产企业汇总记录会为空。所以将一次处理返回的时候将当前时间段的订单价作为一个统计标识,如果这个标识为空,则认为在筛选时间段内,该生产企业没有进行交易。于此去掉 接着将对二次处理的结果进行聚合 根据年月日进行分组汇总
问: 你说说mysql的索引结构
mysql按照物理存储划分,分为聚簇索引以及非聚簇索引。分别对应Innodb引擎和mylsam引擎. 不同的地方在于。聚簇索引的主键地址以及数据记录都是放在同一个叶子结点 而聚簇索引是将主键地址和数据记录存放到不同的节点。所以从存储方式可以看出,多读的场景选择聚簇索引,多写的场景选择非聚簇索引
按照数据结构进行划分,分为B+ tree 索引以及hash索引
B+ tree索引的特征是 非叶子节点上只存储key值信息,高度一般都在2-4层,查找某一键值的行记录时最多只需要1~3次磁盘I/O操作 hash索引的特征是 对每个字段对hash值进行字段索引的检索可以一次定位, 不像B-Tree 索引需要从根节点到枝节点,最后才能访问到页节点这样多次的IO访问,
问:那你说下为什么要采用B+tree?那为什么普遍使用B+tree而不是hash索引呢?
答:hash索引 仅仅能满足"=","IN"和"!="查询,不能使用范围查询。而采用B+tree首先是因为他是一棵平衡查找树 但B tree和B+tree都属于平衡查找树,下面我简述一下两者的特点:(blog.csdn.net/alex_xfboy/…) m阶 B-Tree 是满足下列条件的数据结构: 所有键值分布在整颗树中 搜索有可能在非叶子结点结束,在关键字全集内做一次查找,性能逼近二分查找
根据根节点找到磁盘块 1,读入内存。【磁盘 I/O 操作第 1 次】 比较关键字 29 在区间(17,35),找到磁盘块 1 的指针 P2。 根据 P2 指针找到磁盘块 3,读入内存。【磁盘 I/O 操作第 2 次】 在磁盘块 3 中的关键字列表中找到关键字 29。
可以看出b tree是根据每个节点的指针去定位所属区间的键值的 同时B-Tree也存在问题: 每个节点中有key,也有data,而每一个页的存储空间是有限的,如果data数据较大时将会导致每个节点(即一个页)能存储的 key 的数量很小。 当存储的数据量很大时同样会导致 B-Tree 的深度较大,增大查询时的磁盘 I/O 次数,进而影响查询效率
B+tree 的特点
B+Tree 是在 B-Tree 基础上的一种优化,InnoDB 存储引擎就是用 B+Tree 实现其索引结构。它带来的变化点:
B+树每个节点可以包含更多的节点,这样做有两个原因,一个是降低树的高度。另外一个是将数据范围变为多个区间,区间越多,数据检索越快 非叶子节点存储key,叶子节点存储key和数据 叶子节点两两指针相互链接(符合磁盘的预读特性),顺序查询性能更高 注:MySQL 的 InnoDB 存储引擎在设计时是将根节点常驻内存的,因此力求达到树的深度不超过 3,也就是说 I/O 不需要超过 3 次。
通常在B+Tree上有两个头指针,一个指向根节点,另一个指向关键字最小的叶子节点,而且所有叶子节点(即数据节点)之间是一种链式环结构。因此可以对 B+Tree 进行两种查找运算:一种是对于主键的范围查找和分页查找,另一种是从根节点开始,进行随机查找。 B-树和B+树的区别 B+树内节点不存储数据,所有数据存储在叶节点导致查询时间复杂度固定为 log n B-树查询时间复杂度不固定,与 key 在树中的位置有关,最好为O(1) B+树叶节点两两相连可大大增加区间访问性,可使用在范围查询等 B-树每个节点 key 和 data 在一起,则无法区间查找 B+树更适合外部存储(存储磁盘数据)。由于内节点无 data 域,每个节点能索引的范围更大更精确。
问:说下你平时自己分析走了哪个索引是怎么做的?用explain
答:www.jianshu.com/p/49cfd65a7…
问:你说下mysql的事务隔离级别
答:脏读,不可重复读,幻读
脏读是指。再读取数据之后,对数据进行修改,但是查询还是原来的数据 最近项目中有个需求,邯郸交易系统与MDM(数据中心)互联互通的需求当中, 由于讨论的方案,生产环境两台机器可能放在公网。所以匹配MDM返回对照id,并更新快照表会产生延迟,如果此时更新挂网目录的数据与需要匹配的数据是同一条。更新时快照表可能会发生脏读 (www.cnblogs.com/powerwu/art…)
连环炮 redis方面
问:你说下怎么在项目中用redis的?redis的数据结构有哪些?你说下他们的特点
SDS主要用来存储一些token信息,列表相当于 Java 语言里面的 LinkedList
答:SDS动态字符串用泛型当作len与capitlty的数据类型,有两种存储方式emanedStr与raw,长度超过44用raw存 字符串在长度小于 1M 之前,扩容空间采用加倍策略,也就是保留 100% 的冗余空间。当长度超过 1M 之后,为了避免加倍后的冗余空间过大而导致浪费,每次扩容只会多分配 1M 大小的冗余空间。 但字符串在设置过期Key需要这样设置 Redis 会持续扫描过期字典 (循环多次),直到过期字典中过期的 key 变得稀疏,才会停止 (循环次数明显下降)。这就会导致线上读写请求出现明显的卡顿现象。导致这种卡顿的另外一种原因是内存管理器需要频繁回收内存页,这也会产生一定的 CPU 消耗。 如果有大批量的 key 过期,要给过期时间设置一个随机范围,而不宜全部在同一时间过期,分散过期处理的压力。
# 在目标过期时间上增加一天的随机时间
redis.expire_at(key, random.randint(86400) + expire_ts)
问:你说下redis的字典
答:dict 结构内部包含两个 hashtable,通常情况下只有一个 hashtable 是有值的。但是在 dict 扩容缩容时,需要分配新的 hashtable,然后进行渐进式搬迁
问:你说下渐进式搬迁的具体过程
答:重新申请新的数组,然后将旧字典所有链表中的元素重新挂接到新的数组下面,会先设置一个步长间隔,实现小步搬迁。这是一个O(n)级别的操作
问:为什么redis的sortset结构不用红黑树,而要用跳跃表
答:因为数据结构简单
问:手写LRU缓存?
private volatile Map<String,String> lruMap;
private int cacheSize;
public LRUcache(int initSize){
this.cacheSize = initSize;
this.lruMap = new LinkedHashMap<String,String>(initSize,0.75f,true){
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
//size()获取map中当前元素数量,和初始设置的值做比较
//超过预设则删除 eldest(年龄最大的)
return size() > LRUcache.this.cacheSize;
}
};
}
public String get(String inKey){
if(inKey.isEmpty() || this.lruMap.isEmpty()){
return null;
}
for(String key : lruMap.keySet()){
if(key.equals(inKey)){
return lruMap.get(key);
}
}
return null;
}
public synchronized boolean put(String key,String value){
this.lruMap.put(key,value);
return true;
}
问:你说下redis集群怎么使用的?一般是CP还是AP?主节点挂了怎么办?此时同步的数据时保留还是丢弃?
答:redis的集群模式是依赖于主从同步的,一般是保证节点宕机后保证能继续通信,也就是AP系统。同步的时候主节点会起一个定时任务,bgsave rdb文件同步到从节点。因为主节点的buffer是一个环形数组,如果数组已满,新生成的buffer元素会覆盖之前的指令流
故redis采用了快照同步,保证先从主节点做一次全量加载到磁盘,从节点清空当前内存后,第一次全量加载,之后通知主节点增量加载。但如果快照同步的时间太长或者复制的buffer太小会造成复制的增量buffer被覆盖,频繁进行快照同步
问:那如果我主节点用AOF保存的话会有什么影响呢?
答:系统正在进行 AOF 的 fsync 操作时如果发生快照,fsync 将会被推迟执行,这就会严重影响主节点的服务效率。 应采用无盘复制,是指主服务器直接通过套接字将快照内容发送到从节点,生成快照是一个遍历的过程,主节点会一边遍历内存,一边将序列化的内容发送到从节点,从节点还是跟之前一样,先将接收到的内容存储到磁盘文件中,再进行一次性加载。
连环炮 kafka方面
问:你说下kafka的日志存储格式是怎么样的呢?
答:将 Log 切分为多个 LogSegment,相当于一个巨型文件被平均分配为多个相对较小的文件,这样也便于消息的维护和清理。
事实上,Log 和 LogSegment 也不是纯粹物理意义上的概念,Log 在物理上只以文件夹的形式存储,每个log对应磁盘上的一个日志文件和两个索引文件, 以及可能的其他文件(.tnxindex 事务索引文件) 消费者提交的位移是保存在 Kafka 内部的主题__consumer_offsets中的,初始情况下这个主题并不存在,当第一次有消费者消费消息时会自动创建这个主题。
kafka最初的消息格式由本条消息提交的偏移量offset和消息大小message size组成消息头 。每一个根目录都会包含最基本的4个检查点文件(xxx-checkpoint)和 meta.properties 文件。
问:你说下kafka生产者客户端的整体架构
答:整个生产者客户端由两个线程协调运行,这两个线程分别为主线程和 Sender 线程(发送线程)。在主线程中由 KafkaProducer 创建消息,然后通过可能的拦截器、序列化器和分区器的作用之后缓存到消息累加器(RecordAccumulator,也称为消息收集器)中。Sender 线程负责从 RecordAccumulator 中获取消息并将其发送到 Kafka 中。
问:那你说下如何提升性能?
答:RecordAccumulator 缓存的大小可以通过生产者客户端参数 buffer.memory 配置,默认值为 33554432B,即32MB。如果生产者发送消息的速度超过发送到服务器的速度,则会导致生产者空间不足,这个时候 KafkaProducer 的 send() 方法调用要么被阻塞,要么抛出异常,这个取决于参数 max.block.ms 的配置,此参数的默认值为60000,
问:看你项目是在一个分区内进行消费,那么如果我此时突然增加到两个分区,你怎么保证分区尽可能均匀地分配给所有的消费者
答:kafka默认的分配策略是RangeAssignor分配策略,原理是按照消费者总数和分区总数进行整除运算来获得一个跨度,然后将分区按照跨度进行平均分配,以保证分区尽可能均匀地分配给所有的消费者。对于每一个主题,RangeAssignor 策略会将消费组内所有订阅这个主题的消费者按照名称的字典序排序,然后为每个消费者划分固定的分区范围,如果不够平均分配,那么字典序靠前的消费者会被多分配一个分区。
我们采用的是RoundRobinAssignor策略,分别限定在不同业务领域的消费组内,如果同一个消费组内所有的消费者的订阅信息都是相同的,那么 RoundRobinAssignor 分配策略的分区分配会是均匀的。举个例子,假设消费组中有2个消费者 C0 和 C1,
连环炮 juc并发方面
问:你说下单例模式有什么?双重锁单例模式有什么坏处?
答: 双重锁检查创建实例的时候,可能会产生CPU级别的指令重排序,所以实现的时候需要加上voalite禁止指令重排序
问:一般创建多少线程是合适的?
答:多线程是用来降低延迟,提高吞吐量,需要解决CPU和I/O设备综合利用率的问题
对于CPU密集型的场景,线程数量设置为CPu核数+1 .因为内存页失效或者其他原因阻塞,线程可以顶上
对于I/O密集型的场景 单核:最佳线程数 =1 +(I/O耗时/CPU耗时)
问:netty里面IO密集型怎么设置呢?
Nthread
= (1 + Tio/Tcpu) * Ncpu
= {1 + ioTime/ [ioTime*(100 - ioRatio) / ioRatio]} * Ncpu
= (100 / ioRatio) * Ncpu
其中 Ncpu 是常量,表示cpu的个数
问:看你的项目中有用到锁,那么什么类型适合当锁呢?Integer、String类型为什么不适合当锁?
答:通过valueOf方法创建Integer对象的时候,如果数值在[-128,127]之间,便返回指向IntegerCache.cache中已经存在的对象的引用;否则创建一个新的Integer对象。
问:如果你自己实现一个线程池?你会怎么做呢?
答:我们项目中处于需求,在执行定时任务的时候,自己封装了一个线程池。 基于ThreadPoolExecutor设置的因为业务上的并行任务数一般最小是5个,导入导出总共也就10个。使用有界阻塞队列,任务被拒绝添加后,会调用当前线程池的所在的线程去执行被拒绝的任务。
问:有使用过juc并发包么?底层核心的同步队列有研究过么?说下原理
答:jdk1.7之前,同步队列是一个链表结构。如果是非独占锁,CAS保证会将直到节点获取到锁之后才加入队列。如果是独占锁,每个节点会处于自旋,线程实现通过tryAcquire()方法尝试获取锁,获取成功的话直接返回,如果尝试失败的话, 进入等待队列排队等待,可以保证线程安全(CAS)的获取同步状态。
问:你项目中用到了 Semaphore,ReadWriteLock能说说他们的原理么?
答:项目中用到Semaphore是因为在agent返回支付中心的回调请求时,需要做异步处理,但是此时支付中心可能有多个请求到达agent.由于agent在外网,支付中心在内网,所以可能产生因为远程调用超时,而引起的线程阻塞。所以这里用采用信号量 控制当前线程是否需要进入等待队列。即时释放临界区资源
而ReadWireLock是因为支付中心作为一个对接所有医院端的支付服务,维护了<医院ID,通道>的Map映射关系。当每个医院连接上时,需要频繁读写Map集合, 这就造成了数据更新不及时的问题。所以采用ReadWriteLock,使某些频繁的写请求加上写锁,等到所有的请求排队拿到锁之后,从写锁降级成读锁
读写锁解决问题是允许多个线程同时读共享变量;只允许一个线程写共享变量;如果一个写线程正在执行写操作,此时禁止读线程读共享变量
连环炮:zookeeper方面
问:你说下zk的架构
答:Leader 事务请求的唯一调度协调者,保证全局事务处理顺序性。Follwer处理费事务请求 事务请求转发给leader。对leader废弃的提议进行投票。参与leaDER选举投票.Observer只充当观察者角色。 www.processon.com/view/link/5…
问:那zk作为一个CP系统,是怎么保证分布式一致性的呢?
答:zk使用zab协议保证分布式数据的一致性,基于该协议zk实现蛀虫模式的系统架构。保证集群中各个副本的一致性。zab包括两种模式,崩溃恢复和原子广播
问:那你说下这两种模式
答:原子广播的实现过程为 leader接收到消息,生成全局唯一自增zxid,将带有zxid的消息放进每个fllwer的fifo队列中,作为一个提议发给所有follwer. follwer收到提议后,写入磁盘,成功后返回一个ack。当leader收到半数成功的ack后,leader就会发出提交命令,同时在本地提交该信息。
崩溃恢复为一旦leader崩溃,或者leader与过半的follower失去了通讯(产生了网络分区),那么此时的leader已经失效了,为了保证系统正常运行,会基于zab重新选主
已经被执行的事务如何保证不丢失,已经被丢弃的事务如何保证不重现
zk选举通过zxid,保证选举出来的leader用友急群众最大的事务编号,因此能保证这个新的leader一定具有已经提交的事务请求 这个新的leader一定具有已经提交的事务请求,新选举出来的leader,生成的zxid高32位大于旧的leader,同时旧的leader低32为消息计数器被清零,旧的leader恢复之后就作为follower角色接入集群。同步旧leader的数据,清空之前未被提交的提议。
机房3与其他机房断开,成为孤岛,发生网络分区,此时zk5无法写入,svb无法重启。导致同机房sva无法调用svb(www.cnblogs.com/theRhyme/p/…)
问:你说下zab和raft的不同和相同
答:相同点是写请求都有leader统一发起,每一任leader唯一一个唯一标识 集群中半数以上的server投票通过方能达成一致 触发选举的条件相同,初次启动,与leader断开通信
不同点 前者为term+index,后者为epoch+count
raft每个server只能投票一次,candidate先到先得,zab的server可以发起多次投票,允许更多次新选票
请求异常时,raft会不断重试,幂等,zab的Follower会主动断开,重新加入集群