背景

以往的 Java Web 开发中,异常处理通常是通过 try-catch 语句块来实现。这种方法在应用程序规模较小的情况下还可以,但是在大型应用中,可能存在大量的代码重复和不一致问题。此外,在抛出未处理的异常时,用户会看到系统生成的默认错误页面,用户体验差。

阅读全文 »

优化慢 SQL

慢查询日志记录了执行时间超过 long_query_time(默认 10s通常设置为 1s)的所有查询语句,在解决 SQL 慢查询问题时经常会用到。

  • 查询慢查询日志是否开启 (默认关闭)

    1
    show variables like "slow_query_log;

    开启慢查询日志

    1
    SET GLOBAL slow_query_log=ON;
  • 查看慢查询的 超时时间

    1
    show variables like "%long_query_time%";

    修改 long_query_time 参数:

    1
    SET GLOBAL long_query_time=1;
  • 查询当前 慢查询语句的个数

    1
    show global status like "%Slow_queries%";
  • 查询慢查询日志存放位置

    1
    SHOW VARIABLES LIKE "slow_query_log_file";
  • 无论是否超时,未被索引的记录也被记录

    1
    SET GLOBAL log_queries_not_using_indexes = "ON";
  • 慢查询仅记录扫描 行数 > 此参数 的 SQL

    1
    SET SESSION min_examined_row_limit = 100;

设置完成后,可以用 SHOW VARIABLES LIKE "slow%"; 命令查看。

阅读全文 »

概述

MySQL支持多种存储引擎,可以通过SHOW ENGINES命令查看MySQL所支持的所有存储引擎。

MySQL 当前默认的存储引擎是 InnoDB。并且,所有的存储引擎中只有 InnoDB 是事务性存储引擎,也就是说只有 InnoDB 支持事务

MySQL 5.5.5 之前MyISAM 是 MySQL 的默认存储引擎。5.5.5 版本之后,InnoDB 是 MySQL 的默认存储引擎。

阅读全文 »

概述

MySQL中常见的日志类型主要有下面几类(针对InnoDB存储引擎):

  1. 慢查询日志 (slow query log)
  2. 二进制日志 (binlog)
  3. 事务日志
    • 重做日志 (redo log)
    • 撤销日志 (undo log)
阅读全文 »

概述

在分布式系统中,分布在不同主机上的节点需要检测其他节点的状态,如服务器节点需要检测从节点是否失效。为了检测对方节点的有效性,每隔固定时间就发送一个固定信息给对方,对方回复一个固定信息,如果长事件没有收到对方的回复,则断开与对方的连接

因为是每隔固定时间发送一次,类似心跳,所以发送的固定信息称为心跳包。一般而言,应该客户端主动向服务器发送心跳包,因为服务器发送会影响性能


心跳机制的实现方式

基于TCP自带的心跳包

TCP的SO_KEEPLIVE选项可以,系统默认的跳帧频率为2小时,超过2小时后,本地的TCP实现会发送一个数据包给远程的Socket。如果远程Socket没有响应,TCP实现就会持续尝试11分钟直到接收到响应,否则自动断开Socket连接。

但TCP自带的心跳包无法检测比较敏感地知道对方的状态,默认2小时的空闲时间,对于大多数的应用而言太长了。可以手工开启KeepAlive功能并设置合理的KeepAlive参数。


应用层自己进行实现

  1. Client使用定时器发送心跳
  2. Server收到心跳后,回复一个包
  3. Server为每个Client启动超时定时器,如果在指定时间内没有收到Client的心跳包,则Client失效。

Java实现心跳机制 - 苍穹2018 - 博客园 (cnblogs.com)

枚举(enum)类型是Java 5新增的特性,它是一种新的类型,允许用常量来表示特定的数据片断,而且全部都以类型安全的形式来表示。

枚举类和静态常量的对比

以一段代码为例:

1
2
3
4
public static final int SEASON_SPRING = 1;
public static final int SEASON_SUMMER = 2;
public static final int SEASON_FALL = 3;
public static final int SEASON_WINTER = 4;

