唯's Blog

笔者是一个热爱编程的 Java 程序员。

0%

Innodb 聚集索引会存放两份数据吗?

  • 我们知道,Innodb引擎下的分为聚集索引和非聚集索引,非聚集索引的叶子节点数据存放的是主键值;主键索引是用主键建立的索引,叶子节点存放的是整行数据。

那么,是不是每行数据都被存了两遍呢(数据本身,一级索引再存一遍)?

  • 当然不是,一级索引是建立再数据本身的

  • 在 Innodb 引擎下,如果建表时没有指定一个主键,mysql 会使用表中整型唯一索引做主键,如果还是没有,则mysql默认创建一个隐藏主键列(自增)。

分布式锁的四个必要条件

  • 互斥性:在任意时刻只有一个客户端能够获取锁资源。
  • Redis 单线程保证了节点在高并发创建时的互斥 set key value 1 px nx
  • ETCD Revision 机制 revision值依次从小到大获取锁,公平锁
  • Zookeeper 容器 顺序节点 处理指令为单线程 保证互斥
  • 安全性:避免在异常情况下产生死锁线程异常中断 一直无法解锁,需要保证线程没有正常解锁的情况下,有机制保证解锁
  • Redis key 过期机制
  • ETCD 租约机制
  • Zookeeper 临时节点
  • 可用性 :加锁、释放锁的过程性能开销要尽量低,同时当提供锁服务的节点发生宕机等不可恢复性故障时,“热备” 节点能够接替故障的节点继续提供服务,并保证自身持有的数据与故障节点一致。
  • Redis 主从哨兵、cluster 性能(内存操作)
  • ETCD 集群 内存操作 性能(内存操作)
  • Zookeeper 主从(Leader - follower -) 性能(内存操作)
  • 对称性:对于任意时刻,加解锁均是同一个客户端(线程)
  • Redis key ()
  • etcd key
  • zookeeper key

特殊问题

主从结构下,数据不一致(同步时延导致)导致锁资源不互斥,同一时刻存在多个客户端获取到锁的情况

  • Redis redlock 锁多个实例,同时向大多数 Redis 实例申请成功之后才算 锁成功

  • Zookeeper ZAP 保证 写一致性(写入大部分节点成功,才算写入成功),leader 节点出现故障,重新选举会选择最新数据的follower节点作主 (写节点永远都是一个节点)

  • ETCD 采用 raft 算法 写一致性

过期时间,业务系统还没执行完,锁 失效了

  • Redis watch dog (底层大致原理异步线程,续约过期时间)

  • Zookeeper 临时节点,会话断开临时节点便会删除,不用自定义过期时间,但是也存在问题,Zookeeper 服务器维护一个Session 依赖客户端发送定时心跳来维持连接,客服端由于网络延迟,GC 等情况定时心跳没发送过来,Zookeeper 也会认为 Session 过期,删除该Session,另外一个客户端就能获取到锁

  • ETCD 的租约机制 保证了安全性,线程异常中断,没有解锁,租约到期自动删除解锁 (watch dog 续约)

由于网络原因,GC 原因没续约,导致锁资源不互斥

  • 客服端由于网络延迟,GC 等情况 Watch dog没续约,Zookeeper 客服端没有发送心跳

  • 解决方案:要存在一个兜底的策略,分布式锁不是绝对安全。

羊群效应,在并发量大的情况下,线程争夺锁资源激烈,可能存在多个线程等待获取锁资源的情况,那么一个线程的解锁会导致多个线程再次去争夺临界资源。

  • Redis,notify ()

  • Zookeeper 创建顺序节点,从小到大依次排列,利用 Zookeeper waitch 机制,后一个节点只监听前一个节点的状态变化,避免了羊群效应

  • ETCD watch 机制,客户端获取锁的列表 /lock/mylock 读取 Key 的值列表(列表中带有 Key 对应的 Revision),判断自己 Key 的 Revision 是否是当前节点最小的,如果是则获取到锁,否则监听列表中前一个比自己小的 Key 的删除事件,一旦监听到删除事件(主动删除或者租约到期删除)则自己获取到锁。

优化

  • 在任何场景都添加互斥锁,性能会急剧下降,所以引出了读写锁

