学习总结
2021-04-12 00:03:58 66 举报
AI智能生成
针对使用到的框架和一些基础知识的总结
作者其他创作
大纲/内容
Spring
IoC DI
假设依赖链路上某个对象多次出现<br>第一次getBean时将对象工厂放入三级缓存<br>第二次getBean时回调对象工厂获取对象,并将获取到的共享对象放入二级缓存<br>后续每次getBean时直接从二级缓存获取对象<br>最后由第一次getBean的方法完成Bean的初始化,并将初始化好的对象移入一级缓存<br>
ApplicationContext <br>extends <br>EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory,<br> MessageSource, ApplicationEventPublisher, ResourcePatternResolver<br>
ApplicationContext是BeanFactory的一个包装器实现<br>添加额外功能,配置解析,环境变量支持,多语和事件驱动机制
AOP
首先创建好原始对象并完成依赖注入之后<br>initializeBean阶段,在BeanPostProcessor中创建代理对象<br>调用时调用了代理方法,切面责任链通过递归调用和目标方法穿插执行
MVC
第一次调用首先初始化九大组件
从IoC容器获取相应的Bean去填充成员变量
doDispatch
根据请求得到Handler链路执行器<br>该执行器内部存储的Handler一般为Controller的某个method<br>
找到能够处理该handler的handler适配器
由该适配器处理handler<br>
调用目标方法 <br>-> Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs)<br>获得返回结果
选择一个返回值处理器进行处理
对于REST接口使用RequestResponseBodyAdviceChain
handler.handleReturnValue
writeWithMessageConverters<br>将反回结果经由消息转换器写入response<br>
此时请求端收到返回结果
对于非Rest请求,会返回mav
processDispatchResult<br>处理请求结果<br>
render(mv, request, response)<br>-> view.render(mv.getModelInternal(), request, response)<br>根据视图名称找到视图进行渲染
Boot
启动流程和自动加载机制原理<br>https://www.processon.com/view/link/6075c6841e08534f371c2f0f<br>
Mybatis
创建会话工厂
解析配置 -> 全局配置文件和Mapper配置文件<br>
解析存储resultMaps、parameterMaps
解析存储mappedStatements:namespace+id到MappedStatement(SQL信息)的映射
根据namespace将接口类型type与MapperProxyFactory进行绑定注册至MapperRegistry<br>用于后续创建代理对象时使用(JDK代理对象使用MapperProxy作为拦截器)<br>(接口类型非必须,存在时才注册,在于用户是否使用接口的方式执行SQL)
存在接口类型的情况则通过MapperAnnotationBuilder解析接口上的注解<br>
解析完毕返回Configuration创建DefaultSqlSessionFactory<br>
创建会话
拿到TransactionFactory去创建一个TX<br><br>注意:TXManager为全局配置文件的必配属性,由该配置生成TXFactory
根据执行器类型和TX去创建一个执行器
若全局开启二级缓存则用CachingExecutor包装原有的执行器<br>
全局开启代表会使用CachingExecutor包装实际的执行器<br>使Executor拥有了和二级缓存交互的能力
Mapper中使用Cache标签代表针对Mapper生成一个二级缓存<br>此时执行器还没有和二级缓存交互的能力<br>只有全局和Mapper同时开启时二级缓存才生效<br>
插件链通过JDK动态代理层层包装执行器Executor
返回DefaultSqlSession
获取代理对象
通过MapperRegistry获取MapperProxyFactory创建代理对象<br><br>MapperProxyFactory由创建会话工厂的第四步完成注入<br><br>MapperProxy作为方法拦截器<br>
MapperProxy包含:<br>SqlSession用于执行SQL;<br>mapperInterface接口类型用于和方法名称共同组成唯一ID找到SQL信息;<br>methodCache用于缓存提高性能。<br>
SQL执行
第一次调用时构建MapperMethodInvoker并缓存<br>然后委托内部的MapperMethod执行<br>
在MapperMethod执行方法内会解析方法参数<br>拿到保存的SQL的ID后交给SqlSession执行<br>此时相当于体现了接口的功能,即不需要手动通过设置ID和参数去调用SqlSession<br>
SqlSession根据Configuration拿到MS交由执行器执行语句<br>
执行器执行SQL<br>假设此时为Select
执行Executor的插件逻辑<br>
执行CachingExecutor逻辑<br>
创建CacheKey<br>
组成:Id、Offset、Limit、Sql、value、环境ID<br>注意:通过Hash提高了比较性能
当Mapper.xml中配置了Cache标签则走二级缓存逻辑
注意:因为二级缓存跨会话,所以需要与事务绑定
<br>
二级缓存写入时首先会放入TransactionalCache的entriesToAddOnCommit<br>当事务提交时才会写入实际缓存
执行器执行SQL
查询本地缓存,本地缓存与执行器Executor1对1绑定,执行器与Session1对1绑定
实际执行
创建StatementHandler,会被插件包装
创建ParameterHandler,会被插件包装
创建ResultSetHandler,会被插件包装<br>
根据StatementHandler创建Statement
通过ParameterHandler对Statement设置参数
调用ps.execute();执行查询
通过ResultSetHandler处理结果集
调用ps.execute();执行查询
本地缓存为STATEMENT级别则清除缓存
Vertx
线程模型原理<br>https://www.processon.com/view/link/603c8acd07912913b4f26903<br>
Netty
详细总结<br>https://www.processon.com/view/link/602cbfa8e0b34d208a81c015#map<br>
服务端启动
入口:bind()
initAndRegister()
new NioServerSocketChannel()<br>
openServerSocketChannel 创建 JDK ServerSocketChannel
id
unsafe -> NioMessageUnsafe<br>
pipeline -> DefaultChannelPipeline<br>
HeadContext
TailContext
SelectionKey.OP_ACCEPT 保存感兴趣的事件<br>
ChannelConfig
init(channel)
设置各种参数
pipeline添加ServerBootstrapAcceptor<br>
用于处理新连接接入
bossGroup.next().register(channel)<br><br>根据Chooser选择下一个NioEventLoop交给channel
AbstractChannel.register(eventLoop, p)
NioEventLoop.execute
第一次执行<br><br>startThread()<br>
executor.execute
由ThreadPerTaskExecutor创建一个线程执行任务
SingleThreadEventExecutor.this.run()<br><br><b>此时NioEventLoop正式启动,开始无限循环</b><br>
addTask(task)
unsafe.register<br>-> AbstractChannel.this.eventLoop = eventLoop<br>此时保存Channel和EventLoop的关系
register0(p)<br>
doRegister()
NioServerSocketChannel内部的JDK Channel<br><br>注册至NioEventLoop内部的JDK Selector<br><br>注意:此时尚未监听任何感兴趣的事件<br>
pipeline.invokeHandlerAddedIfNeeded()<br>
ChannelInitializer.handlerAdded<br><br>-> initChannel(ctx)
pipeline.fireChannelRegistered()
doBind0(channel)
channel.bind()<br>-> pipeline.bind<br>-> tail.bind<br><br>bind事件从尾到头传播<br>
HeadContext.bind<br>-> unsafe.bind<br>-> doBind<br><br>此时JDK Channel与端口进行绑定<br>
pipeline.fireChannelActive()<br>通道激活传播事件
AbstractChannelHandlerContext.invokeChannelActive(head)
HeadContext.channelActive
当传播结束时,调用readIfIsAutoRead();
channel.read()<br><br>-> tail.read()<br><br>-> HeadContext.read()
unsafe.beginRead()
Channel.doBeginRead
selectionKey.interestOps(interestOps | readInterestOp)<br><br>开始对之前保存的感兴趣事件进行监听,此时为Accept事件
注意:<br>对于Inbound事件,从头部往尾部传播;<br>对于Outbound事件,从尾部往前传播;<br><br>Inbound为被动事件<br>OutBound为主动事件
NioEventLoop
伴随NioEventLoopGroup创建<br>
EventExecutor[] children
chooser
可以认为,Group是一个拥有固定核心线程数的一个线程池<br><br>NioEventLoop为内部的一个核心线程<br><br>一个Group下的NioEventLoop共用一个ThreadPerTaskExecutor进行线程创建<br><br>NioEventLoop执行任务时,会委托内部的Executor执行,此时会创建一个线程,开启无限循环<br><br>NioEventLoop内部会包含这个创建好的线程,用来判断提交任务的线程是否为EventLoop线程
启动
NioEventLoop.execute<br>
第一次启动时此时内部保存的线程为null,启动NioEventLoop
doStartThread()
executor.execute<br><br>由ThreadPerTaskExecutor分配一个线程执行启动逻辑<br>
this.thread = Thread.currentThread();<br><br>NioEventLoop内部保存这个创建好的线程
SingleThreadEventExecutor.this.run()<br><br>-> NioEventLoop.run()<br><br>正式启动NioEventLoop,即无限循环<br>
执行逻辑
select
阻塞式Select,当有穿插任务时,会被唤醒
解决空轮训Bug<br>
本应该阻塞实际未发生阻塞<br><br>并且select次数超过512次<br><br>触发rebuildSelector,将旧的selector上注册的channel迁移至新的selector上
processSelectedKeys
处理IO事件
runAllTasks
默认处理IO事件与任务的事件比为1比1<br><br>即任务并不一定能够一次处理完毕
循环获取定时任务中应该执行的任务,将其放入taskQueue<br>
在deadline之前不断取出task执行<br>
afterRunningAllTasks<br>
当执行过任务时,执行尾部任务
新连接接入
unsafe.read()
不断的accept连接<br>创建NioSocketChannel<br><br>-> 放入readBuf这个List中<br>
NioSocketChannel同样保存类似于NioServerSocketChannel的信息
循环调用pipeline.fireChannelRead(NioSocketChannel)<br>
从Head节点向后传播
ServerBootstrapAcceptor.channelRead<br><br>处理新连接
childGroup.register(child)<br><br>给Channel分配一个Reactor线程
unsafe().register
register0<br>
doRegister<br><br>将Netty Channel内部的JDK Channel与Reactor内部的Selector绑定<br><br>注意:此时未监听任何事件
pipeline.invokeHandlerAddedIfNeeded()<br><br>触发Child的ChannelInitializer的initChannel方法<br>
pipeline.fireChannelRegistered()
pipeline.fireChannelActive()<br><br>注意:<br>对于SocketChannel来说,accept后已经处于激活状态,所以此时直接触发active事件<br>Server的Channel需要bind才会处于激活状态
Active从Head节点开始传播<br><br>Head节点传播结束后,调用readIfIsAutoRead()
unsafe.beginRead()<br><br>设置感兴趣的事件为SelectionKey.OP_READ
pipeline.fireChannelReadComplete()<br>
IO
BIO
NIO
MappedByteBuffer用于内存映射文件
所能打开的最大连接数
FD巨增的情况
消息传递方式
AIO
对比
MySql
组成
连接器,管理连接,验证身份和权限
解析器,词法语法分析,生成解析树<br>
预处理器,检查,表,字段是否存在
优化器,SQL语句优化,生成执行计划,通过explain可以查看<br>
执行引擎,执行执行计划,返回数据<br>
常见存储引擎,提供读写接口
innodb
myisam
InnoDB
内存结构
Buffer Pool<br>包含Change buffer
按页读取放入缓冲池,页默认大小16K<br>预读机制,加载更多的页
修改数据写入缓冲池,此时数据不一致为脏页<br>由后台线程进行刷脏<br>
Log Buffer(redo log)
重做日志,属于物理日志,记录数据页上的操作<br>顺序写,当写满时触发buffer pool刷脏<br>用于崩溃恢复,如buffer pool尚未刷脏,即满足D(持久性)需求<br>
log buffer写入时机<br>
0 延迟写,log buffer每秒1次写入file并flush至磁盘<br>
1 默认,实时写,实时刷,每次事务提交写入file并flush至磁盘<br>
2 实时写,延时刷,实时写入file,每隔1秒flush<br>
undo log
回滚日志,属于逻辑日志,记录事务发生前的状态<br>记录反向操作,insert记录delete,update记录原来的值<br><br>为满足A(原子性)需求
磁盘结构
系统表空间
数据字典,undo logs<br>
双写缓冲区<br>由于操作系统page大小为4K,innoDB默认为16K<br>需4次写入,可能导致部分写失效<br>故写入磁盘page时先顺序写一份page的副本<br>
独占表空间
ibd,存放表的索引和数据<br>
共享表空间,临时表空间
redo log
bin log<br>由Server层提供<br>
属于逻辑日志,记录了所有的操作,DDL,DML<br>用于主从复制和数据恢复<br>文件内容可追加,没有大小限制
更新语句执行
客户端发送更新语句至Server层
执行引擎执行更新语句后<br>调用存储引擎接口进行更新后数据的存储<br>
InnoDB将修改结果写入内存中
InnoDB记录undo log和redo log,此时redo log状态设为prepare<br>
返回执行器,代表此时可以发生提交
执行器记录bin log日志<br>
执行器调用接口提交事务
InnoDB将redo log设为commit状态
语句执行完毕
索引
数据结构<br>
多路平衡查找树(Balanced Tree)<br>
解决了平衡二叉树节点对Page页利用不足的情况<br><br>InnoDB按页读取数据<br>所以通过B Tree在一个节点(16K)上存储足够多的索引数据<br><br>B Tree其实就是平衡多叉树,节点空间利用足够多,减少磁盘IO<br><br>频繁更新的列上建索引会导致树不断的分裂和合并<br>
B+树<br>
节点内关键字和路数相等<br>关键字存储的磁盘位置为关键字到下一个关键字的左闭右开区间<br>磁盘位置引用:[关键字,下一个关键字)
根节点和枝节点不存储数据,只有叶子节点存储数据<br><br>叶子节点之间构成双向链表
优势:非叶子节点不再存储数据,存储更多关键字,树深更低,减少磁盘IO<br>范围查找优势,通过底层双向链表遍历<br>IO性能稳定,都需要从子节点获取数据
存储引擎存储方式
myisam
数据存在.MYD
索引存在.MYI<br><br>所有的索引叶子节点都为数据的物理磁盘地址,从MYD查找数据
innodb
聚集(簇)索引 - 索引顺序与物理磁盘顺序一致<br>
若有主键索引,则选择主键索引作为聚集索引,叶子节点存放数据<br><br>其他索引为二级索引,叶子节点存放主键值,需要回表<br>
二级索引查询,首先找到主键值,在主键索引查询<br><br>叶子节点未存放物理地址,因为物理地址可能发生变化
若没有主键索引,则选择第一个不包含null的唯一索引作为主键索引,选其为聚集索引<br>
若没有主键索引也没有非空唯一索引<br>则innoDB生成一个默认6字节长的_rowid作为隐藏的聚集索引,随着插入单调递增<br>
索引使用原则
离散度
联合索引最左匹配原则
覆盖索引<br>
当数据列所需的字段在二级索引中就能获得,则不需要回表<br><br>这种使用索引的方式称为覆盖索引<br><br>注意:当查询未使用索引时,但是优化器发现使用索引树查询列数据更快时<br>仍然会发生覆盖索引<br><br>extra:'Using index' 覆盖索引<br>
索引条件下推(ICP)<br>
针对二级索引,未使用索引仍由索引过滤数据称为索引条件下推,默认开启<br>如 %xxx 未使用索引(未根据索引匹配数据,但有可能通过索引树查数据,根据列字段判断)<br>发生条件下推时由二级索引过滤数据然后回表,返回Server层的数据在某种情况下可以骤减<br>相当于在存储引擎层完成了数据过滤<br><br>extra:Using index condition 发生索引条件下推,即由索引过滤数据<br>extra:Using where 返回的数据不满足全部条件,由Server层过滤<br>
全表扫描索引<br>
根据列字段全表扫描发生在索引树的情况,我们需要关注rows字段<br>这种情况为使用了索引但是全表扫描了索引<br>
常用字段说明
type
数据匹配类型
possible_keys
可能用到的索引
key
实际使用到的索引
rows
扫描行数<br>
extra
'Using index' 覆盖索引,无需回表,直接使用索引树
Using index condition 发生索引条件下推,即由索引过滤数据
Using where 返回的数据不满足全部条件,由Server层过滤
事务
特性
atomicity<br>
操作共同失败成功<br><br>undolog保证<br>
consistency<br>
分为数据库一致性和用户自定义一致性<br><br>数据库一致性:由其他三个属性保证,保证数据的合法状态<br><br>用户自定义一致性:<br>能量守恒,数据不会凭空多/少,或非法如余额<0的状态(通常在代码中控制)<br>
isolation
并发干扰<br>注意:事务的原子性和并发编程中原子性的区别<br>
<span style="color: rgb(64, 64, 64); font-family: -apple-system, BlinkMacSystemFont, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 16px;">durability</span>
只要事务提交,结果就为永久性的<br><br>redolog和双写缓冲保证<br>
并发的三大问题<br><br>即读一致性问题
脏读:一个事务读到另一个事物未提交的数据<br><br>此时第二个事务回滚时会导致数据不一致<br>
不可重复读:一个事务内读到另一个事务提交的修改后的数据<br>
幻读:一个事务内的范围查询读到另一个事务提交插入的数据<br><br>注意:幻读特指新插入的行
读不一致:未发生更新数据时事务内两次读到的数据不一致
解决方案,由数据库的隔离性解决
读一致性解决方案
LBCC
加锁,不允许并发读写,即一个事务读时不允许其他事务写
MVCC
基于数据版本
能看到:<br>第一次查询前已提交的事务的修改<br>本事务的修改<br><br>不能看到:<br>比当前事务id大的事务,即当前事务后创建的事务<br>未提交的事务
隐藏字段
DB_TRX_ID
当前事务ID,创建版本号
DB_ROLL_PTR<br>
回滚指针,删除版本号
Read View<br>
m_ids当前系统活跃(未提交)事务ids<br>
min_trx_id 最小m_ids<br>
max_trx_id 系统分配的下一事务id<br>
creator_trx_id 生成RV的事务id<br>
可见性算法
trxid = c-trx-id,当前事务修改,可见
id <min_id,生成RV时已提交的数据,可见<br>
id>max_id,生成RV后提交的数据,不可见<br>
min<id<max,在活跃id中则不可见,否则可见<br>
由最新的版本往前查找,若可见则停止
RR第一次快照读时建立RV<br><br>RC每次快照读时均建立RV<br>
锁
共享锁/读锁
排它锁/写锁
增删改会自动加互斥锁<br>
锁的释放
当事务提交时释放锁
意向共享/排他锁
用于表示有数据行已发生加锁行为<br><br>便于加表锁时的判断,不需要扫描行是否加锁<br><br>用于提交加锁效率
行锁原理<br>
对于主键索引和二级索引来说,锁的是主键索引<br><br>对于全表扫描来说,锁的是整个表
Record记录锁
根据索引精确匹配时只会锁住一行
Gap间隙锁
等值/范围查询未命中数据行时,使用间隙锁
间隙锁用于阻塞insert,相同的间隙锁之间不冲突<br>
锁区间
Next-Key临键锁
范围查询包含记录和区间时<br>间隙锁+记录锁:(x, y]<br>
对于间隙,阻塞插入<br>对于记录,阻塞获取锁,即无法修改或带锁查询<br>
锁住当前区间和下一个左开右闭区间<br><br>阻塞了insert操作,即不会出现幻读
innoDB隔离级别的实现
RU 不加锁<br>
序列化 Select隐式加共享锁,与修改互斥<br>
RR<br>
普通Select使用快照读,第一次生成RV,基于MVCC实现
加锁Select和更新使用当前读,底层使用记录锁,间隙锁,邻键锁<br>
RC
普通Select使用快照读,每次生成RV
只使用记录锁,没有Gap锁<br><br>并不会阻塞insert操作,无法解决幻读问题,修改时的受影响行数和看到的不一致<br>
RR和RC区别
RR间隙锁导致锁定范围增大<br>
条件列未使用索引时,RR锁表,RC锁行<br><br>测试结果:<br><br>在RC下同样使用非索引字段会互斥,产生锁表效果,但一方使用非索引字段,一方使用索引字段,不会互斥(若同一行数据仍然互斥)<br>表现结果为:锁非索引字段和锁行记录,不会阻塞索引字段上的非当前行加锁,使用索引字段加锁会导致非索引字段全表锁定<br><br>RR任何时候未匹配索引,会导致锁表,即使一方使用非索引字段,一方使用索引字段<br>
RC半一致性读增加update效率
优化
连接<br>
增加可用连接数
及时释放不活动的连接,防止客户端假死导致连接未释放<br>
客户端层面对连接复用,使用连接池<br><br>设置合适的连接池大小
缓存
报表数据缓存
数据缓存,减少数据库复杂查询
主从,读写分离<br>
分库分表
慢查询优化<br>
开启慢查询,设置慢查询参数
explain分析<br>
id不同时按id顺序逆序分析,id最大为最里的子查询<br>
id相同为关联查询,正序分析<br><br>小表驱动大表,先执行查询结果少的表<br>
select type
SIMPLE<br><br>简单查询,不包含子查询<br>
PRIMARY 主查询,外层查询<br><br>SUBQUERY 子查询<br><br>DERIVED 衍生查询,用到临时表<br><br>UNION 联合查询<br>
type
index<br>
full index scan<br><br>查询全部索引中的数据,特殊的全表扫描<br>
all
full table scan<br><br>全表扫描
possible keys 可能用到的索引<br>key 实际用到的索引<br>
key_len 索引长度<br>
rows 预估扫描行数<br>
filters
存储引擎返回的有效数据比例<br><br>当比例很低时,代表返回了过多的数据需要在Server层过滤
extra<br>
using index 覆盖索引无需回表
using where 需要Server层过滤数据
using index condition 索引条件下推<br>
using filesort 无法使用索引排序,使用了额外的排序,需要优化<br>
using temporary 使用了临时表<br>
字段定义<br>
恰当的整数类型
固定长度使用char,varchar需要一个字节记录长度<br>
非空,使用特殊值代替null<br>
不要使用外键,触发器,存储过程,视图<br>
大文件存储至文件服务器,数据库只存url
表拆分或字段冗余
利用事务的超卖解决方案<br>
假设两个并发事务共同开启<br>两个事务共同查询发现余额为1,可消费<br>假设第二个事务此时发生更新,那么由于锁定机制第一个事务的update阻塞<br>第二个事务更新后发现库存减少到0,那么提交事务<br>第一个事务在第二个事务提交后完成更新操作,之后继续读操作,发现库存减少到-1,回滚
Redis
数据类型<br>
String
存储形式
redisObject是对五大数据类型的抽象<br><br>type为对外类型,encoding为编码类型<br><br>指向实际的存储对象,此时为string<br>
SDS<br>Simple Dynamic String<br>
C语言本身没有字符串<br><br>通过字符数组存储字符,需要提前预先分配足够的空间<br><br>本身是个字符数组,获取长度需要遍历<br><br>C函数库操作字符串的方式通过\0结尾,存储一些二进制文件可能不安全<br>
SDS是一个结构体,可以近似认为一个对象<br><br>提供了自动扩容,记录字符串长度的能力<br><br>通过预分配和惰性释放防止每次重新分配内存<br><br>通过len长度属性判断字符串结尾,二进制安全
编码类型
int 存储8字节长整型,当长度超出时转为embstr<br>
embstr 存储小于44字节的字符串<br>设置时大于44字节时转为raw<br>或每次修改时转为raw<br>
一次连续内存分配,分配RedisObject和SDS<br><br>释放只需要一次释放
只读,每次修改时转为raw
raw 存储大于44字节的字符串
两次内存分配,分别分配<br>
使用场景
缓存热点,报表数据
分布式session
基于setnx的分布式锁<br><br>注意:并不推介使用这种方式,无法实现可靠稳定的锁<br>推介使用lua脚本的方式实现<br>
全局ID生成器 incrby<br>
计数器 incr<br>限流 如密码输错N次不允许再次输入<br>
Hash / HSet
String类型组成的Map
编码类型<br>
ziplist
特殊的双向链表,本质为一个数组<br><br>不存储指向pre和post的指针<br><br>取代存储上个节点长度和当前节点长度<br><br>时间换空间<br>
当field < 512<br>且所有k-v字符串长度和 < 64字节<br><br>即字段个数小且字段值小的情况下使用ziplist<br>
hashtable
数组+链表<br><br>定义了两个hash表,用于扩容使用<br>类似的有Netty使用的2个hash表<br>chm内部也会多创建一个hash表
使用场景
存储对象,如JSON对象本质就是一个map
String能做的都可以做
购物车
key 用户ID<br>
field 商品id
value 商品数量
hincr/hdecr 增加减少购买数量<br>
hgetall 全选
hlen 商品数量
<span style="color: rgb(51, 51, 51); font-family: "Open Sans", Helvetica, sans-serif; font-size: 16px;">List</span><br>
链表,按照String元素插入排序<br>
编码类型:quicklist
双向链表+数组<br><br>quicklist中的每个quickNode存储一个ziplist
使用场景
存储有序内容
分布式阻塞队列/分布式队列
分布式锁排队
Set
String类型组成的无序集合<br>本质是个隐藏value的HashMap
编码类型
intset
当元素全为整形时
hashtable
不是整形/元素个数超出512<br>
使用场景
spop 抽奖,随机获取元素
sadd 点赞<br>srem 取消点赞<br>sismember 是否已经点赞<br>smembers 获取所有点赞用户<br>scard 点赞数
sadd 商品标签<br>
sdiff 差集 可能认识的人<br>sinter 交集 共同关注<br>sunion 并集<br>
ZSet / Sorted set<br>
String类型元素组成的有序集合<br><br>每个元素有一个score,由小到大排序
编码类型
ziplist
按照score递增排序,插入时需要移动之后的数据<br>
skiplist
元素个数大于等于128个<br>或任一member字符串长度大于等于64字节<br>
通过设置不同的节点高度,来减少查找数量<br><br>首先从最高的节点找,当发现查找节点小于某个节点是回退去下一层节点找
使用场景
实现任务权重队列
排行榜
点击数
获取今天点击量最高的数据<br>
BitMap
位图
HyperLogLog
不精确的数据统计,内存占用非常小
Geo<br>
二维记录<br>
Stream
类似于kafka的消息队列
其他使用场景
基于发布订阅
服务注册与发现
配置中心
分布式锁的唤醒
持久化机制
RDB<br><br>redis database<br>
定时触发
shutdown触发
save 当前保存,阻塞redis处理线程<br>bgsave 后台保存,fork子进程处理数据生成rdb<br>由于OS的copy-on-write机制,两个进程使用同一份数据<br>当父进程修改数据时会重新创建一份数据,所以子进程相当于拿了一个快照数据
redis down掉会丢失最后一次快照之后的数据<br>
主从复制时触发
AOF<br><br>append only file
通过日志的顺序写记录每个写操作,先写入磁盘缓存<br>
持久化策略<br><br>磁盘缓存
从不fsync,由操作系统刷盘<br>
每次写入刷盘
每秒刷盘 默认
通过重写机制防止文件过大<br><br>读取现有键值对,保留恢复当前数据的最小指令集
百分比参数 100<br><br>当文件大小为上次重写后AOF文件的2倍进行重写<br>
最小文件参数 64M<br>
手动触发:BGREWRITEAOF
比较
启动优先级:AOF > RDB<br>
体积: AOF > RDB
恢复速度:RDB更快
数据安全:RDB丢失最后一次快照后的数据,AOF由策略决定
RDB每次进行全量同步,影响磁盘IO性能<br>
常见问题
数据一致性
实时性不强的情况下,可以使用定时任务/过期时间令缓存数据失效<br><br>从而达到最终一致性
更新缓存容易导致数据不一致,所以一般使用删除缓存
更新数据库,删除缓存,通过重试令这次删除成功
先删除缓存,后更新数据库<br>此时需要对数据库访问线程加锁,防止更新数据库时有别的线程重置了缓存<br><br>即当发现需要更新数据时,加锁,删缓存,更新数据库<br>
缓存三大问题
缓存雪崩<br>
加锁或使用队列,对同一个key只有一条数据访问数据库<br>
定时预更新
添加随机数防止同时失效<br>
永不过期
缓存穿透
缓存空数据或特殊字符,设置较短的过期时间<br>
当恶意请求,每次使用不同的ID访问,上面这种方式失效<br><br>使用布隆过滤器,针对假阳性的数据进行特殊缓存
缓存击穿
特殊的缓存雪崩,对操作数据库的线程加锁即可
海量数据查询某一固定前缀的key
scan模糊查询代替keys指令
pipeline
批量发送命令
主从同步原理
全同步<br>
slave发送sync命令至master<br><br>master启动后台进程,保存RDB文件<br><br>master缓存接收到的后续写命令<br><br>master完成写文件后将文件发送给slave<br><br>使用新的rdb替换旧的rdb,进行数据恢复<br><br>master将增量写命令发送至slave进行回放<br>
增量同步<br>
master判断接受到的命令是否为写命令,即是否需要同步<br><br>将操作记录追加至AOF中<br><br>发送数据至slave
一致性hash
hash环倾斜,导致数据分布不均匀
虚拟节点
通过slot的分配方式
JVM
ClassLoader
默认委派关系
Bootstrap ClassLoader<br>-> Extendsion ClassLoader<br>-> Application ClassLoader<br>-> Customer ClassLoader<br>
loadClass(String name)
this.findLoadedClass(name)<br>-> parent.loadClass(name, false)<br>-> this.findClass(name)(此时一般会调用defineClass根据字节码生成Class对象)<br>-> resolveClass(c) (是否link,默认false)
Class.forName(String)<br>-> forName0(className, true, ClassLoader.getClassLoader(caller), caller)<br><br>注意:这里的ture,代表初始化
初始化时才会执行静态代码块
Verify 是用来校验加载进来的class文件是否符合class文件标准,如果不符合直接就会被拒绝了;
Prepare 是将class文件静态变量赋默认值而不是初始值,例如static int i =8;这个步骤并不是将i赋值为8,而是赋值为默认值0;
Resolve 是把class文件常量池中用到的符号引用转换成直接内存地址,可以访问到的内容;<br>
内存结构
运行时数据区
线程共享区
方法区
已被虚拟机加载的类信息<br><br>常量池<br><br>静态变量<br><br>即时编译后的代码
java8之前处于永久代
java8之后处于元空间,堆外内存
堆
存储实例对象
线程独占区
虚拟机栈<br>
存放方法运行时所需的数据,成为栈帧<br><br>栈帧 = 局部变量表+操作数栈<br><br>局部变量表包含本地变量
注意:逃逸分析作用域在方法内的小对象会进行栈上分配,<br>并进行标量替换(将对象字段作为局部变量分配)<br>
本地方法栈
服务native方法
程序计数器
记录当前线程所执行到的字节码行号
执行引擎
jit即时编译器
将热点代码编译为本地平台相关的机器码
gc垃圾回收器
用于申请和回收垃圾
本地库接口
本地方法区
内存划分
非堆
堆
old<br>
young<br>
eden
s0
s1<br>
引用类型
强引用
SoftReference
WeakReference
PhantomReference
弱引用和虚引用都不会影响对象的回收
虚引用用在直接内存的回收<br>内存映射文件,当对象回收时,收到通知并清理直接内存<br>
对象生命周期
GC
回收时机
eden/survivor空间不足<br>
老年代空间不足
方法区空间不足
System.gc()通知
垃圾回收算法
复制(Copy)
将内存划为相等的两块区域,每次只使用其中一块<br>
直接将存活的对象复制到另一块区域,将使用过的内存区域一次清除<br>
空间利用率低<br>适合存活率低的场景,copy数量少,效率高<br>分配内存只需要直接移动堆顶指针,按序分配<br>若不按1比1分配空间,需要额外空间担保
标记-清除(Mark-Sweep)
标记:标记存活对象
清除:清除未标记的对象,释放出对应的内存空间
产生大量内存碎片<br>
标记-整理(Mark-Compact)
标记所有存活对象
让所有存活对象向一端移动,清理掉边界外的内存<br>
分代收集
Young区<br>
使用复制算法,大部分对象朝生夕死,由Old区作分配担保
Old区<br>
使用标记清除或标记整理算法,old区对象存活时间长,复制算法效率低下<br>
标记算法
引用计数
GC Root链路
双色标记<br>
三色标记
垃圾收集器
垃圾回收算法的具体实现
Serial
复制算法,单线程收集,新生代
Serial Old
标记-整理算法,单线程收集,老年代<br>
ParNew
Serial的多线程版本,新生代
Parallel Scavenge<br>
类似ParNew,可以控制吞吐量和停顿时间<br>-XX:MaxGCPauseMillis控制最大的垃圾收集停顿时间<br> -XX:GCTimeRatio直接设置吞吐量的大小。<br>
Parallel Old
Parallel Scavenge的老年代版本,标记-整理算法
CMS<br>Concurrent Mark Sweep<br>
三色标记,减少STW时间<br>由于并发清除,使用了标记清除算法<br>concurrent mode failure导致其退化为serial old<br>老年代空间不足,只能暂停用户线程进行清理<br>
初始标记,GC ROOT直接可达对象
并发标记,由这些灰色节点出发标记
重新标记,对突变导致变回灰色的节点重新标记
并发清理
清除不可达对象,注意finalize方法
G1<br>Garbage First
内存布局基于Region,新老不再物理隔离,分代收集<br><br>H:当对象大小等于Region一半时认为大对象,放入这个区<br><br>region之间使用复制算法,分配内存通过指针碰撞<br>
G1使用了三色标记算法,标记整理算法
-XX: G1HeapReginSize 设置Region大小<br>-XX:MaxGCPauseMillis 最大停顿时间<br>-XX:ParallelGCThread 并行GC工作的线程数<br>-XX:ConcGCThreads 并发标记的线程数<br>-XX:InitiatingHeapOcccupancyPercent<br>默认45%,代表GC堆占用达到多少的时候开始垃圾收集<br>
术语<br>
Stop The World
停顿时间,垃圾收集器收集过程中用户线程暂停时间<br>
吞吐量
运行用户代码时间 / (运行用户代码+垃圾收集时间)<br>
GC Root
方法区中的常量,静态变量<br><br>栈中局部变量表的引用变量<br><br>本地方法栈的引用变量<br><br>被同步锁持有的对象
SafePoint
用户线程只会运行至安全点暂停
常用命令
jps
查看java进程
jinfo
实时查看修改JVM参数<br><br>manageable的flag才能实时修改<br>
jstat
查看类装载信息
查看垃圾收集信息<br>
jstack
查看线程堆栈信息,可以发现程序死锁
通过jstack解决CPU100的情况
jmap
生成Dump文件<br><br>-XX:+HeapDumpOnOutOfMemoryError <br>-XX:HeapDumpPath="D:/LYT/gc dump/dump.hprof"<br>指定参数,发送内存溢出是自动生成dump文件<br>
jmap -heap PID 查看堆内存使用情况
jconsole
可视化查看,还可以看mbeans
jvisualvm
监控本地/远程java进程
并发编程/异步编程
volatile
可见性
有序性<br>
双重检查锁创建单例的安全问题
memory = allocate()<br>分配对象内存空间<br>
initialize(memory)<br>初始化对象<br>
instance = memory<br>设置instance指向刚分配好的内存空间<br>此时instance != null<br>
原理
高速缓存架构
CPU多级缓存 - 解决CPU与主存之间读写速度不匹配的问题
MESI协议
Modify
数据被修改,只存于当前缓存行中,与主存数据不一致<br><br>缓存行必须时刻监听所有试图读该缓存行相对应的内存的操作<br>其他缓存须在本缓存行写回内存并将状态置为E之后才能操作该缓存行对应的内存数据<br>
疑问:若变为E才读取内存数据<br>那While循环应该就是最终一致性的,但测试while循环无法停止
Exclusive
数据只存在于当前缓存行中,与内存数据一致<br><br>缓存行必须监听其他缓存读主内存中该缓存行相对应的内存的操作<br>一旦有这种操作,该缓存行需要变成S状态<br>
Shared
数据被多个CPU缓存,且各个缓存中的数据与主存一致<br><br>监听缓存行的修改,将当前缓存行置为无效状态<br>
<div><span style="mso-spacerun:'yes';font-size:9.75453pt;font-family:OpenSans;color:rgb(51,51,51);">Invalid</span></div>
表示当前缓存行已失效<br>
总线嗅探机制
前提:所有内存的传输都发生在一条共享的总线上<br>
目标:CPU缓存不停嗅探总线上发生的数据交换,跟踪其他CPU缓存做了什么<br>当发现数据变更时,且当前CPU缓存存在这份数据,则令其失效
MESI优化
Store Buffer<br>
CPU写入Store Buffer,由SB发送通知接收ACK,然后写入缓存<br>此时CPU不再需要等待invalid的ack<br>执行下一个指令,导致执行重排序<br>
invalid queue
用于失效CPU缓存行
内存屏障
读屏障 - 执行invalid queue,后续从主存读取数据<br>
写屏障 - 执行Store Buffer,将数据直接写入主存<br>并且解决了指令重排,重排序其实优化了缓存行的使用<br>
0x0000000002931351: lock add dword ptr [rsp],0h ;*putstatic instance<br><br>x86 在写入static变量时会设置store load屏障,此时会使用lock汇编指令<br>x86 只有FIFO的写缓存,没有无效缓存,所以只响应store load就可以了<br>x86 唯一可能出现乱序的就是Store Load,因为写是异步发生
volatile根据不同操作系统和CPU架构生成不同的内存屏障指令<br>
总结
Thread-A
Thread-A发出LOCK#指令加锁
发出的LOCK#指令锁总线(或锁缓存行)<br>1) 令Thread-B高速缓存中的缓存行内容失效<br>2) Thread-A向主存回写最新修改的值<br>
锁释放
Thread-B
发现对应地址的缓存行被锁了,等待锁的释放,缓存一致性协议会保证它读取到最新的值<br>
JMM
提供了可见性和有序性的解决方案
重排序:<br>源代码 -> 编译器优化重排序 -> 指令重排序 -> 内存系统重排序 -> 最终执行的指令序列
可见性:重排序导致了可见性问题
happens before
程序顺序规则<br><br>注意:与as-if-serial的区别
对于单个线程中的每个操作,前继操作happens-before于该线程中的任意后续操作
监视器锁规则
对一个锁的解锁,happens-before于随后对这个锁的加锁
volatile变量规则<br>
volatile变量的写操作,happens-before这个变量的读操作<br>
传递性
如果A happens-before B,且B happens-before C,那么A happens-before C
线程启动原则<br>
在主线程A执行过程中,启动子线程B,那么线程A在启动子线程B之前对共享变量的修改结果对线程B可见<br>
线程中断原则<br>
A调用B的interrupt,A对中断标识的修改对B可见<br>
线程终止规则
线程中所有的操作都happens-before线程的终止检测,如Thread.join() Thread.isAlive()<br>
对象终结规则<br>
一个对象的初始化完成happens-before它的finalize()方法开始<br>
finally原则
内存屏障保证
简单的说就是我们只需要满足这个原则,而不用关心每个原则的底层实现是什么,如不用了解volatile的实现原理<br>通过内存屏障保证可见性
synchronized
满足原子性和可见性(happens before原则之一)
对象锁,类锁(锁的是类的Class对象)
实现原理
Mark Word<br>
组成部分,重量级锁需要使用ObjectMonitor,类似于AQS
Monitor<br>
指令
monitorenter - 获取锁<br>相当于调用lock方法
monitorexit - 释放锁<br>相当于调用unlock方法
锁升级<br>
偏向锁<br>
CAS获取锁的线程再次获取锁时<br>只需检查锁标志位为偏向锁<br>并比较当前线程ID是否等于头部记录的线程ID<br>
轻量级锁
偏向锁竞争才会释放,偏向锁撤销需等待全局安全点,暂停偏向锁线程<br>若此时持锁线程还活着,则升级为轻量级锁
加锁
栈帧生成锁记录LockRecord,存储一份MarkWord的拷贝<br><br>尝试通过CAS操作将Mark Word指向LockRecord<br>并将Lock Record里的owner指针指向Mark Word<br><br>更新失败则判断Mark Word是否已经指向栈帧<br>若指向,则代表已经获取了轻量级锁<br>
解锁
若Mark Word仍指向栈帧,通过CAS替换Mark Word,此时可能替换失败<br><br>未指向和替换失败代表此时锁已经升级为重量级锁<br>需要唤醒锁阻塞队列的下一个等待线程<br>
重量级锁
当多次获取轻量级锁失败时,升级为重量级锁<br>进入Monitor的锁阻塞队列,等待持锁节点唤醒
优化
锁消除<br>
锁粗化
早期依赖于操作系统实现<br>线程间切换需要由用户态转为核心态
ThreadPoolExecutor
状态转换
<span style="color: rgb(51, 51, 51); font-family: "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 14px;">RUNNING:接受新任务并且处理阻塞队列里的任务;</span>
<span style="color: rgb(51, 51, 51); font-family: "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 14px;">SHUTDOWN:拒绝新任务但是处理阻塞队列里的任务;</span>
<span style="color: rgb(51, 51, 51); font-family: "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 14px;">STOP:拒绝新任务并且抛弃阻塞队列里的任务,同时会中断正在处理的任务;</span>
TIDYING:所有任务都执行完(包含阻塞队列里面任务)当前线程池活动线程为 0,将要调用 terminated 方法;
TERMINATED:终止状态,terminated方法调用完成以后的状态。
源码解析
参数
ctl<br>用来标记线程池状态(高3位),线程个数(低29位)<br>默认是RUNNING状态,线程个数为0<br>
https://www.cnblogs.com/huangjuncong/p/10031525.html
execute
添加任务执行
addWorker
添加工作线程
执行流程
线程池使用ThreadLocal导致内存溢出<br>不去调用remove,导致所有的线程一直都会持有对象
ThreadLocal
原理图
内存泄露问题
线程池使用ThreadLocal导致内存溢出<br>不去调用remove,导致所有的线程一直都会持有对象
CHM
HashMap
参数
链表长度除去头结点大于8转为红黑树<br><br>小于6退化<br><br>转红黑树的另一个前提元素个数大于64<br>当不满足该条件时只会发生扩容
流程
延迟初始化:put -> resize(初始化/扩容)<br>-> p = tab[i = (n - 1) & hash](算出元素下标)<br>
优化
使元素分布更均匀:高低位异或<br>(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)<br>
扩容
jdk8 -> 高低位尾插法,不会无限循环
JDK1.7,在扩容时由于顺序颠倒,可能导致环形链表的产生,导致无限循环
CHM
参数
sizeCtl:<br>负数代表扩容或初始化<br>-1 代表初始化<br>-n 代表有n-1个线程进行扩容操作<br>0代表还未被初始化<br>初始化后这个值代表下一次初始化的大小
流程
优化
CAS + synchronized<br>
区间划分,协助扩容
addCount<br><br>CounterCell<br>
1.7 分段锁,二次Hash
AQS
独占
void acquire(int arg)<br>void acquireInterruptibly(int arg)<br>boolean release(int arg)<br>
tryAcquire一般实现:<br>CAS修改state状态<br>设置当前持锁线程<br>下次获取是重入<br><br>acquire:<br>当已被其他线程持有<br>独占模式进入阻塞队列并等待前一个节点唤醒<br>唤醒后设置当前节点为head
tryRelease一般实现:<br>减少重入次数,当减少为0时,清空当前持锁线程<br><br>release:<br>尝试释放锁成功则唤醒下个节点
共享
void acquireShared(int arg)<br>void acquireSharedInterruptibly(int arg)<br>boolean releaseShared(int arg)<br>
tryAcquireShared一般实现:<br>自选通过CAS减少State值<br><br>acquireShared:<br>获取失败<br>共享模式进入锁阻塞队列并等待前一个节点唤醒<br>唤醒后设置当前节点为head<br>并进行传递,唤醒下个共享节点<br>
tryReleaseShared一般实现:<br>CAS增加state个数<br><br>releaseShared:<br>尝试成功唤醒下个节点
ConditionObject
单向链表存储条件队列<br>await 释放锁进入条件队列<br>signal 将条件队列的头部节点转移至AQS队列并唤醒让其尝试获取锁
Lock
ReentrantLock
ReentrantReadWriteLock
高16位:共享锁状态<br>低16位:互斥锁状态
CopyOnWriteArrayList
写时拷贝,读快照
其他
CountDownLatch 闭锁
CyclicBarrier 栅栏<br>
Semaphore 信号量
Exchanger 交换器<br>
BlockingQueue
实现
设计模式
// TODO
Zookeeper
集群模式启动<br>https://www.processon.com/view/link/60603dfb1e0853028ab273a5<br>
IO通信模型<br>https://www.processon.com/view/link/6061913c7d9c08555e684073<br>
Leader选举和客户端请求处理<br>https://www.processon.com/view/link/6062e663079129148ce4a71f<br>
Watch机制的存储与转发<br>https://www.processon.com/view/link/607d37597d9c08283dd3b312<br>
Kafka/Cloud stream
Kafka消息传递<br>https://www.processon.com/view/link/607d37a4079129368891ca82<br>
Cloud Stream的启动与坑<br>https://www.processon.com/view/link/607077a25653bb4d47999d0d<br>
问题记录
beanFactory.registerBeanDefinition<br><br>beanFactory.registerSingleton<br><br>注册顺序问题
注册Bean定义和注册单例Bean时的顺序问题
若先注册单例,会被保存至手工单例容器中<br><br>此后注册Bean定义,会将手工单例容器中的Bean移至单例Bean容器中<br>最后会重置Bena定义,将单例Bean容器中的Bean Remove掉<br><br>此时相当于由容器管理我们Bean的创建,而手动注入的方式是为了自己管理Bean的创建
SpringCloudStream<br>Binder启动时消息队列尚未启动会导致启动线程阻塞,即Tomcat无法监听端口<br>
@Async无法解决循环依赖
对于prototype类型的Bean,其销毁方法并不会由Spring管理
出现循环依赖时,不允许在Bean初始化时替换对象,否则各对象之间状态不一致<br>只能在三级缓存放入二级缓存的那一刻替换代理对象
0 条评论
下一页