使用常量会有缺陷:类型不安全。如果一个方法要求传入季节参数,开发者可以传入任意的int值,如果是枚举类型,就只能传入枚举类中包含的对象


枚举的使用

定义枚举类

1
2
3
public enum Color {
RED, GREEN, BLANK, YELLOW
}

使用方式一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Demo1 {
public static void main(String[] args) {
System.out.println(isRed(Color.BLANK)) ; //结果: false
System.out.println(isRed(Color.RED)) ; //结果: true
}


static boolean isRed(Color color){
if (Color.RED.equals(color)) {
return true ;
}
return false ;
}

}

使用方式二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 public class Demo2 {
public static void main(String[] args) {
showColor(Color.RED);
}

static void showColor(Color color){
switch (color) {
case BLANK:
System.out.println(color);
break;
case RED :
System.out.println(color);
break;
default:
System.out.println(color);
break;
}
}
}

自定义函数

由于枚举的理想使用场景是直接使用,应该避免使用有参构造函数、set方法等可以外部更新枚举值的操作,将属性定义为final,只提供get方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public enum Color {
RED("红色", 1),
GREEN("绿色", 2),
YELLOW("黄色", 3);

private final String name;
private final int index;

public String getName() {
return name;
}

public int getIndex() {
return index;
}
}

使用

1
2
3
4
5
6
7
8
9
10
11
12
public class Demo {
public static void main(String[] args) {
//输出某一枚举的值
System.out.println(Color.RED.getName());
System.out.println(Color.RED.getIndex());

//遍历所有的枚举
for(Color color : Color.values()){
System.out.println(color + " name: " + color.getName() + " index: " + color.getIndex());
}
}
}

概述

ZooKeeper是一个开源的分布式协调服务框架,它的设计目标是将那些复杂且容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一系列简单易用的接口提供给用户使用。

ZooKeeper通常被用于实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master选举、分布式锁和分布式队列等功能。这些功能的实现主要依赖于ZooKeeper提供的数据存储 + 事件监听功能。

ZooKeeper将数据保存在内存中,性能不错。协调服务的典型场景是读多于写,更能凸显ZooKeeper的优势。


重要概念

数据模型 Data model

ZK的数据模型采用层次化的多叉树型结构,每个节点上都可以存储数据,这些数据可以是数字、字符串或者是二级制序列。

每个节点可以拥有N个子节点,最上层是根节点,以/来代表。每个数据节点在ZK中被称为znode,它是ZK中数据的最小单元。并且,每个znode都有一个唯一的路径标识。

ZK主要是用来协调服务的,而不是用来存储业务数据的,所以不要放比较大的数据在znode上,ZK给出的上限是每个节点的数据大小为1M


数据节点 znode

通常将znode分为4大类:

  • 持久节点 PERSISTENT

    一旦创建就一直存在,即使ZK集群宕机也是如此,直到将其删除。

  • 临时节点 EPHEMERAL

    临时节点的生命周期与客户端会话(Session)绑定,Session消失则节点消失。并且,临时节点只能做叶子节点,不能创建子节点。

  • 持久顺序节点 PERSISTENT_SEQUENTIAL

    除了具有持久节点的特性之外,子节点的名称还具有顺序性,比如/node1/app0000001/node/app0000002

  • 临时顺序节点 EPHEMERAL_SEQUENTIAL

    除了具有临时节点的特性之外,子节点的名称还具有顺序性

每个znode由两部分组成:

  1. stat 状态信息
  2. data 节点存放的数据的具体内容

版本 version

stat中记录了znode的三个相关版本:

  1. dataVersion

    当前znode节点的版本号

  2. cversion

    当前znode子节点的版本

  3. aclVersion

    当前znode的ACL版本


权限控制 ACL

ZK采用ACL(AccessControlLists)策略来进行权限控制,类似于UNIX文件系统的权限控制。

ZK提供了5种znode的操作权限

  1. CREATE 能创建子节点
  2. READ 能获取节点数据和列出其子节点
  3. WRITE 能设置/更新节点数据
  4. DELETE 能删除子节点
  5. ADMIN 能设置节点ACL的权限

