Fork me on GitHub
夸克的博客


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

  • 搜索

B+树能三层高度承载的数据大小

发表于 2022-07-01 | 分类于 mysql | 热度: ℃
字数统计: 244 | 阅读时长 ≈ 1

一颗B+树能承载多少数据的计算方式

单表数据上亿之后,索引的查询效果可能就没那么明显了。因为B+树的层级可能会变高,定位一条数据可能要经过好几次磁盘IO加载。

以三层高度为例,一颗B+树能承载的数据量是多少?

这里只考虑聚簇索引,数据页大小16kb,假设一行用户记录大小为1kb,所以叶子节点一个数据页存放记录16条。 对于非叶子节点,一个目录项由主键id和页号组成,主键id是(bigint)8个字节,Innodb中页号指针6个字节,所以一个非叶子节点的数据页有大概 16k / (8+6) = 1170条目录项记录。

那么此时高度为3的树可以存放 1170 1170 16 约等于2000w的数据,所以2000w以内的数据的索引树高度大概是1到3层。

一些系统调优案例的总结

发表于 2021-11-25 | 分类于 系统调优 | 热度: ℃
字数统计: 2,316 | 阅读时长 ≈ 9

1. SQL造成大对象JVM频繁full gc

哪些场景会频繁full gc?

  • 内存泄漏(对象没有及时被回收,某些代码问题导致)
  • 死循环一直创建对象
  • 一直创建大对象

主要看下大对象的场景:

  • 可能来源于数据库,查询的结果集太大。
  • 第三方接口传输的对象
  • 消息队列的消息太大

一个排查步骤

  • 是否有吻合时间点的发布?机器的CPU、网络IO、带宽是否正常?如果有先回滚和隔离机器。
  • 观察内存监控,是否有逐渐上升的内存占用,且触发GC之后没办法释放内存(内存泄漏)
  • 如果无内存泄漏(内存在每次fullgc之后能降下来)
    • jmap -histo工具查看存活对象
    • jmap -dump:format=b,file=file [pid]命令dump内存快照。利用mat工具分析
      • dump内存占用很小,考虑堆外内存的占用。
      • dump分析占用最多的对象和相关调用栈,查看可疑代码。
  • 比如:sql查出来的大对象一般伴随着网络IO变大,时间点吻合可以重点欢迎是不是sql的原因。

案例原因

  • 一个in list查找,参数没有判空,导致条件没有带上,查询命中很多记录。
  • sql一般会复用mybatis模板,某个sql模板如果参数没校验,可能产生大量记录的查询。

2. 内存泄漏案例

  • 内存溢出:程序没有足够的内存分配对象,会发生内存溢出。
  • 内存泄漏:对象没有及时释放,导致内存逐渐增加,就是内存泄漏。内存泄漏一般不会造成程序无法运行,但是会不断的累计造成内存不足,明显现象:触发了GC之后内存占用率不下降,且缓慢上升。
    image-20220701103254617

案例及原因

  • 本地内存存放商品数据,如果只存放热点商品,内存占用不会太大。但是如果全量商品加载到内存中,那么内存会不够。
  • 每个缓存记录加了7天的失效时间,保证淘汰掉不是热点访问的商品数据。
  • 经过一次重构之后,过期时间功能失效,没有过期时间淘汰,本地缓存越来越大。
  • 在一定时间之后,报警内存不足。dump内存之后发现缓存了大量的商品数据,造成内存泄漏,且因为本地缓存的引用一直没办法被GC清除。

3. 磁盘IO导致线程阻塞

  • 日常的CPU高的问题可以通过标准步骤:
    • top -H - p pid来查看cpu使用率高的线程id
    • 线程id转换16进制
    • jstack -l pid | grep ‘线程id16进制’
  • 但是有时候cpu突刺可能是在一个很短的时间内发生,此时可以采用shell脚本自动执行jstack,比如5s执行一次stack,每次执行完成之后放到不同的日志文件中。只保留2000个日志文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#!/bin/bash
num=0
log="/tmp/jstack_thread_log/thread_info"

cd /tmp
if [ ! -d "jstack_thread_log" ]; then
mkdir jstack_thread_log
fi

while ((num <= 10000));

do

ID=`ps -ef | grep java | grep gaea | grep -v "grep" | awk '{print $2}'`

if [ -n "$ID" ]; then
jstack $ID >> ${log}
fi

num=$(( $num + 1 ))

mod=$(( $num%100 ))

if [ $mod -eq 0 ]; then
back=$log$num
mv $log $back
fi

sleep 5

done

下一次响应变慢的时候,找到对应时间点的jstack日志文件,里面有很多线程阻塞在logback输出日志的过程,后来精简了log + 配置日志异步输出,则问题得以解决。

4. 死锁案例

1. update顺序导致的死锁

一个关单定时任务和人工后台关单的死锁案例

背景:
image-20220701103317304

  1. 定时关单的sql及加锁分析。
    1
    update order set status = 'canceled' where created_time > '2020-01-01 08:00:00' and created_time < '2020-01-01 08:00:00' and status = 'UNPAID';

因为created_time是二级索引,会先给二级索引加锁,再给对应的聚簇索引加锁。(加锁步骤如下所示)
image-20220701103343047

  1. 后台关单的sql及加锁分析
1
update t_order set status = 'CANCELLED' where id in (2, 3, 5) and status = 'UNPAID'

直接在聚簇索引的记录上加锁:
image-20220701103353119

可以看到定时任务的sql对主键索引加锁顺序是5,4,3,2。而后台的sql对主键加锁顺序是2,3,5。

比如第一个sql对3加锁之后,尝试对2加锁时发现后台取消的sql事务已经对2加锁,而此时第二个sql又尝试对3加锁,两个sql互相等待对方的锁,也就发生了死锁。

