【随时更新】比较杂乱的笔记


Spring AOP

是啥

AOP的编程思想就是把业务逻辑和横切的问题进行分离,从而达到解耦的目的,使代码的重用性和开发效率高(目的是重用代码,把公共的代码抽取出来)。

应用场景

日志记录、权限验证、效率检查、事务管理

Spring 的声明式事务管理在底层是建立在 AOP 的基础之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。

底层

JDK动态代理、CGLIB代理

运行期织入,生成字节码,再加载到虚拟机中,JDK是利用反射原理,CGLIB使用了ASM原理。初始化的时候,已经将目标对象进行代理,放入到spring 容器中。如果实现了接口的类,是使用jdk。如果没实现接口,就使用cglib。

ASM从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。

AOP中的切面、切点、连接点、通知,四者的关系

切面(Aspect):切面是由切点和增强组成的

Point cut 表示连接点的集合(类似一个表)

Join point 目标对象中的方法(每一条记录)

增强(Advice):增强是织入到目标类连接点上的一段程序代码。

weaving 把代理逻辑加入到目标对象上的过程叫做织入

Java类加载顺序

类中,加载顺序为:

1.首先加载父类的静态字段或者静态语句块

2.子类的静态字段或静态语句块

3.父类普通变量以及语句块

4.父类构造方法被加载

5.子类变量或者语句块被加载

6.子类构造方法被加载

分布式锁

表主键唯一、redis setnx、ZooKeeper主要服务于分布式系统,可以用ZooKeeper来做:统一配置管理、统一命名服务、分布式锁、集群管理。

TCP拥塞控制

image-20211129160705888

ThreadLocal

思想就是:给每一个使用到这个资源的线程都克隆一份,实现了不同线程使用不同的资源,且该资源之间相互独立

ThreadLocal有一个机制,每一次set和get都会检测是否有key为Null的实体存储在Map中,如果有就直接干掉了。如果忘记了remove,在当前线程的其他ThreadLocal使用的时候也是可以被干掉的。不过建议开发者自己控制,使用完就干掉。

ThreadLocal会发生内存泄漏吗?

会。

所以在调用get()、set(T)、remove()等方法的时候,会自动清理key为null的Entity。

不错的文章:https://zhuanlan.zhihu.com/p/158684233

image-20211129160044993

数据库读写分离

image-20211129151343061

分布式事务

Redis不支持回滚。Redis 官网解释了为什么不支持回滚,他们说首先如果命令出错那都是语法使用错误,是你们自己编程出错,而且这种情况应该在开发的时候就被检测出来,不应在生产环境中出现。

然后 Redis 就是为了快!不需要提供回滚。

严格意义上的事务实现应该是具备原子性、一致性、隔离性和持久性,简称 ACID。

  • 原子性(Atomicity),可以理解为一个事务内的所有操作要么都执行,要么都不执行。
  • 一致性(Consistency),可以理解为数据是满足完整性约束的,也就是不会存在中间状态的数据,比如你账上有400,我账上有100,你给我打200块,此时你账上的钱应该是200,我账上的钱应该是300,不会存在我账上钱加了,你账上钱没扣的中间状态
  • 隔离性(Isolation),指的是多个事务并发执行的时候不会互相干扰,即一个事务内部的数据对于其他事务来说是隔离的。
  • 持久性(Durability),指的是一个事务完成了之后数据就被永远保存下来,之后的其他操作或故障都不会对事务的结果产生影响。

二阶段提交

(Two-phase commit protocol)

假如在第一阶段所有参与者都返回准备成功,那么协调者则向所有参与者发送提交事务命令,然后等待所有事务都提交成功之后,返回事务执行成功。

image-20211129145913398

消息队列

1)支付宝在扣款事务提交之前,向实时消息服务请求发送消息,实时消息服务只记录消息数据,而不真正发送,只有消息发送成功后才会提交事务;

2)当支付宝扣款事务被提交成功后,向实时消息服务确认发送。只有在得到确认发送指令后,实时消息服务才真正发送该消息;