ZK还提供了4种身份认证的方式:

  1. world 默认,所有用户都可以访问
  2. auth 不使用任何id,表示任何已认证的用户
  3. digest 用户名:密码认证方式
  4. ip 对指定ip进行限制

事件监听器 Watcher

ZK允许用户在指定节点上注册一些Watcher,并且在一些特定事件触发的时候,ZK服务端将事件通知到感兴趣的客户端上。该机制是ZK实现分布式协调服务的重要特性


会话 Session

Session可以看作是ZK服务器与客户端之间的一个TCP长连接,通过这个连接,客户端能够通过心跳检测与服务器保持有效的会话,也能向ZK服务器发送请求并接收响应,同时还能够通过该连接接收来自服务器的Watcher事件通知

在为客户端创建会话之前,服务端首先会为每个客户端都分配一个sessionIDsessionID是ZK会话的一个重要标识,许多与会话相关的运行机制都基于这个sessionID,因此,**sessionID的分配务必保证全局唯一**。


ZooKeeper集群

通常3台服务器就可以构成一个ZK集群,每台服务器都会在内存中维护当前的服务器状态,并且服务器之间都保持着通信。集群之间通过ZAB协议(ZooKeeper Atomic Broadcast)来保持数据的一致性。


集群角色

最典型的集群模式:Master/Slave 模式(主备模式)。这种模式中,通常Master服务器作为主服务器提供写服务,其他Slave服务器作为从服务器通过异步复制的方式获取Master服务器最新的数据提供读服务。但是ZK中没有选择传统的Master/Slave概念,而是引入了Leader、Follower、Observer三种角色。

image-20230919104014780

ZK集群中的所有机器通过一个Leader选举过程来选定一台称为Leader的机器。

角色 说明
Leader 为客户端提供读、写服务。负责选举过程的投票发起和决议,更新系统状态
Follower 为客户端提供读服务,如果是写服务则转发给Leader参与选举过程中的投票
Observer 为客户端提供读服务,如果是写服务则转发给Leader不参与选举过程中的投票,也不参与写操作的“过半写成功”策略,因此Observer可以在不影响写性能的情况下提升集群的读性能。ZK 3.3新增的角色。

Leader选举

当Leader服务器出现网络中断、崩溃退出、重启等异常情况时,就会进入Leader选举过程,产生新的Leader服务器。Leader选举过程如下:

  1. Leader election (选举阶段)

    节点在一开始都处于选举阶段,只要有一个节点得到超半数节点的票数,就可以当选准Leader

  2. Discovery (发现阶段)

    follower跟准Leader进行通信,同步follower最近接收的事务提议

  3. Synchronization (同步阶段)

    利用Leader前一阶段获得的最新提议历史,同步集群中所有的副本。同步完成后,准Leader称为真正的Leader

  4. Broadcast (广播阶段)

    ZK正式对外提供事务服务,并且Leader可以进行消息广播。如果有新节点加入,对新节点进行同步。

ZK集群中的服务器状态有4种

  1. LOOKING 寻找Leader
  2. LEADING leader状态
  3. FOLLOWING Follower状态
  4. OBSERVING Observer状态

ZK集群为什么最好奇数台?

ZK集群在宕机了几个ZK服务器后,如果剩下的ZK服务器个数>宕机的服务器个数,ZK才依然可用。奇数台能更高效地保持ZK集群的稳定性(3服务器,允许宕机1台,4服务器也只能允许宕机1台,偶数台对ZK集群的稳定性没有提升)。


选举的过半机制 防止脑裂

对于一个集群,通常会将多台机器部署在不同机房,来提高这个集群的可用性。如果发生一种机房间网络线路故障,导致机房之间的网络不不通,集群被割裂成几个小集群,这时候子集群各自选主,当网络恢复的时候,会有多个主节点,仿佛是大脑分裂,故称脑裂现象。脑裂期间,各个主节点都可能对外提供了服务,会带来数据一致性等问题

ZK的过半机制导致不可能产生2个leader。


ZAB 协议