解决办法:

  • sql从语句上0保证加锁顺序一致。也就是都按照id排序加锁防止死锁。
  • 或者让批量操作,变为单个执行,减少锁占用的粒度和时间。
  • 取消订单都加一个分布式锁排队执行。

5. 后台导出数据引发的OOM

问题描述:公司的后台系统,导出功能之后偶发性的发生OOM。

排查步骤

  1. 因为是偶发性的,所以认为是后台系统的内存不足,单方面加大了内存。
  2. 但是没有解决问题,只是降低了OOM的频率。加入参数-XX:+HeapDumpOnOutOfMemoryError参数,来触发OOM时自动dump内存。
  3. dump内存之后进行分析,确认了大量string对象,跟其引用都是导出excel处服务。
  4. 结合Arthas查看,导出执行时间比较长,但是会在短时间内执行导出很多次,且用户session和参数是一致的。
  5. 现象总结就是短时间内多次导出相同的数据,且操作人是一个,产生大量的string对象。未能及时在年轻代清除(比如空间担保机制、比如超过了对象的阈值、比如创建对象速度过快未进行标记就晋升到老年代)。
  6. 最后排查是点击导出按钮没有在点击时置灰,导出交互会引导操作人多次点击,每次点击都会去读数据库的几万数据且导出excel,方法执行比较慢,对象无法回收,导致内存溢出。
  7. 最后解决:前端导出之后置灰,响应了之后才可以继续点击,且减少了查询订单信息的非必字段来瘦身对象,且分批次查询数据库改为了多线程。之后没有出现OOM。

6. 网站流量暴增后,网站反应出现卡顿

问题描述:在测试环境的页面速度很快,但到生产会变慢。推测访问量增大之后,对象创建变多,频繁的GC导致STW。

  1. 定位:使用jstat -gc pid指令观察JVM的GC次数,且观察Eden区增长的速度,来观察到触发MinorGC频率特别高,FullGC的触发频率次数和MinorGC几乎一致,甚至更多,这样导致STW业务线程,造成网页出现卡顿。
  2. 猜想是触发了老年代的空间担保机制,新生代在minorGC之前发现老年代可用连续空间比新生代所有对象小(或者是历次晋升的平均大小),就会触发空间担保机制,进而触发一次fullGC。
  3. 调大内存,老年代内存变大,且新生代的内存也变大,对象触发MinorGC的频次减少。
  4. 但是还是会发生卡顿,且卡顿时间比之前更长,发生卡顿频率降低。
  5. 继续jstat -gc pid观察,发现GC的次数不多,但是每次GC的时间变大,推测是因为内存调整变大,GC时间被拉长了。
  6. jinfo看到使用的是parallel垃圾回收器的组合,想到特点是多线程进行回收,但是整个回收过程会STW,造成GC期间不能正常使用。
  7. 这里替换为parNew + CMS垃圾回收器,CMS在垃圾回收阶段可以和用户线程并发执行,能有效的减少STW时间。
  8. 替换之后要设置合理的触发CMSGC的阈值,如果太大,比如在垃圾回收过程因为内存不足再次因为Concurrent mode failure触发full gc,会导致CMS退化为Serial Old单线程回收,整个回收过程都会STW。 总之使用了CMS要注意GC日志中是否有Concurrent mode failure关键字。

7. 未设置元空间大小应用启动很慢

MetaSpace会存放静态变量和很多类的信息,在启动时如果不设置元空间大小,则默认21M。

  1. 启动时间很长,且GC日志显示频发触发FullGC metaSpace不足
  2. 查看启动参数,未设置初始化的metaSpace大小。默认是21m
  3. 启动时会加载很多类,大量使用MetaSpace,21m很快被用完触发FullGC,然后MetaSpace动态扩容,然后再full gc再扩容的过程,使得启动变慢。
  4. -XX:MetaspaceSize 参数意思是触发元空间fullgc的阈值。max的参数代表元空间最大值,设置为-1时也受制于直接内存的大小。

mysql面试题总结