Spring 事务失效场景之Bean对象调用自己方法

  • Bean 对象调用自己的方法,会导致自己的方法事务失效

  • 多线程导致事务失效,这样会导致两个方法不在同一个线程中,获取到的数据库连接不一样,从而是两个不同的事务。如果想doOtherThing方法中抛了异常,add方法也回滚是不可能的

  • Spring 声明式事务依赖的是 动态代理对象,在动态代理对象中this.method 调用的不是代理方法而是调用对象本身的方法,导致没有添加事务方法 ,形成了事务失效

  • 为什么在我们日常开发中事务失效了,但是没有暴露出线上问题呢?

  1. 因为事务的传播机制,Spring默认的事务行为是:propagation_required,

客户端与调用端存在同一个事务中,如果被调用端发生异常,那么调用端和被调用端事务都将回滚

Spring 支持7种事务传播行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

propagation_required(xml文件中为required) 表示当前方法必须在一个具有事务的上下文中运行,如有客户端有事务在进行,那么被调用端将在该事务中运行,否则的话重新开启一个事务。(如果被调用端发生异常,那么调用端和被调用端事务都将回滚

propagation_supports(xml文件中为supports) 表示当前方法不必需要具有一个事务上下文,但是如果有一个事务的话,它也可以在这个事务中运行

propagation_mandatory(xml文件中为mandatory) 表示当前方法必须在一个事务中运行,如果没有事务,将抛出异常

propagation_nested(xml文件中为nested) 表示如果当前方法正有一个事务在运行中,则该方法应该运行在一个嵌套事务中,被嵌套的事务可以独立于被封装的事务中进行提交或者回滚。如果封装事务存在,并且外层事务抛出异常回滚,那么内层事务必须回滚,反之,内层事务并不影响外层事务。如果封装事务不存在,则同propagation_required的一样

propagation_never(xml文件中为never) 表示当方法务不应该在一个事务中运行,如果存在一个事务,则抛出异常

propagation_requires_new(xml文件中为requires_new) 表示当前方法必须运行在它自己的事务中。一个新的事务将启动,而且如果有一个现有的事务在运行的话,则这个方法将在运行期被挂起,直到新的事务提交或者回滚才恢复执行

propagation_not_supported(xml文件中为not_supported) 表示该方法不应该在一个事务中运行。如果有一个事务正在运行,它将在运行期被挂起,直到这个事务提交或者回滚才恢复执行

TransactionSynchronizationManager(事务信息的管理器,用于实现事务传播行为)

Spring嵌套事务依赖Spring实现事务挂起功能(suspend())底层实现:(事务的执行过程,无论是嵌套还是其他的事务传播行为,都是同一个线程因此需要事务的挂起)

其实就是将线程变量里面的事务信息拿出来,再置空。 待事务提交或回滚后再放回线程变量中

TransactionSynchronizationManager 中的事务信息拿出来,存放在 SuspendedResourceHolder,等嵌套事务执行完,在将其放回 TransactionSynchronizationManager

嵌套事务还有一个特性,外层事务影响内层事务,内层不会影响外层,怎么实现?

利用 JDBC Savepoint,JDBC Savepoint帮我们在事务中创建检查点(checkpoint),这样就可以回滚到指定点

tip

Spring 引入事务:

  • starter
1
2
3
4
5
6
7
8
9

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-jdbc</artifactId>

</dependency>

  • jar
1
2
3
4
5
6
7
8
9

<dependency>

<groupId>org.springframework</groupId>

<artifactId>spring-tx</artifactId>

</dependency>

核心原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

// 定义自己的 Thread

FastThreadLocalThread

private InternalThreadLocalMap threadLocalMap;



FastThreadLocal 初始化时获取一个定值的Index

index = InternalThreadLocalMap.nextVariableIndex()

index 便是 threadLocalMap 查找对应元素的索引

优化点

高效查找

1
2
3
4
5
6
7

FastThradLocal

Object[]

Object[0] == Set<FastThreadLocal<?>> (addToVariablesToRemove) // 方便清除

安全(内存泄露)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

ThreadLocal Key 弱引用存在内存泄露的问题,虽然针对这个问题 JDK 有优化,在 set()/get() 会清除无引用的 value,这种主动清除的方式会有一定的性能消耗。

针对这个问题,Netty 定义 FastThreadLocal ,定义 FastThreadLocalRunnable 在多线程的情况下,线程运行完自动调用 FastThreadLocal.removeAll() 清除相关内存引用(Set<FastThreadLocal<?>>)

// 利用装饰者模式 保证 Runnable

@Override

public void run() {

try {

runnable.run();

} finally {

FastThreadLocal.removeAll();

}

}

并行加载的实现方式

从IO模型上分为同步模型异步模型

同步模型的缺陷

  • CPU资源大量浪费在阻塞等待上,导致CPU资源利用率低。在Java 8之前,一般会通过回调的方式来减少阻塞,但是大量使用回调,又引发臭名昭著的回调地狱问题,导致代码可读性和可维护性大大降低。

  • 为了增加并发度,会引入更多额外的线程池,随着CPU调度线程数的增加,会导致更严重的资源争用,宝贵的CPU资源被损耗在上下文切换上,而且线程本身也会占用系统资源,且不能无限增加。

NIO 异步模型

使用 CompletableFuture 相较于 Future、RxJava、Reactor 具有 学习成本低、满足我们异步、可组合的需求

假设有三个操作step1、step2、step3存在依赖关系,其中step3的执行依赖step1和step2的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

CompletableFuture<String> step1 = CompletableFuture.supplyAsync(() -> "123");

CompletableFuture<String> step2 = CompletableFuture.supplyAsync(() -> "abc");

uCompletableFuture.thenCombine(stringCompletableFuture, (s1, s2) -> {

return s1 + s2;

}).thenAccept(s -> {

System.out.println(s);

});

设计思想

类似“观察者模式”的设计思想

每个 Completable 可以看做一个被观察者,其内部有一个 Completion 类型的链表成员变量 stack。

AQS 使用 FIFO 队列实现了一个锁相关的并发器模板,可以基于这个模板来实现各种锁,包括独占锁、共享锁、信号量等。

AQS 中,有一个核心状态是 waitStatus,这个代表节点的状态,决定了当前节点的后续操作,比如是否等待唤醒,是否要唤醒后继节点。

1
2
3
4
5
6
7

// a.newegg.org/newegg-docker 可替换

alias dfimage="docker run -v /var/run/docker.sock:/var/run/docker.sock --rm a.newegg.org/newegg-docker/alpine/dfimage"

dfimage -sV=1.36 7270ef2de0b0

https://img2020.cnblogs.com/blog/1128804/202010/1128804-20201022161838828-1750298688.png

  • 默认情况下,Namespace=public、Group=DEFAULT_GROUP,默认Cluster是DEFAULT。

  • Nacos的默认的命名空间是public,Namespace主要用来实现隔离。比如说,现在有三个环境:开发、测试和生产环境,我们就可以创建三个Namespace,不同的Namespace之间是隔离的。

  • Group默认是DEFAULT_GROUP,Group可以把不同的微服务划分到同一个分组里面。

  • Service就是微服务。一个Service可以包含多个Cluster(集群),Nacos默认Cluster就是DEFAULT,Cluster是对指定微服务的一个虚拟划分。比如说,为了容灾,将Service微服务分别部署在了杭州机房和广州机房,这时候就可以给杭州机房的Service微服务起一个集群名称(HZ),给广州机房的Service微服务起一个集群名称(GZ),还可以尽量让同一个机房的微服务互相调用,以提升性能。

  • Instance,就是微服务的实例。

在表数据量大的情况下,直接对表进行新增修改字段操作会导致锁表,这个过程可能需要很长时间甚至导致服务崩溃。因此需要另外一种方法

一般情况下,十几万的数据量,可以直接进行加字段操作。

  1. 将当前数据库表导出成文件

  2. 基于要操作的表创建一个临时表,执行要修改的操作,比如add column或者drop column

  3. 将文件导入新的临时表

注意不要用intsert into table_copy select * from table,

导成文件

1
2
3
4
5

select * from cms_gift_code into outfile '/usr/local/mysql/data/cms_gift_code.txt' fields terminated by ',' line terminated by '

';

将表中数据导入到新的临时表

  1. 对新表进行修改新增字段操作 DDL

  2. 将在这期间旧表产生的数据 复制到 新表。(ID、时间)

  3. 将新表重命名, 旧表重命名为新表

1
2
3

rename table product to product_bak, product_copy to product;

注:导出 导入需要时间、因此步骤3的操作导入的数据可能不完整,这时需要通过一些机制来比对新老数据 (ID、时间)。找到差异数据、再导入新的临时表

不过还是会可能损失极少量的数据。

所以,如果表的数据特别大,同时又要保证数据完整,最好停机操作。

另外的方法

  1. 在从库进行加字段操作,然后主从切换

  2. 使用第三方在线改字段的工具

服务注册 (异步、单线程、CopyOnWrite)

AP 模式下,服务注册使用异步思想,具体实现放入到一个阻塞队列中,提高了客户端服务的启动速度。真实注册服务时使用一个单线程消费阻塞队列,写入复制一份,不会阻塞读,单线程不存在上下文切换、线程安全。