ZAB (ZooKeeper Atomic Broadcast 原子广播)协议是为分布式协调服务ZK专门设计的一种支持崩溃恢复的原子广播协议。ZK主要依赖ZAB协议来实现分布式数据一致性。基于该协议,ZK实现了一种主备模式的系统架构来保持集群中各个副本之间的数据一致性。

ZAB协议包括两种基本的模式:

  1. 崩溃恢复

    当整个服务框架在启动过程中,或是Leader服务器出现网络中断、崩溃退出、重启等异常情况,ZAB协议就会进入恢复模式并选举产生新的Leader服务器。

    当选举产生了新的Leader服务器,同时集群中已经有过半的机器完成了状态同步(数据同步),ZAB协议就退出恢复模式。

  2. 消息广播

    当集群中已经有过半的Follower服务器完成了和Leader服务器的状态同步,整个服务框架就可以进入消息广播模式。


一致性问题

设计一个分布式系统一定会遇到一个问题:因为分区容忍性(partition tolerance)的存在,就必定要求我们需要在系统可用性(availability)数据一致性(consistency)中做出权衡。


典型应用场景

选主

由于ZK的强一致性,能够很好地保证在高并发的情况下,保证节点创建的全局唯一性(即无法重复创建一样的节点)

  1. 利用这个特性,可以让多个客户端创建一个指定的临时节点,创建成功的就是Master。
  2. 让不是master的节点监听master节点的状态(比如监听这个临时节点的父节点,如果子节点数变了,就表示master挂了,触发回调函数重新进行选举;或者直接监听节点的状态,通过节点是否已经失去连接来爬暖master是否挂了)

分布式锁

互斥锁

  1. 由于创建节点的唯一性,可以让多个客户端同时创建一个临时节点,创建成功的说明获取到了锁
  2. 没有获取到锁的客户端创建一个watcher进行节点状态的监听,如果这个互斥锁被释放了(客户端宕机了,或者客户端主动释放了锁),可以调用回调函数重新获得锁。

同时实现共享锁和互斥锁

  1. 创建有序节点。
  2. 读请求时(需要获取共享锁),如果没有比自己更小的节点,或者比自己小的节点都是读请求,就可以获取到共享锁。如果比自己小的节点中有写请求,则当前客户端无法获取到共享锁,只能等待写请求完成。
  3. 写请求时(需要获取互斥锁),如果没有比自己更小的节点,就可以获取到互斥锁,对数据进行修改。如果发现有比自己更小的节点,无论读写操作,都不能获取到互斥的写锁,等待所有前面的操作完成。

这样就很好地同时实现了共享锁和互斥锁。但是当一个锁得到释放,它会通知所有等待地客户端,从而造成羊群效应,可以让等待的节点只监听他们前面的节点


命名服务

如何给一个对象设置ID,大家可能都会想到UUID,但是UUID太长了。可以使用节点的全路径作为命名方式,由于路径是可以自定义的,对于有些语义的对象的ID设置可以提高可读性。


集群管理和注册中心

  • 集群管理

    可能我们会有这样的需求,需要了解整个集群中有多少机器在工作,我们想对集群中的每台机器的运行时状态进行数据采集,对集群中的机器进行上下线等操作。

    ZK天然支持的watcher和临时节点能很好地实现这些需求。我们可以为每台机器创建临时节点,并监控其父节点,如果子节点列表有变动,可以使用在其父节点绑定的watcher进行状态监控和回调。

  • 注册中心

    服务提供者在ZK中创建一个临时节点,并将自己的ip、port、调用方式写入节点,当服务消费者需要调用的时候,通过注册中心找到相应的服务的地址列表 ,并缓存到本地(方便以后调用),当消费者调用服务时,不再去请求注册中心,直接通过负载均衡算法从地址列表中取一个服务提供者的服务器调用服务。

    当服务提供者的某台服务器宕机或者下线时,相应的地址会从服务提供者地址列表中移除。同时,注册中心会将新的服务地址列表发送给服务消费者的机器并缓存在消费者本机(也可以让消费者进行节点监听)


数据发布/订阅

通过Watcher机制,可以方便地实现数据发布/订阅。将数据发布到ZK被监听的节点上,其他机器可以通过监听节点上的变化来实现配置的动态更新。