发表于 2021-10-01 | 分类于 面试题总结 | 热度: ℃
字数统计: 1,673 | 阅读时长 ≈ 6
  1. 描述一下B+树索引的结构?

  2. 什么叫做聚簇索引?
    聚簇索引是指叶子节点包含完整数据列,即主键索引和其余所有数据列,innodb中以主键索引构建的B+Tree即为聚簇索引,按照主键id排序。其余的索引皆为稀疏索引。

  3. B树和B+树的区别是什么?为什么选用B+树?

  4. 为什么不使用Hash结构来作索引?

  • Hash不能很好的支持范围查询
  • hash碰撞问题
  • hash一般需要一段连续的空间来存储hash表,这样方便维护hash表内部的链表或者数组的移动。
  1. 为什么不用二叉树来做索引?
  • 极端情况会弱化为链表,查找速度很慢。
  • 数据变多,树会变的很高,导致查找时IO次数增多。
  1. 为什么不用红黑树来做索引?
  • 红黑树的高度维护
  • 插入效率不高,需要维护平衡,伴随着左旋/右旋操作
  • 虽然平衡了,但是要树高度也会随着数据量变大而增高。
  1. 为什么建议主键用自增id而不使用UUID?

    • 因为聚簇索引是按照主键进行排序的,如果是UUID则是无序插入,是一个随机过程,可能在维护B+树时频繁造成页分裂和记录移动的场景,增加了不必要的磁盘开销。
    • 自增id占用的空间更小,能承载更多的数据。
  2. 什么是索引覆盖?什么是回表?

  • 查询的字段和条件字段能从索引中直接获取,不需要回表查找聚簇索引。
  • 回表:从二级索引过滤完成之后,需要根据二级索引关联的主键值去聚簇索引中读取完整的数据列。
  1. 索引失效的场景有哪些?
  • 索引列使用函数、运算等操作,不能使用索引。
  • in、is not null、!=、or等操作命中记录数太多,不如直接聚簇索引查询,可能成本分析导致不能走索引。
  • 联合索引没有使用最左前缀原则。
  • order by、group by没有使用到所以,用了文件排序或者临时表完成。
  • 索引字段发生了隐式替换。
  • like的通配符在最前面 like ‘%xxx’
  1. 设计索引的原则有哪些?需要注意哪些问题?
  2. 常见的SQL优化方向有哪些?怎么做SQL优化?
  3. explain type列有哪些值,代表什么含义?
  4. explain extra列有哪些值,代表什么含义?
  5. 索引下推是什么?
  6. 为什么联合索引是最左前缀原则?
  7. 索引树大约有多高?能存多少数据?
  8. 非叶子节点存放的是什么?
  • 非叶子节点是目录项数据页,其中存放的是索引值和指向叶子节点(或者下层非叶子节点)的页号。
  1. 事务的四大特性?
    ACID:
  • A:原子性
  • C:一致性
  • I:隔离性
  • D:持久性
  1. 原子性怎么实现的?
  • 在写记录的操作会写undo log,记录上隐藏列的roll_pointer会指向上一条版本记录,如果需要回滚可以找到对应的历史版本记录,保证了原子性。
  1. 持久性怎么实现的?
  • 持久性是通过redo log写磁盘来实现的。
  • 对数据修改的物理日志,比如“对表空间号为2的数据页号为50的偏移量xx地址的记录修改数据内容是什么”,在事务过程中会写入磁盘。
  • redolog写入是二阶段,和binlog保持了一致性。redo log可以设置在事务提交的时候写到OS Cache缓存或者磁盘。
  1. 事务的隔离级别有哪些?
  • RU、RC、RR、串行化
  1. 不同隔离级别在并发读写下有哪些问题?
  • 脏写:事务内修改了一个未提交事务修改的值。
  • 脏读:事务内读到了一个未提交事务修改的值。
  • 不可重复读:事务内多次读的结果不一致。
  • 幻读:读到了符合条件的多条新增记录。
  1. 什么是MVCC?
  1. 什么是undo log?
  2. 什么是redo log?
  3. 一条SQL执行的过程是怎样的?
  4. Mysql有那些锁?
  5. 插入一条SQL的过程?
  6. RR隔离级别下可重复读的原理?
  7. RR下的加锁语句分析。
  8. Innodb和Myisam存储引擎的区别有哪些?
  • Innodb支持事务、行锁。而Myisam不支持事务和行锁。
  • Innodb数据在聚簇索引上,而Myisam存储引擎索引和数据分离,索引只保存指向数据文件的地址指针。
  1. Buffer Pool是什么?描述一下结构和运行原理?
  1. Mysql count(字段) 和 count(*) 有什么区别?哪个效率高?
  • count(字段)不统计null值。
  • 效率相差不大,就是字段有二级索引会直接扫描二级索引,效率更高。
  1. 什么是file sort,怎么避免?
  • 不能使用到索引的order by场景会使用文件排序,文件排序可能在内存,也可能在磁盘中排序,效率很低。
  • 避免使用索引排序。
  1. 连接查询的原理是什么样子的?
  • 嵌套循环查询(有索引)和基于块的嵌套循环查询(没有索引,利用join_buffer)。
  • 连接查询分为驱动表和被驱动表,驱动表每行记录会一直和被驱动表进行查询。
  1. 连接查询有什么优化吗?什么是基于块的嵌套循环查询?
  • 对连接的字段加索引
  • 连接查询不能使用索引则调整join_buffer的大小。
  1. GAP锁的作用是什么?
  • GAP锁之间不相互阻塞,只阻塞插入过程的插入意向锁,避免在RR隔离级别下插入幻影记录。
  1. RR怎么解决一部分的幻读问题?
  • 快照读场景:事务内多次快照读,只会在第一次快照读时生成对应的ReadView,通过MVCC访问机制,去访问对应的undo log版本链,来解决了不可重复读和幻读问题。
  • 当前读:如果当前读是加锁读,那么是通过加GAP锁来防止间隙处插入符合条件新的记录。Gap锁阻塞插入意向锁,所以是通过锁排队场景来避免当前读的幻读的。
  1. 什么是binlog?和redolog的区别是什么?
  2. 死锁是怎么发生的?怎么检测死锁?
  3. 如何避免死锁?

image-20220701105558852

JVM面试题总结