3)当支付宝扣款事务提交失败回滚后,向实时消息服务取消发送。在得到取消发送指令后,该消息将不会被发送;

4)对于那些未确认的消息或者取消的消息,需要有一个消息状态确认系统定时去支付宝系统查询这个消息的状态并进行更新。为什么需要这一步骤,举个例子:假设在第2步支付宝扣款事务被成功提交后,系统挂了,此时消息状态并未被更新为“确认发送”,从而导致消息不能被发送。

优点:消息数据独立存储,降低业务系统与消息系统间的耦合;

缺点:一次消息发送需要两次请求;业务处理服务需要实现消息状态回查接口。

避免重复消息?记录状态。

微服务

image-20211129141340458

一个电商系统,比如淘宝,我们在首页会展示很多数据信息,例如:首页信息、商品信息、个人信息、推送信息等等很多。如果首页展示的数据来自100个不同的应用/系统,那么通过如上架构,我们在后端便会出现几百个乃至上千个通信的交互,那么后端的结构就会变得非常的庞大和复杂。所以在这样的架构下,我们需要对上面结构作出一些调整 ,所以我们就引入了SOA架构。

SOA(全称:Service Oriented Architecture),中文意思为 “面向服务的架构”,你可以将它理解为一个架构模型或者一种设计方法,而并不是服务解决方案。其中包含多个服务, 服务之间通过相互依赖或者通过通信机制,来完成相互通信的,最终提供一系列的功能。一个服务通常以独立的形式存在与操作系统进程中。各个服务之间通过网络调用 。

跟 SOA 相提并论的还有一个 ESB(企业服务总线),简单来说ESB就是一根管道,用来连接各个服务节点。为了集成不同系统,不同协议的服务,ESB 可以简单理解为:它做了消息的转化解释和路由工作,让不同的服务互联互通。

image-20211129144502015

SOA 和微服务架构的差别

  1. 微服务去中心化,去掉ESB企业总线。微服务不再强调传统SOA架构里面比较重的ESB企业服务总线,同时SOA的思想进入到单个业务系统内部实现真正的组件化

  2. Docker容器技术的出现,为微服务提供了更便利的条件,比如更小的部署单元,每个服务可以通过类似Node或者Spring Boot等技术跑在自己的进程中

  3. SOA注重的是系统集成方面,而微服务关注的是完全分离

Hashmap扩容

Java8中扩容只需要满足一个条件:当前存放新值(注意不是替换已有元素位置时)的时候已有元素的个数大于等于阈值(已有元素等于阈值,下一个存放后必然触发扩容机制)

ConcurrentHashMap有3个参数:

  1. initialCapacity:初始总容量,默认16
  2. loadFactor:加载因子,默认0.75
  3. concurrencyLevel:并发级别,默认16

把数组中的每个元素看成一个桶。大部分都是CAS操作,加锁的部分是对桶的头节点进行加锁,锁粒度很小。

ConcurrentHashMap底层实现原理

image-20211129125258837

CAP理论

CAP理论,指的是在一个分布式系统中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼。

redis缓存一致性

强一致性:写啥读啥

弱一致性:写入后不承诺立即可以读到写入的值,但是会尽可能保证某个时间级别后,数据达到一致状态。

最终一致性:弱一致性的特例。保证一定时间内达到数据一致状态。

Cache-Aside Pattern

旁路缓存。

image-20211129122132008

为什么删除缓存而不是更新缓存:

image-20211129122736232

Read-Through/Write-Through

(读写穿透)

Read-Through就是比 Cache-Aside 多了一层Cache-Provider而已,流程如下:

image-20211129122418128

Write-Through模式下,当发生写请求时,也是由缓存抽象层完成数据源和缓存数据的更新,流程如下:

image-20211129122507224

保证数据库与缓存的一致性

延时双删

image-20211129123310048

  1. 线程1删除缓存,然后去更新数据库
  2. 线程2来读缓存,发现缓存已经被删除,所以直接从数据库中读取,这时候由于线程1还没有更新完成,所以读到的是旧值,然后把旧值写入缓存
  3. 线程1,根据估算的时间,sleep,由于sleep的时间大于线程2读数据+写缓存的时间,所以缓存被再次删除
  4. 如果还有其他线程来读取缓存的话,就会再次从数据库中读取到最新值