Docker安装ZooKeeper

  1. docker pull zookeeper
  2. docker run -d -name=zookeeper -p 2181:2181 zookeeper

连接ZooKeeper服务

  1. docker ps查看ZooKeeper容器的ID
  2. docker exec -it ContainerId bash启动并进入容器
  3. ./bin/zkCli.sh -server 127.0.0.1:2181命令连接至ZooKeeper服务

常用命令

  1. ls 查看节点结构

  2. create 创建节点

    • 在根目录创建了node1节点,与之关联的字符串是”node1”

      create /node1 "node1"

    • 在node1节点下创建node1.1节点,与之关联的内容是数字123

      create /node1/node1.1 123

  3. set 更新节点数据内容

    set /node1 "set node1"

  4. get 获取节点的数据

    get -s /node1

    -s 额外显示节点状态信息

  5. stat 查看节点状态

  6. delete 删除节点

    需要被删除的节点没有子节点

    delete /node1


ZooKeeper的Java客户端Curator

相比于ZooKeeper自带的客户端zookeeper来说,Curator的封装更加完善,各种API都能比较方便地使用。

1
2
3
4
5
6
7
8
9
10
11
<!-- zookeeper的java客户端 -->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>5.5.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>5.5.0</version>
</dependency>

创建客户端

Curator的创建会话方式与原生的API和ZkClient的创建方式区别很大。Curator创建客户端是通过CuratorFrameworkFactory工厂类来实现的。

1
2
3
4
5
6
7
8
9
10
11
12
// 提供重试策略的接口,初始sleep时间1000ms,最大重试次数3次,并在之后的每次重试的等待时间大概率会增加(随机)
// long sleepMs = baseSleepTimeMs * Math.max(1, random.nextInt(1 << (retryCount + 1)));
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
private static CuratorFramework Client = CuratorFrameworkFactory.builder()
.connectString("hadoop1:2181,hadoop2:2181,hadoop3:2181") // zk的server地址,多个server之间使用英文逗号分隔开
.connectionTimeoutMs(5000) // 连接超时时间
.sessionTimeoutMs(3000) // 会话超时时间
.retryPolicy(retryPolicy) // 失败重试策略
.build();

// 阻塞到创建会话成功为止
client.start();

创建节点

创建一个初始内容为空的节点

1
client.create().forPath(path);

Curator默认创建的是持久节点,内容为空。


创建一个包含内容的节点

1
client.create().forPath(path, "我是内容".getBytes());

Curator和ZkClient不同的是依旧采用Zookeeper原生API的风格,内容使用byte[]作为方法参数。


创建临时节点,并递归创建父节点

1
client.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath(path);

此处Curator和ZkClient一样封装了递归创建父节点的方法。在递归创建父节点时,父节点为持久节点


删除节点

删除一个子节点

1
client.delete().forPath(path);

删除节点并递归删除其子节点

1
client.delete().deletingChildrenIfNeeded().forPath(path);

指定版本进行删除

1
client.delete().withVersion(1).forPath(path);

如果此版本已经不存在,则删除异常,异常信息如下:org.apache.zookeeper.KeeperException$BadVersionException: KeeperErrorCode = BadVersion for


强制保证删除一个节点

1
client.delete().guaranteed().forPath(path);

只要客户端会话有效,那么Curator会在后台持续进行删除操作,直到节点删除成功。比如遇到一些网络异常的情况,此guaranteed的强制删除就会很有效果。


读取数据

Curator提供了传入一个Stat,使用节点当前的Stat替换到传入的Stat的方法,查询方法执行完成之后,Stat引用已经执行当前最新的节点Stat。

1
2
3
4
5
// 普通查询
client.getData().forPath(path);
// 包含状态查询
Stat stat = new Stat();
client.getData().storingStatIn(stat()).forPath(path);

更新数据

更新数据,如果未传入version参数,那么更新当前最新版本,如果传入version则更新指定version,如果version已经变更,则抛出异常。

1
2
3
4
// 普通更新
client.setData().forPath(path,"新内容".getBytes());
// 指定版本更新
client.setData().withVersion(1).forPath(path);