发表于 2021-10-01 | 分类于 面试题总结 | 热度: ℃
字数统计: 1,085 | 阅读时长 ≈ 4
  1. 为什么堆区域采用分代?
  • 年轻代一般放新创建的对象,内存占用小,GC频繁但时间短。
  • 老年代存放生命周期长的对象,内存占用大,GC不频繁时间长。
  • 这样分代按照对象生命周期划分了区域,绝大部分都在年轻代创建之后被回收,实在不行还可以晋升老年代,有一定缓冲和保护内存的作用。
  1. 新生代为什么选择复制算法?为什么会有Survivor区域
  • 新生代分为Eden、S0、S1区,S区一个时间只能使用一个。新创建的对象会首先分配在Eden区,如果Eden不够会MinorGC放入S0区,下次MinorGC会清理Eden和S0,将存活对象复制到S1区。
  • 划分Survivor区主要是为了实现复制算法,复制算法的好处是复制之后内存是规整的,可以采用指针碰撞的方式来移动指针分配内存,不会有空间碎片。且复制之后可以一次性清除一个被复制的区域,效率高;缺点是只利用了一般的Survivor区域,利用率低。但是一般对象会在MinorGC之后被回收,存活对象很少,所以Survivor区不会很大。
  1. jdk有哪些类加载器?
  2. 双亲委派模型?
  3. 如何打破双亲委派模型?有哪些实例是打破的?
  4. 类加载的过程,每一步都干了什么?
  5. JVM内存区域模型描述一下。
  6. 如何判定一个对象是垃圾对象?
  7. 哪些对象可以作为GCRoots?
  8. 为什么标记不采用引用计数法?
  9. CMS垃圾回收有哪些过程?
  10. 什么操作或者场景下发生FullGC
  11. 描述一下java对象四种引用和应用场景。
  12. new一个对象一定堆上分配吗?
  13. 逃逸分析和标量替换是什么?
  14. JIT编译会做哪些事情?
  15. 常见的垃圾回收器及其特点?
  16. CMS垃圾收集器一定不会STW吗?哪些阶段会STW,哪些阶段会并发执行?
  17. 描述一下Java对象的结构?对象头中的结构?
  18. G1垃圾回收器的特点什么?
  19. G1垃圾回收器和CMS的区别是什么?
  20. 常用的JVM参数有哪些?
  21. 常用的JDK调优工具有哪些?
  22. 线上CPU打满怎么排查?
  23. 内存泄漏和内存溢出有什么区别?
  • 内存泄漏:已经无用的对象还被GCRoots连通引用,被标记为存活无法回收,
  • 内存溢出:当前内存不足以开辟出连续内存用来分配对象,则会内存溢出。
  1. OOM有哪些类型?怎么排查OOM?
  • OOM:unable to create new native thread。线程栈也是堆内存的一部分,当无法创建机器的native线程会报错OOM。
  • OOM:MetaSpace。元空间无可用内存会OOM,比如加载了很多类或者动态生成了很多类,导致元空间内存溢出。
  • OOM:heapSpcae。堆空间内存溢出,比如老年代FullGC之后还是没有足够空间,则会OOM
  • OOM:directMemory。直接空间(堆外内存)不足也会造成OOM。
  1. 线上JVM调优经验?
  2. 线上其他问题排查经验?
  3. 线上频繁出现FullGC怎么排查?
  4. 什么是老年代空间担保机制? promotion failure
  5. 在CMS中,什么是Concurrent mode failure,怎么触发的,有什么结果?
  6. 元空间不足会FullGC吗?元空间的参数不设置会怎么样?
  7. 线程的状态有哪些?调用sleep和Locksupport.park、synchronized锁阻塞、lock.lock分别是什么状态?
  8. 常量池在JVM内存的哪个区域?常量池中存放的是什么?
  9. JVM内存模型中,线程私有的区域都有哪些?
  10. 什么是直接内存?怎么应用?怎么对直接内存进行垃圾回收。
  11. OOM之后的Dump文件很小,且分析没有异常,可能是发生什么?
  12. 什么是栈帧?栈帧中存放哪些结构?
  13. 1.7的方法区和1.8的元空间有什么区别?为什么要废弃方法区?常量池和静态变量1.8jdk在哪个区域?

并发编程面试题总结

发表于 2021-09-22 | 分类于 面试题总结 | 热度: ℃
字数统计: 1,033 | 阅读时长 ≈ 4
  1. Java线程的状态有哪些?
  2. 状态迁移大概是怎样的?wait()、sleep()、LockSupport.lock、synchronized、lock.lock()执行之后对应的锁状态是哪些?
  3. 如何实现线程通讯?
  4. Callable和Runnable的区别?
  5. interrupt()方法怎么中断的线程?如何响应中断?
  6. CPU的三级缓存模型是怎么样的?
  7. 什么是JMM工作模型?为什么要这样去划分工作内存和共享内存?
  8. 并发的三个特性是什么?什么叫做可见性?什么叫做有序性?
  9. 指令重排是什么?
  10. 如何禁止指令重排?
  11. volatile的底层实现原理是什么?
  12. 什么是mesi缓存一致性协议?
  13. synchronized有哪些用法?static方法锁的是什么?
  14. synchronized在jdk高版本有什么优化?
  15. 什么叫轻量级锁?
  16. 什么叫偏向锁?偏向锁的过程?
  17. 什么叫自旋锁?
  18. 上边这几种锁的适用场景?
  19. synchronized的底层实现原理是什么?什么是Monitor对象?
  20. wait()方法和notify()方法为什么一定要在synchronized区域中调用。
  21. 锁竞争的过程,竞争失败会怎样,如何被唤醒?
  22. 什么是用户态和内核态?为什么线程上下文切换会是开销大的操作?
  23. 可重入锁ReentrantLock和synchronized的区别?
  24. 如果利用AQS实现的可重入锁?
  25. AQS内部原理?
  26. 怎么实现的公平锁?
  27. 怎么实现的锁超时等待?
  28. 怎么实现的加锁失败在队列中等待?
  29. Conditional怎么在AQS中实现?
  30. AQS还有哪些应用?
  31. JUC这个包常用什么?怎么看待JUC这个包的结构?能分几层?
  32. 有哪些阻塞队列?
  33. CAS是什么?
  34. 以AtomicInteger为例,怎么依赖CAS实现?UnSafe怎么在其中使用的?
  35. CAS的ABA问题是什么?如何解决?
  36. UnSafe还在哪用到了?
  37. 线程池的工作原理?
  38. 线程池核心参数?
  39. 为什么要自己实现线程池参数而不用Executors工具类?
  40. 怎么对线程池进行监控?
  41. submit和execute方法有什么区别,在异常处理方面有什么区别?
  42. FutureTask怎么实现的?如何实现get阻塞等待的?怎么实现cancel的?
  43. 线程池核心线程数量大小选择?
  44. 线程池如何实现线程退出的?
  45. 线程池的优点?
  46. 线程池如何优雅关闭?
  47. 线程死锁怎么排查的?
  48. 生产者消费者模型
  49. CountDownLatch、Semaphore、CyclicBarrier都什么作用?用在什么场景?
  50. CompletableFuture怎么使用?使用有什么注意点吗?
  51. Future和CompletableFuture区别,怎么实现非阻塞的获取结果异步回调的?
  52. ThreadLocal实现原理?
  53. ThreadLocal和Thread的关系?
  54. ThreadLocal使用场景有哪些?
  55. ThreadLocal会内存泄漏吗?
  56. ThreadLocalMap怎么解决hash碰撞的?
  57. ThreadLocalMap怎么删除无用元素的?
  58. ThreadLocalMap内部结构
  59. 为什么ThreadLocal的引用是WeakedHashMap?
  60. ThreadLocal怎么跨线程引用?
  61. 了解ThredLocal的扩展吗?比如Netty的FastThreadLocal?
  62. 什么是ForkJoin?什么是工作窃取算法?
  63. Stream的原理是什么?Stream并行流怎么实现的?
  64. HashMap put流程?
  65. 为什么HashMap负载因子是0.75?
  66. HashMap数据结构是什么样的?
  67. 为什么要采用红黑树?
  68. 描述一下红黑树的结构?
  69. 为什么长度是2的整数次幂?
  70. HashMap是线程安全的吗?HashTable呢?
  71. HashMap线程不安全的点在哪?
  72. 扩容的时候为什么会发生死循环?
  73. ConcurrentHashMap怎么实现的线程安全?
  74. ConcurrentHashMap1.7和1.8的区别?
  75. ConcurrentHashMap size()操作怎么计算的?一定会加锁吗?
  76. ArrayList扩容机制是什么?
  77. LinkedList和ArrayList适用于什么场景?
  78. LinkedList是双向链表吗?
  79. 线程安全的List或者Set有哪些?
  80. HashSet怎么实现的?
  81. CopyOnWrite机制适合于什么场景?
  82. LongAdder怎么实现更高的效率?
  83. 了解过无锁队列Disputor吗?原理是什么?