消息队列

先更新数据库,成功后往消息队列发消息,消费到消息后再删除缓存,借助消息队列的重试机制来实现,达到最终一致性的效果。

为什么要用数据库连接池?

原文:知乎

应用程序和数据库建立连接的过程是这样的:

  1. 首先通过TCP协议的三次握手和数据库服务器建立连接,然后发送数据库用户账号密码,等待数据库验证用户身份。
  2. 完成用户身份验证后,系统才可以提交SQL语句到数据库执行。
  3. 好了,这个时候假设我们不使用数据库连接池,那么完成一次SQL查询后,我们还要把连接关闭,关闭连接就需要和数据库通信告诉它我们要断开连接了然后再TCP四次挥手最后完成关闭。

这个过程中每一次发起SQL查询所经历的TCP建立连接,数据库验证用户身份,数据库用户登出,TCP断开连接消耗的等待时间都是可以避免的,这明显是一种浪费。打个比方,你去网吧去玩游戏,每次去到呢先插网线,然后开机登录游戏,玩了一会儿要去上厕所,你就退出游戏,然后关机拔网线。去完厕所回来就又重新插网线开机登游戏。

有没有觉得上面例子中的行为很扯蛋,所以每次SQL查询都创建链接,查询完后又关闭连接这个做法本身就很扯蛋。合理的做法就应该是系统启动的时候就创建数据库连接,然后需要使用SQL查询的时候,就从系统拿出数据库连接对象并提交查询,查询完了就把连接对象还给系统。系统在整个程序运行结束的时候再把数据库连接关闭。考虑到一般数据库应用都是Web多用户并发应用,那么只有一个数据库连接对象肯定不够用,所以系统启动的时候就应该多创建几个数据库连接对象供多个线程使用,这一批数据库连接对象集合在一起就被称之为数据库连接池。

数据库连接池就是典型的用空间换时间的思想,系统启动预先创建多个数据库连接对象虽然会占用一定的内存空间,但是可以省去后面每次SQL查询时创建连接和关闭连接消耗的时间。

数据库连接池:维护一定的连接数,方便系统获取连接,使用就去池子中获取,用完放回去就可以了,我们不需要关心连接的创建与销毁,也不需要关心线程池是怎么去维护这些连接的。

Java中数据库连接池

在Java中使用得比较流行的数据库连接池主要有:DBCP,c3p0,druid。 另外,不论使用什么连接池,低层都是使用JDBC连接,即:在应用程序中都需要加载JDBC驱动程序。

一条 SQL 的执行过程

image-20211124125320911

反问问题记录

进去之后有没有遗留的技术大坑需要填,比如会不会进去之后先改半年的 Bug?

线程进程

与进程不同的是同类的多个线程共享进程的方法区资源,但每个线程有自己的程序计数器虚拟机栈本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。虚拟内存。。。

内存管理

内存管理方式主要分为:页式管理、段式管理和段页式管理。

典型地址空间有三个逻辑不同的段:代码,堆,栈。既然堆、栈之间的未分配区域造成了内存碎片,那就更细化一点,不是将进程地址整个映射到物理地址,而是将不同的段分别映射到物理地址。更简单的方法就是通过空闲列表管理算法实现,尝试保留更大的内存块。具体的算法很多,但都无法完全消除外部碎片。

操作系统为每个进程保存一个数据结构,来记录地址空间虚拟页在物理内存的位置,称为页表。主要用来进行地址转换。使用分页作为核心机制来实现虚拟内存,性能开销较大,那如何加速地址转换呢?软件上想不通的,就要由硬件来提供帮助,而且很多时候硬件的一些略微的改变,会带来巨大的性能提升。这里就是要增加地址转换旁路缓冲存储器(translation-lookaside buffer, TLB),或者称为地址转换缓存。还记的上面的访问速度图吗,cache超快的。每次内存访问,先看一下TLB,如果有就很快完成转换,不再访问页表,没有就需要去查页表了。