版本不一直异常信息:org.apache.zookeeper.KeeperException$BadVersionException: KeeperErrorCode = BadVersion for


Zookeeper框架Curator使用 - 扎心了,老铁 - 博客园 (cnblogs.com)

代理模式

代理模式简单来说就是,使用代理对象代替真实对象的访问。可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能,比如在某个方法执行前后,可以增加一些自定义的操作。

[静态代理](多线程详解.md/#静态代理模式 static proxy)

动态代理

静态代理中,对目标对象的每个方法的扩展都是手动完成的,非常不灵活(比如一旦新增方法,目标对象和代理对象都要进行修改)、麻烦(需要对每个目标类都单独写一个代理类)。日常开发几乎看不到使用静态代理的场景

相比于静态代理来说,动态代理更加灵活。不需要针对每个目标类都单独创建一个代理类,并且也不需要我们必须实现接口,可以直接代理实现类(CGLIB 动态代理机制)。从JVM角度说,动态代理是在运行时 动态生成类字节码,并加载到JVM中

Spring AOP、RPC框架的实现都依赖了动态代理。动态代理在日常开发中使用相对较少,但在框架中是必用的一门技术,对于各种框架原理的理解和学习很有帮助。就Java来说,动态代理的实现方式有很多种,比如JDK 动态代理、CGLIB 动态代理

阅读全文 »

Kryo的注册

和很多其他的序列化框架一样,Kryo 为了提供性能和减小序列化结果体积,提供注册的序列化对象类的方式。在注册时,会为该序列化类生成 int ID,后续在序列化时使用 int ID 唯一标识该类型。注册的方式如下:

1
kryo.register(SomeClass.class);

线程不安全

Kryo 不是线程安全的。每个线程都应该有自己的 Kryo 对象、输入和输出实例。因此在多线程环境中,可以考虑使用 ThreadLocal 或者对象池来保证线程安全性。

ThreadLocal 是一种典型的牺牲空间来换取并发安全的方式,它会为每个线程都单独创建本线程专用的 kryo 对象。对于每条线程的每个 kryo 对象来说,都是顺序执行的,因此天然避免了并发安全问题。创建方法如下:

1
2
3
4
5
6
7
8
9
static private final ThreadLocal<Kryo> kryos = new ThreadLocal<Kryo>() {
protected Kryo initialValue() {
Kryo kryo = new Kryo();
// 在此处配置kryo对象的使用示例,如循环引用等
return kryo;
};
};

Kryo kryo = kryos.get();

之后,仅需要通过 kryos.get() 方法从线程上下文中取出对象即可使用。


Kryo相关配置参数详解

深入理解RPC之序列化篇–Kryo - 知乎 (zhihu.com)

1
2
kryo.setRegistrationRequired(false);//关闭注册行为
kryo.setReferences(true);//支持循环引用

Kryo支持注册行为,如kryo.register(SomeClazz.class);,这会赋予该Class一个从0开始的编号。但Kryo使用注册行为最大的问题在于,其不保证同一个Class每一次注册的号码相同,这与注册的顺序有关,也就意味着在不同的机器、同一个机器重启前后都有可能拥有不同的编号,这会导致序列化产生问题,所以在分布式项目中,一般关闭注册行为。

第二个注意点在于循环引用,Kryo为了追求高性能,可以关闭循环引用的支持。不过我并不认为关闭它是一件好的选择,大多数情况下,请保持kryo.setReferences(true)

Kryo默认情况下是不启用对象引用的。这意味着如果一个对象多次出现在一个对象图中,它将被多次写入,并将被反序列化为多个不同的对象。

举个例子,当开启了引用属性,每个对象第一次出现在对象图中,会在记录时写入一个 varint,用于标记。当此后有同一对象出现时,只会记录一个 varint,以此达到节省空间的目标。此举虽然会节省序列化空间,但是是一种用时间换空间的做法,会影响序列化的性能,这是因为在写入/读取对象时都需要进行追踪。

开发者可以使用 kryo 自带的 setReferences 方法来决定是否启用 Kryo 的引用功能。

深入浅出序列化(2)——Kryo序列化

0%