索引数据结构的选择

发表于 2021-08-14 | 分类于 mysql | 热度: ℃
字数统计: 994 | 阅读时长 ≈ 3

二叉树

什么是二叉树?

  • 每个节点至多只有两棵子树,左子树和右子树。左子树和右子树。
  • 逻辑上二叉树有五种基本形态,空二叉树、只有一个根节点的二叉树、只有左子树、只有右子树、完全二叉树(满二叉树)
  • 遍历二叉树有前序遍历、中序遍历、后续遍历。

image-20220701101436662

数据库的索引是要求排好序的数据结构。引入了二叉查找树。

二叉查找树

  • 在二叉树的基础上,加上左子树的节点值总是小于根的节点值,右子树的节点值总是大于跟的节点值,也就是左子树节点值 < 根的节点值 < 右子树节点值。

设计合理的时候可以将一组数据转化为二叉查找树,时间复杂度和二分查找一致。
image-20220701101447155

但是存在设计不合理的场景:
当二叉查找树不平衡,可能退化为一个线性的链表,查找时间复杂度高了起来。
image-20220701101456153

二叉搜索树可能不平衡,所以再看下平衡二叉树

平衡二叉树(AVL树)

  • 符合二叉查找树的定义,然后再满足任何节点的两个子树的高度之差的绝对值不超过1。

image-20220701101509334

相对于二叉查找树,平衡二叉树查找效率稳定。

但也有缺点:

  • 查询速度快,但是插入数据时需要维护平衡二叉树,需要1次或多次左旋或者右旋来完成更改节点之后的平衡性。
  • 平衡二叉树随着数据增多,高度会增加,作为索引的数据结构可能查找一个数据可能要好多次IO。

B-tree和B+Tree

平衡二叉树存在树过高导致的IO问题,B-Tree用来解决的思想是在每一个节点上存放多个元素,来降低树的高度,以来减少磁盘IO。

B-Tree

B-Tree是一个多路搜索树,m阶的B-tree有如下特性:

  • 每个节点最多有m棵子树。
  • 若根节点不是叶子节点,则至少有两个孩子。
  • 每个节点上存有关键字(索引值),且节点的子树按照关键字排序。
  • 每个节点上的关键字都有一个指针,指针指向子树的所有节点索引值都小于当前索引值,都大于上一个索引值。
  • 所有叶子节点都在同一层。

image-20220702044034037

B-Tree中索引和除了索引之外的列是放在一起的,非叶子节点没有冗余索引,同时上层节点中索引关联的指针指向叶子节点,索引节点的左边孩子节点都比索引值小,右边节点都比索引值大。

这样的多路搜索已经降低了树的高度。

但Mysql还是采用了B+Tree。

B+Tree

是B-Tree的变种,也是一种多路搜索树,有几点不同:

  • 非叶子节点的子树指针与索引个数相同
  • 非叶子节点只存储索引信息,不存放其余的列。
  • 非叶子节点冗余了索引值。
  • 所有叶子节点之间用双向指针连接,形成双向链表。
  • 叶子节点存放索引之外,还存放了其余列。
    image-20220701101549392

为什么用B+Tree而不用B树?

  1. 非叶子节点不存放数据,单个节点能装下更多索引的值,降低了树的高度。
    image-20220701101605998

  2. 叶子节点双向指针连接,叶子节点中记录项用单向指针连接,提高了区间访问和范围查找的效率。

Hash结构

为什么不使用hash结构来存放索引呢?

  • hash碰撞和扩容问题。
  • hash数据结构也能快速根据索引列定位数据。hash算法的优点
  • hash能支持 = in查询,但对于范围查询不好支持。

redis和zk分布式锁比较

发表于 2021-07-24 | 分类于 分布式锁 | 热度: ℃
字数统计: 579 | 阅读时长 ≈ 2

比较

性能和可用性

  • Redis加锁性能更高,但会出现锁丢失数据不一致问题;ZK加锁性能没有那么高,但集群通过ZAB协议(写入时原子广播,故障恢复时zxid最大作为Leader节点)保证了数据一致性。

  • Redis主从/哨兵或者cluster模式下都是AP的,保证了可用性而采用master异步复制数据给slave,存在master加锁成功之后挂了,slave未同步master数据之后再升级为新的主节点,之前的master锁数据就丢失的场景。

    • Redis也提供了RedLock算法来解决锁丢失问题,写入奇数个master节点,但性能差且对集群部署方式有要求。
  • ZK集群是CP模式,写leader节点数据的过程中需要同步广播数据到follower节点,保证了数据一致性,不会出现数据不一致的场景。但是如果leader节点挂了,重新选举的过程中zk集群不可使用,牺牲了可用性。

加锁原理

  • Redis加锁(Redisson)是利用了hash结构存放,key(hash的field)是uuid:threadid,value是重入次数来实现的。默认Redisson是非公平锁,其他被阻塞的客户端订阅锁释放或者自旋重试获取锁。
  • Zk是创建临时顺序节点来实现分布式锁的,多个客户端会在写入的时候判断写入的序号是否为当前临时节点的第一个,不是的话注册Watcher监听上一个序号的临时节点,wait()实现阻塞,等上一个节点删除之后触发notifyAll()唤醒来重新尝试加锁。

锁续期

Redis的Redisson实现分布式锁在重入加锁、重入解锁有锁续期逻辑,且调用默认的lock方法时会注册一个watchdog任务来完成锁续期的逻辑。所以可以支持锁续期;ZK默认不支持锁续期,当然zk是在执行完成之后去释放锁,不存在业务执行超过锁时间的问题。

公平锁

  • Redisson要实现公平锁还得借助增加list(排队)和zset(加锁超时),复杂度变高。
  • zk监听上一个序号的临时节点,支持公平锁。先创建临时节点的先加锁。

ZK实现分布式锁

发表于 2021-07-21 | 分类于 分布式锁 | 热度: ℃
字数统计: 2,678 | 阅读时长 ≈ 12

zk实现分布式锁的核心原理

zk实现分布式锁的核心原理是利用临时顺序节点和其监听Watcher机制。临时节点有些特点:

  • 生命周期是随着客户端的session周期的,session长时间心跳失败或者客户端关闭session,临时节点会自动删除。
  • 临时目录节点不能创建子目录

基本过程:当客户端抢到锁之后为这个客户端分配一个临时节点,只要锁没有释放就一直持有这个临时节点,当锁释放或者服务意外宕机之后,临时节点会删除,其他客户端可以继续抢占锁(创建临时节点)

image-20220701072706757

未竞争到锁的客户端会阻塞等待,并会开一个监听器监听上一个节点,如果上一个节点释放了锁,那么立即得到通知去上锁。所以加锁的客户端需要感知到上一个客户端节点是谁,也就需要有顺序编号的临时节点,即临时顺序节点。

image-20220701072715929

最后的释放锁就是持有锁的节点删除临时顺序节点,注册监听的其他客户端会收到通知,然后去创建自己的临时顺序节点加锁。

zk实现分布式锁的demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
/**
* curator封装的分布式锁api demo
* 提供了几种锁
* InterProcessMutex 分布式可重入互斥锁
* InterProcessReadWriteLock 分布式可重入读写锁
* InterProcessSemaphoreMutex 分布式不可重入互斥锁
* InterProcessSemaphoreV2 分布式信号量
*/
public class CuratorDistributeKLockDemo {
static {
// 输出日志info级别
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
List<Logger> loggerList = loggerContext.getLoggerList();
loggerList.forEach(logger -> {
logger.setLevel(Level.INFO);
});
}

public static void main(String[] args) throws Exception{
// zk地址 如果是集群 多个节点地址逗号隔开
String address = "127.0.0.1:2181";
// 重试策略 连接不上服务端的时候 会重试 重试间隔递增
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
CuratorFramework client = CuratorFrameworkFactory.newClient(address, retryPolicy);
// 启动zk客户端
client.start();

try {
testInterProcessMutex(client);

System.in.read();
} catch(Exception e) {
e.printStackTrace();
}
finally {
client.close();
}
}

static void testInterProcessMutex(CuratorFramework client) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
InterProcessMutex lock = new InterProcessMutex(client, "/curator-demo/lock");
for (int i =0 ; i < 3; i++) {
executorService.execute(new Task(lock, ("任务" + i)));
}
}
static class Task implements Runnable {
private final String taskName;
private final InterProcessMutex lock;
public Task(InterProcessMutex lock, String taskName) {
this.taskName = taskName;
this.lock = lock;
}
@SneakyThrows
@Override
public void run() {
while (true) {
lock.acquire();
try {
info(taskName + ": 成功获取锁 #1");
// 模拟业务耗时
Thread.sleep(1000);
// 尝试锁重入
methodA();
} catch (Exception e) {
System.out.println(taskName + ": Happen Exception: " + e.getMessage());
} finally {
info(taskName + ": 释放锁 #1\n");
try {
lock.release();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}


void methodA() {
try{
lock.acquire();
info(taskName + ": 成功获取锁 #2");

// 模拟业务耗时
Thread.sleep(1000);
} catch (Exception e) {
System.out.println(taskName + ": Happen Exception: " + e.getMessage());
} finally {
info(taskName + ": 释放锁 #2");
try {
lock.release();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
private static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSS");

private static void info(String msg) {
String time = formatter.format(LocalTime.now());
String thread = Thread.currentThread().getName();
String log = "["+time+"] "+ " <"+ thread +"> " + msg;
System.out.println(log);
}
}

zk分布式重入锁实现

加锁

客户端加锁入口是:

1
2
3
4
5
// org.apache.curator.framework.recipes.locks.InterProcessMutex#acquire(long, java.util.concurrent.TimeUnit)
public boolean acquire(long time, TimeUnit unit) throws Exception {
// 直接调用internalLock()方法
return internalLock(time, unit);
}

内部的internalLock方法(删减代码版):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// org.apache.curator.framework.recipes.locks.InterProcessMutex#internalLock
private boolean internalLock(long time, TimeUnit unit) throws Exception {
// 获取当前线程
Thread currentThread = Thread.currentThread();
// 尝试加锁
String lockPath = internals.attemptLock(time, unit, getLockNodeBytes());
// 加锁成功的话就放到threadData里 是一个currentHashMap中,缓存在本地是为了锁重入
if ( lockPath != null ) {
LockData newLockData = new LockData(currentThread, lockPath);
threadData.put(currentThread, newLockData);
return true;
}
return false;
}

尝试加锁调用的internals.attemptLock方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// org.apache.curator.framework.recipes.locks.LockInternals#attemptLock
// 下面是删减后的代码
String attemptLock(long time, TimeUnit unit, byte[] lockNodeBytes) throws Exception {
try {
// 创建这个锁
ourPath = driver.createsTheLock(client, path, localLockNodeBytes);
// 多个client抢锁时互斥阻塞等待
hasTheLock = internalLockLoop(startMillis, millisToWait, ourPath);
}
catch ( KeeperException.NoNodeException e ) {
//...
}
return null;
}

// createsTheLock内部逻辑
// org.apache.curator.framework.recipes.locks.StandardLockInternalsDriver#createsTheLock
// 下面代码是删减后的代码
public String createsTheLock(CuratorFramework client, String path, byte[] lockNodeBytes) throws Exception {
return client
.create()
.creatingParentContainersIfNeeded()
.withProtection()
// 就是在对应的目录下创建一个临时顺序节点
.withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
.forPath(path, lockNodeBytes);
}

可以看到加锁就是创建了一个临时顺序节点,加锁成功会构建一个LockData类型的对象,内部维护了线程id、重入次数和锁节点的路径,根据线程id为key缓存在本地的一个ConcurrentHashMap中。

创建的锁临时顺序节点示例:
image-20220701072730143

锁的目录会加一个UUID防止幽灵节点。即创建成功但因为网络原因客户端没拿到成功的结果再次去重试创建对应的节点,会带着UUID去查询,如果存在说明不需要去重试。

整体流程:
image-20220701072739680

锁重入

锁的重入次数就是维护在本地的一个map中,value是一个LockData结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private final ConcurrentMap<Thread, LockData> threadData = Maps.newConcurrentMap();

// LockData结构
private static class LockData
{
final Thread owningThread;
final String lockPath;
// 锁的重入次数
final AtomicInteger lockCount = new AtomicInteger(1);

private LockData(Thread owningThread, String lockPath)
{
this.owningThread = owningThread;
this.lockPath = lockPath;
}
}

可以看到在加锁流程中,以当前线程为key查询LockData对象如果存在,直接是把其lockCount++,以此来记录锁的重入次数。

1
2
3
4
5
6
7
8
// 根据当前线程获取锁对象,如果获取到了,那肯定是有锁的,这次是锁重入
LockData lockData = threadData.get(currentThread);
if ( lockData != null ) {
// 锁重入的关键:其实就是我们上面说的那个AtomicInteger原子类自增1
lockData.lockCount.incrementAndGet();
// 加锁直接返回成功
return true;
}

所以锁重入的流程嵌入加锁流程为:
image-20220701072759686

锁互斥等待

当客户端尝试获取锁时,如果发现/lock目录被占用,这里为了满足锁的互斥性,当前客户端要互斥等待Watcher回调通知。

这里互斥等待的逻辑在LockInternals#internalLockLoop方法中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// org.apache.curator.framework.recipes.locks.LockInternals#internalLockLoop
// 下面代码是删减后的代码
private boolean internalLockLoop(long startMillis, Long millisToWait, String ourPath) throws Exception {
boolean haveTheLock = false;
try {
while ((client.getState() == CuratorFrameworkState.STARTED) && !haveTheLock) {
// 获取path下对应临时顺序节点,并按编号从小到大排序。底层采取的java.util.Comparator#compare来排序的
List<String> children = getSortedChildren();
// 获取当前线程创建的临时顺序节点名称
String sequenceNodeName = ourPath.substring(basePath.length() + 1);
// 这个方法底层就是判断当前节点编号是不是children里的第一个,是的话就能抢锁,不是的话就计算出上一个节点序号是谁,然后下面监听这个节点。(因为按照编号排序了,所以可以得出上一个节点是谁)
PredicateResults predicateResults = driver.getsTheLock(client, children, sequenceNodeName, maxLeases);
// 如果当前客户端就是持有锁的客户端,直接返回true
if (predicateResults.getsTheLock() ) {
haveTheLock = true;
} else {
// 如果没抢到锁,则监听上一个节点
String previousSequencePath = basePath + "/" + predicateResults.getPathToWatch();
synchronized(this) {
try {
// 监听器,watcher下面分析
client.getData().usingWatcher(watcher).forPath(previousSequencePath);
// 重点在这了,wait(),等待。也就是说没抢到锁的话就开启监听器然后wait()等待。
wait();
} catch ( KeeperException.NoNodeException e ) {}
}
}
}
}
return haveTheLock;
}

逻辑为:

  • 查询所有的临时顺序节点(全部抢锁的客户端),按照编号从小到大排序;
  • 判断当前客户端的编号是不是第一个,是的话代表加锁成功。不是的话要计算上一个节点序号,注册Watcher监听此节点。(解锁之后删除此节点会收到Watcher的通知回调)
  • 调用wait()方法实现阻塞。

image-20220701072815675

注册的Watcher通知回调逻辑:(其实就是notifyAll来实现)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// org.apache.curator.framework.recipes.locks.LockInternals#watcher
private final Watcher watcher = new Watcher() {
@Override
public void process(WatchedEvent event) {
// 调postSafeNotify方法
client.postSafeNotify(LockInternals.this);
}
};

// org.apache.curator.framework.CuratorFramework#postSafeNotify
default CompletableFuture<Void> postSafeNotify(Object monitorHolder) {
return this.runSafe(() -> {
synchronized(monitorHolder) {
// 重点在这里,notifyAll。通知所有等待(wait)的节点。
monitorHolder.notifyAll();
}
});
}s

锁释放

锁释放调用的是release方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// org.apache.curator.framework.recipes.locks.InterProcessMutex#release
// 下面代码是删减后的代码
public void release() throws Exception {
// 获取当前线程
Thread currentThread = Thread.currentThread();
// 获取当前线程的锁对象,从ConcurrentHashMap里获取
LockData lockData = threadData.get(currentThread);
// 锁重入次数-1,然后看看是不是大于0,如果大于0那代表有锁重入,直接-1,不删除锁节点,因为没释放完全。
int newLockCount = lockData.lockCount.decrementAndGet();
if ( newLockCount > 0 ) {
return;
}
try {
// 如果锁重入次数为0了,那就释放锁
internals.releaseLock(lockData.lockPath);
}
finally {
// 释放完后从ConcurrentHashMap里移除
threadData.remove(currentThread);
}
}

流程为:

  • 根据线程对象从锁缓存中获取锁对象,对其中的锁重入 次数-1
  • 如果大于0 说明是重入的解锁逻辑,直接返回。
  • 如果锁重入次数为0,直接去删除对应的临时顺序节点即可。(删除之后ZK会发送之前阻塞在这个路径节点下Watcher通知,通知逻辑及时notifyall wait在同一锁对象的client线程,让其再去按照序号去竞争锁,可以看到保证了公平性)
  • 缓存中remove此锁对象。

image-20220701072828067

zk实现分布式锁小结

image-20220701072840287

一些问题

为什么去设计排队,让被阻塞的客户端去只监听上一个序号的目录节点呢?

最基本的zk分布式锁实现:不考虑临时顺序节点,只去创建一个临时节点/lock。

未竞争到锁的客户端去监听外层的/lock节点的删除事件。

这带来两个问题:

  • 非公平。很显然没有顺序,再次抢占锁的客户端是非公平的。
  • 羊群效应:比如1000个client被锁节点阻塞,都注册Watcher到/lock节点,当持有锁的节点释放锁时候,会并发去抢占锁,给ZK服务端带来羊群效应。

image-20220701072850411

基于这两个问题,利用ZK的临时顺序节点的功能,让被阻塞的节点只监听上一个序号的节点,去注册Watcher,实现了公平排队获取,也避免了羊群效应。

Redisson锁续期

发表于 2021-07-16 | 分类于 分布式锁 | 热度: ℃
字数统计: 1,082 | 阅读时长 ≈ 5

watchDog核心原理

定时检测业务是否执行完毕,没结束的话观察这个key是否经历的锁超时时间的的三分之一,如果经历了,那么进行到期时间的重新设置,防止业务没有执行完就被释放锁造成的并发场景。

阅读全文 »

dubbo服务调用过程

发表于 2021-07-16 | 分类于 dubbo | 热度: ℃
字数统计: 537 | 阅读时长 ≈ 2

调用过程

消费端

消费端的发送请求模型如下:
image-20220716000723857

调用过程总结

客户端发送请求

  1. 调用一个dubbo接口,此时调用的是在服务引入过程生成的动态代理对象,内部代理逻辑就是构造RpcInvocation请求参数,然后调用Invoker.invoke方法。
  2. Invoker会通过ClusterInvoker,依次调用服务目录Directory的list、服务路由的router、负载均衡的 select选出一个具体的Invoker,本身也会根据Cluster的配置实现对应的集群容错的处理。
  3. 按照顺序执行客户端的invokerFilter链,然后走到一个AsyncToSyncInvoker,内部会持有DubboInvoker.invoke方法,内部会通过NettyClient发起真正的远程调用。在其中返回的AsyncRpcResult中会持有发起调用时生成的DefaultFuture,且每个Future都绑定了一个id,在AsyncToSyncInvoker中就是调用了其get()方法来实现的Netty异步网络调用但阻塞了当前调用的业务线程。

服务端接收请求

  1. NettyServer收到请求之后,根据具体的协议反序列化为对象,然后按照派发策略将请求消息封装为ChannelEventRunnable发送给线程池中(DubboServerHandler)执行,IO线程返回继续处理读/写请求。
  2. Dubbo服务线程会判断消息类型最后执行到在DubboProtocol内部实现的ExchangeHandlerAdapter,将请求调用到期reply方法处理。
  3. Dubbo服务线程根据serviceKey从服务导出时存入的exporterMap中找到对应的DubboExporter,经过一系列的服务端的InvokerFilter链之后,最终通过反射调用到真正服务实现类的方法。最后通过DubboServerHandler向客户端发送结果Response。

客户端接收返回请求

  1. 和服务端接收请求一样,经历反序列化之后,按照派发策略派发给DubboClientHandler线程执行。
  2. 根据之前绑定DefaultFuture的id从本地Future的缓存Map中找到DefaultFuture,通过DefaultFuture.received(response)来唤醒阻塞在AsyncToSyncInvoker的client线程,将最后的结果返回到调用处即可。

客户端/服务端接收数据(请求、结果写回)之后的线程派发模型

image-20220716000818307

12…12
夸克

夸克

愿赌服输

114 日志
32 分类
121 标签
GitHub E-Mail csdn
© 2022 夸克 | Site words total count: 168.9k
|
主题 — NexT.Muse v5.1.4
博客全站共168.9k字

载入天数...载入时分秒...