2024年2月

在zookeeper中,follower也可以接收客户端连接,处理客户端请求,本文将分析follower处理客户端请求的流程:

  • 读请求处理
  • 写请求转发与响应

follower接收转发客户端请求

网络层接收客户端数据包

leader、follower都会启动ServerCnxnFactory组件,用来接收客户端连接、读取客户端数据包、将客户端数据包转发给zk应用层。

在"zookeeper源码(08)请求处理及数据读写流程"一文中已经介绍,ServerCnxn在读取到客户端数据包之后,会调用zookeeperServer的processConnectRequest或processPacket方法:

  • processConnectRequest方法:创建session
  • processPacket方法:处理业务请求

processConnectRequest创建session

  • 会使用sessionTracker生成sessionId、创建session对象
  • 生成一个密码
  • 提交一个createSession类型Request并提交给业务处理器
long createSession(ServerCnxn cnxn, byte[] passwd, int timeout) {
    // 生成sessionId、创建session对象
    long sessionId = sessionTracker.createSession(timeout);
    // 生成密码
    Random r = new Random(sessionId ^ superSecret);
    r.nextBytes(passwd);
    // 提交createSession类型Request
    CreateSessionTxn txn = new CreateSessionTxn(timeout);
    cnxn.setSessionId(sessionId);
    Request si = new Request(cnxn, sessionId, 0, OpCode.createSession, RequestRecord.fromRecord(txn), null);
    submitRequest(si);
    return sessionId;
}

processPacket处理业务请求

  • 封装Request
  • 验证largeRequest
  • 提交业务层处理器
Request si = new Request(cnxn, cnxn.getSessionId(), h.getXid(), h.getType(), request, cnxn.getAuthInfo());
int length = request.limit();
if (isLargeRequest(length)) {
    // checkRequestSize will throw IOException if request is rejected
    checkRequestSizeWhenMessageReceived(length);
    si.setLargeRequestSize(length);
}
si.setOwner(ServerCnxn.me);
submitRequest(si);

FollowerRequestProcessor处理器

在follower端,客户端请求会由FollowerRequestProcessor处理:

  1. 把请求提交下游CommitProcessor处理器
  2. 写请求转发给leader处理
  3. 读请求经过CommitProcessor直接转发给FinalRequestProcessor处理器,直接查询数据返回给客户端
public void run() {
    try {
        while (!finished) {

            Request request = queuedRequests.take();

            // Screen quorum requests against ACLs first 略

            // 转发给CommitProcessor处理器
            // 提交到queuedRequests队列
            // 写请求还会提交到queuedWriteRequests队列
            maybeSendRequestToNextProcessor(request);

            // ...

            // 写请求需要转发给leader处理
            switch (request.type) {
            case OpCode.sync:
                zks.pendingSyncs.add(request); // 待同步命令
                zks.getFollower().request(request);
                break;
            case OpCode.create:
            case OpCode.create2:
            case OpCode.createTTL:
            case OpCode.createContainer:
            case OpCode.delete:
            case OpCode.deleteContainer:
            case OpCode.setData:
            case OpCode.reconfig:
            case OpCode.setACL:
            case OpCode.multi:
            case OpCode.check:
                zks.getFollower().request(request);
                break;
            case OpCode.createSession:
            case OpCode.closeSession:
                if (!request.isLocalSession()) {
                    zks.getFollower().request(request);
                }
                break;
            }
        }
    } catch (Exception e) {
        handleException(this.getName(), e);
    }
}

转发leader

zks.getFollower().request(request);

Learner转发请求:

void request(Request request) throws IOException {
    // 略

    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    DataOutputStream oa = new DataOutputStream(baos);
    oa.writeLong(request.sessionId); // sessionId
    oa.writeInt(request.cxid); // 客户端xid
    oa.writeInt(request.type); // 业务类型
    byte[] payload = request.readRequestBytes(); // 请求体
    if (payload != null) {
        oa.write(payload);
    }
    oa.close();
    // 封装REQUEST数据包
    QuorumPacket qp = new QuorumPacket(Leader.REQUEST, -1, baos.toByteArray(), request.authInfo);
    writePacket(qp, true); // 通过网络发给leader服务器
}

leader处理follower请求

LearnerHandler接收REQUEST请求

case Leader.REQUEST:
    bb = ByteBuffer.wrap(qp.getData());
    sessionId = bb.getLong(); // 解析请求信息
    cxid = bb.getInt();
    type = bb.getInt();
    bb = bb.slice();
    Request si;
    if (type == OpCode.sync) {
        si = new LearnerSyncRequest(
            this, sessionId, cxid, type, RequestRecord.fromBytes(bb), qp.getAuthinfo());
    } else {
        si = new Request(null, sessionId, cxid, type, RequestRecord.fromBytes(bb), qp.getAuthinfo());
    }
    si.setOwner(this); // 用来判断请求来自follower
    learnerMaster.submitLearnerRequest(si); // 提交给业务处理器
    requestsReceived.incrementAndGet();

submitLearnerRequest提交业务处理器:

public void submitLearnerRequest(Request si) {
    zk.submitLearnerRequest(si);
}

LeaderZooKeeperServer提交业务处理器:

public void submitLearnerRequest(Request request) {
    // 提交给PrepRequestProcessor处理器
    prepRequestProcessor.processRequest(request);
}

从此处开始走leader处理写请求流程。

leader处理写请求流程回顾

  • PrepRequestProcessor - 做事务设置
  • ProposalRequestProcessor - 发起proposal,将Request转发给SyncRequestProcessor写事务log、本地ack
  • CommitProcessor - 读请求直接调用下游处理器,写请求需要等待足够的ack之后commit再调用下游RequestProcessor处理器
  • ToBeAppliedRequestProcessor - 维护toBeApplied列表
  • FinalRequestProcessor - 把事务应用到ZKDatabase,提供查询功能,返回响应

follower处理leader数据

在follower中,Follower使用processPacket方法处理来自leader的数据包,此处看一下PROPOSAL和COMMIT的逻辑。

PROPOSAL数据包

fzk.logRequest(hdr, txn, digest);

logRequest会使用syncProcessor将事务写入到txnlog文件,之后调用SendAckRequestProcessor处理器给leader发ack数据包。

leader收到超过半数的ack之后会发COMMIT数据包让各个节点将事务应用到ZKDatabase中。

COMMIT数据包

fzk.commit(qp.getZxid());

CommitProcessor处理器会将其提交到committedRequests队列,之后客户端Request会继续向下游FinalRequestProcessor处理器传递。

FinalRequestProcessor处理器

  • 把事务应用到ZKDatabase中
  • 提供查询功能
  • 给客户端返回响应

前言

我们每天写
vue3
项目的时候都会使用
setup
语法糖,但是你有没有思考过下面几个问题。
setup
语法糖经过编译后是什么样子的?为什么在
setup
顶层定义的变量可以在
template
中可以直接使用?为什么
import
一个组件后就可以直接使用,无需使用
components
选项来显式注册组件?

vue 文件如何渲染到浏览器上

要回答上面的问题,我们先来了解一下从一个
vue
文件到渲染到浏览器这一过程经历了什么?

我们的
vue
代码一般都是写在后缀名为vue的文件上,显然浏览器是不认识vue文件的,浏览器只认识html、css、jss等文件。所以第一步就是通过
webpack
或者
vite
将一个vue文件编译为一个包含
render
函数的
js
文件。然后执行
render
函数生成虚拟DOM,再调用浏览器的
DOM API
根据虚拟DOM生成真实DOM挂载到浏览器上。

setup
编译后的样子


javascript
标准中
script
标签是不支持
setup
属性的,浏览器根本就不认识
setup
属性。所以很明显
setup
是作用于编译时阶段,也就是从vue文件编译为js文件这一过程。

我们来看一个简单的demo,这个是
index.vue
源代码:

<template>
  <h1>{{ title }}</h1>
  <h1>{{ msg }}</h1>
  <Child />
</template>

<script lang="ts" setup>
import { ref } from "vue";
import Child from "./child.vue";

const msg = ref("Hello World!");
const title = "title";
if (msg.value) {
  const content = "content";
  console.log(content);
}
</script>

这里我们定义了一个名为
msg

ref
响应式变量和非响应式的
title
变量,还有
import

child.vue
组件。

这个是
child.vue
的源代码

<template>
  <div>i am child</div>
</template>

我们接下来看
index.vue
编译后的样子,代码我已经做过了简化:

import { ref } from "vue";
import Child from "./Child.vue";

const title = "title";

const __sfc__ = {
  __name: "index",
  setup() {
    const msg = ref("Hello World!");
    if (msg.value) {
      const content = "content";
      console.log(content);
    }
    const __returned__ = { title, msg, Child };
    return __returned__;
  },
};

import {
  toDisplayString as _toDisplayString,
  createElementVNode as _createElementVNode,
  createVNode as _createVNode,
  Fragment as _Fragment,
  openBlock as _openBlock,
  createElementBlock as _createElementBlock,
} from "vue";
function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (
    _openBlock(),
    _createElementBlock(
      _Fragment,
      null,
      [
        _createElementVNode("h1", null, _toDisplayString($setup.title)),
        _createElementVNode(
          "h1",
          null,
          _toDisplayString($setup.msg),
          1 /* TEXT */
        ),
        _createVNode($setup["Child"]),
      ],
      64 /* STABLE_FRAGMENT */
    )
  );
}
__sfc__.render = render;
export default __sfc__;

我们可以看到
index.vue
编译后的代码中已经没有了
template
标签和
script
标签,取而代之是
render
函数和
__sfc__
对象。并且使用
__sfc__.render = render

render
函数挂到
__sfc__
对象上,然后将
__sfc__
对象
export default
出去。

看到这里你应该知道了其实一个
vue
组件就是一个普通的js对象,
import
一个
vue
组件,实际就是
import
这个
js
对象。这个js对象中包含
render
方法和
setup
方法。

编译后的
setup
方法

我们先来看看这个
setup
方法,是不是觉得和我们源代码中的
setup
语法糖中的代码很相似?没错,这个
setup
方法内的代码就是由
setup
语法糖中的代码编译后来的。

setup
语法糖原始代码

<script lang="ts" setup>
import { ref } from "vue";
import Child from "./child.vue";

const msg = ref("Hello World!");
const title = "title";
if (msg.value) {
  const content = "content";
  console.log(content);
}
</script>

setup
编译后的代码

import { ref } from "vue";
import Child from "./Child.vue";

const title = "title";

const __sfc__ = {
  __name: "index",
  setup() {
    const msg = ref("Hello World!");
    if (msg.value) {
      const content = "content";
      console.log(content);
    }
    const __returned__ = { title, msg, Child };
    return __returned__;
  },
};

经过分析我们发现
title
变量由于不是响应式变量,所以编译后
title
变量被提到了
js
文件的全局变量上面去了。而
msg
变量是响应式变量,所以依然还是在
setup
方法中。我们再来看看
setup
的返回值,返回值是一个对象,对象中包含
title

msg

Child
属性,非
setup
顶层中定义的
content
变量就不在返回值对象中。

看到这里,可以回答我们前面提的第一个问题。

setup
语法糖经过编译后是什么样子的?

setup
语法糖编译后会变成一个
setup
方法,编译后
setup
方法中的代码和
script
标签中的源代码很相似。方法会返回一个对象,对象由
setup
中定义的顶层变量和
import
导入的内容组成。


template
编译后的
render
函数

我们先来看看原本
template
中的代码:

<template>
  <h1>{{ title }}</h1>
  <h1>{{ msg }}</h1>
  <Child />
</template>

我们再来看看由
template
编译成的
render
函数:

import {
  toDisplayString as _toDisplayString,
  createElementVNode as _createElementVNode,
  createVNode as _createVNode,
  Fragment as _Fragment,
  openBlock as _openBlock,
  createElementBlock as _createElementBlock,
} from "vue";
function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (
    _openBlock(),
    _createElementBlock(
      _Fragment,
      null,
      [
        _createElementVNode("h1", null, _toDisplayString($setup.title)),
        _createElementVNode(
          "h1",
          null,
          _toDisplayString($setup.msg),
          1 /* TEXT */
        ),
        _createVNode($setup["Child"]),
      ],
      64 /* STABLE_FRAGMENT */
    )
  );
}

我们这次主要看在
render
函数中如何访问
setup
中定义的顶层变量
title

msg

createElementBlock

createElementVNode
等创建虚拟DOM的函数不在这篇文章的讨论范围内。你只需要知道
createElementVNode("h1", null, _toDisplayString($setup.title))
为创建一个
h1
标签的虚拟DOM就行了。


render
函数中我们发现读取
title
变量的值是通过
$setup.title
读取到的,读取
msg
变量的值是通过
$setup.msg
读取到的。这个
$setup
对象就是调用
render
函数时传入的第四个变量,我想你应该猜出来了,这个
$setup
对象就是我们前面的
setup
方法返回的对象。

那么问题来了,在执行
render
函数的时候是如何将
setup
方法的返回值作为第四个变量传递给
render
函数的呢?我在下一节会一步一步的带你通过
debug
源码的方式去搞清楚这个问题,我们带着问题去
debug
源码其实非常简单。

debug
源码搞清楚是如何调用
render
函数

有的小伙伴看到这里需要看源码就觉得头大了,别着急,其实很简单,我会一步一步的带着你去debug源码。

首先我们将
Enable JavaScript source maps
给取消勾选了,不然在debug源码的时候断点就会走到
vue
文件中,而不是走到编译会的js文件中。

然后我们需要在设置里面的Ignore List看看
node_modules
文件夹是否被忽略。新版谷歌浏览器中会默认排除掉
node_modules
文件夹,所以我们需要将这个取消勾选。如果忽略了
node_modules
文件夹,那么
debug
的时候断点就不会走到
node_modules

vue
的源码中去了。

接下来我们需要在浏览器中找到vue文件编译后的js代码,我们只需要在
network
面板中找到这个
vue
文件的
http
请求,然后在
Response
下右键选择
Open in Sources panel
,就会自动在
sources
面板自动打开对应编译后的js文件代码。

找到编译后的js文件,我们想
debug
看看是如何调用
render
函数的,所以我们给render函数加一个断点。然后刷新页面,发现代码已经走到了断点的地方。我们再来看看右边的Call Stack调用栈,发现
render
函数是由一个
vue
源码中的
renderComponentRoot
函数调用的。

点击Call Stack中的
renderComponentRoot
函数就可以跳转到
renderComponentRoot
函数的源码,我们发现
renderComponentRoot
函数中调用
render
函数的代码主要是下面这样的:

function renderComponentRoot(instance) {
  const {
    props,
    data,
    setupState,
    // 省略...
  } = instance;

  render2.call(
    thisProxy,
    proxyToUse,
    renderCache,
    props,
    setupState,
    data,
    ctx
  )
}

这里我们可以看到前面的
$setup
实际就是由
setupState
赋值的,而
setupState
是当前vue实例上面的一个属性。那么
setupState
属性是如何被赋值到
vue
实例上面的呢?

我们需要给
setup
函数加一个断点,然后刷新页面进入断点。通过分析Call Stack调用栈,我们发现
setup
函数是由
vue
中的一个
setupStatefulComponent
函数调用执行的。

点击Call Stack调用栈中的
setupStatefulComponent
,进入到
setupStatefulComponent
的源码。我们看到
setupStatefulComponent
中的代码主要是这样的:

function setupStatefulComponent(instance) {
  const { setup } = Component;
  // 省略
  const setupResult = callWithErrorHandling(
    setup,
    instance
  );
  handleSetupResult(instance, setupResult);
}

setup
函数是
Component
上面的一个属性,我们将鼠标放到
Component
上面,看看这个
Component
是什么东西?

看到这个
Component
对象中既有
render
方法也有
setup
方法是不是感觉很熟悉,没错这个
Component
对象实际就是我们的
vue
文件编译后的js对象。

const __sfc__ = {
  __name: "index",
  setup() {
    const msg = ref("Hello World!");
    if (msg.value) {
      const content = "content";
      console.log(content);
    }
    const __returned__ = { title, msg, Child };
    return __returned__;
  },
};

__sfc__.render = render;

从Component对象中拿到
setup
函数,然后执行
setup
函数得到
setupResult
对象。然后再调用
handleSetupResult(instance, setupResult);

我们再来看看
handleSetupResult
函数是什么样的,下面是我简化后的代码:

function handleSetupResult(instance, setupResult) {
  if (isFunction(setupResult)) {
    // 省略
  } else if (isObject(setupResult)) {
    instance.setupState = proxyRefs(setupResult);
  }
}

我们的
setup
的返回值是一个对象,所以这里会执行
instance.setupState = proxyRefs(setupResult)
,将setup执行会的返回值赋值到vue实例的setupState属性上。

看到这里我们整个流程已经可以串起来了,首先会执行由
setup
语法糖编译后的
setup
函数。然后将
setup
函数中由顶层变量和
import
导入组成的返回值对象赋值给
vue
实例的
setupState
属性,然后执行
render
函数的时候从
vue
实例中取出
setupState
属性也就是
setup
的返回值。这样在
render
函数也就是
template
模版就可以访问到
setup
中的顶层变量和
import
导入。

现在我们可以回答前面提的另外两个问题了:

为什么在
setup
顶层定义的变量可以在
template
中可以直接使用?

因为在
setup
语法糖顶层定义的变量经过编译后会被加入到
setup
函数返回值对象
__returned__
中,而非
setup
顶层定义的变量不会加入到
__returned__
对象中。
setup
函数返回值会被塞到
vue
实例的
setupState
属性上,执行
render
函数的时候会将
vue
实例上的
setupState
属性传递给
render
函数,所以在
render
函数中就可以访问到
setup
顶层定义的变量和
import
导入。而
render
函数实际就是由
template
编译得来的,所以说在
template
中可以访问到
setup
顶层定义的变量和
import
导入。。

为什么
import
一个组件后就可以直接使用,无需使用
components
选项来显式注册组件?

因为在
setup
语法糖中
import
导入的组件对象经过编译后同样也会被加入到
setup
函数返回值对象
__returned__
中,同理在
template
中也可以访问到
setup
的返回值对象,也就可以直接使用这个导入的组件了。

总结

setup
语法糖经过编译后就变成了
setup
函数,而
setup
函数的返回值是一个对象,这个对象就是由在
setup
顶层定义的变量和
import
导入组成的。
vue
在初始化的时候会执行
setup
函数,然后将
setup
函数返回值塞到
vue
实例的
setupState
属性上。执行
render
函数的时候会将
vue
实例上的
setupState
属性(也就是
setup
函数的返回值)传递给
render
函数,所以在
render
函数中就可以访问到
setup
顶层定义的变量和
import
导入。而
render
函数实际就是由
template
编译得来的,所以说在
template
中就可以访问到
setup
顶层定义的变量和
import
导入。

如果我的文章对你有点帮助,欢迎关注公众号:【欧阳码农】,文章在公众号首发。你的支持就是我创作的最大动力,感谢感谢!

开心一刻

今天去超市买饮料

老板说 5 元,我听成了“会员”

我说没有

老板跟我对视了 10 秒

然后,老板说:没有你买啥?

我说:没有就不让买?

当时的老板:

瑕疵回顾

都说了能不动就别动,非要去调整,出生产事故了吧
中有一个地方讲的有瑕疵,不知道你们发现了没有

框住的第一句是没问题的,但第二句是不够严谨的,我自罚三耳光!

为什么这么说,我们细看下当时的异常堆栈信息

发现了什么?


Mybatis

JDBC
之间有
hikari

hikari
是什么,一个性能极高的数据库连接池,它是可以有自己的想法的!

思维再扩散一点,格局再打开一点,是不是就是:在
Mybatis

JDBC
之间有
数据库连接池

哪些说不用
数据库连接池
的小伙伴,你最好想清楚了再说

用关系型数据库,而不用数据库连接池的项目多吗?仔细回忆回忆

回到框住的第二句,严谨的说法应该是:而是交由下游组件

至于下游组件是
Hikari
,还是
Druid
,亦或是其他的,是不是都囊括进来了?是不是就严谨了?

druid SQLFeatureNotSupportedException

mybatis-plus/issues/6666664
中提到了一个异常:
java.sql.SQLFeatureNotSupportedException

基于
druid 1.1.16
触发的异常

我们调整下代码

1、引入
druid
依赖

2、修改数据源类型(默认的
hikari
可以不用配置
type

数据库数据源就已经切成
druid

我们将
mysql-connector-java
版本调回到最初的
5.1.26

Mybatis-Plus
仍使用
3.1.1

运行
com.qsl.OrderTest#orderListAllTest
,此时的异常是什么,还是
Conversion not supported
for
type java.time.LocalDateTime

吗?


org.springframework.dao.InvalidDataAccessApiUsageException: Error attempting to get column 'pay_time'from result set.  Cause: java.sql.SQLFeatureNotSupportedException
;
null; nested exception is java.sql.SQLFeatureNotSupportedException

at org.springframework.jdbc.support.SQLExceptionSubclassTranslator.doTranslate(SQLExceptionSubclassTranslator.java:
96)
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:
72)
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:
81)
at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:
73)
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:
446)
at com.sun.proxy.$Proxy54.selectList(Unknown Source)
at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:
230)
at com.baomidou.mybatisplus.core.override.MybatisMapperMethod.executeForMany(MybatisMapperMethod.java:
158)
at com.baomidou.mybatisplus.core.override.MybatisMapperMethod.execute(MybatisMapperMethod.java:
76)
at com.baomidou.mybatisplus.core.override.MybatisMapperProxy.invoke(MybatisMapperProxy.java:
62)
at com.sun.proxy.$Proxy60.selectList(Unknown Source)
at com.qsl.OrderTest.orderListAllTest(OrderTest.java:
33)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:
62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:
43)
at java.lang.reflect.Method.invoke(Method.java:
498)
at org.junit.runners.model.FrameworkMethod$
1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:
12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:
47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:
17)
at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:
74)
at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:
84)
at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:
75)
at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:
86)
at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:
84)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:
325)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:
251)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:
97)
at org.junit.runners.ParentRunner$
3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$
1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:
288)
at org.junit.runners.ParentRunner.access$
000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$
2.evaluate(ParentRunner.java:268)
at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:
61)
at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:
70)
at org.junit.runners.ParentRunner.run(ParentRunner.java:
363)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:
190)
at org.junit.runner.JUnitCore.run(JUnitCore.java:
137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:
69)
at com.intellij.rt.junit.IdeaTestRunner$Repeater$
1.execute(IdeaTestRunner.java:38)
at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:
11)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:
35)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:
232)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:
55)
Caused by: java.sql.SQLFeatureNotSupportedException
at com.alibaba.druid.pool.DruidPooledResultSet.getObject(DruidPooledResultSet.java:
1771)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:
62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:
43)
at java.lang.reflect.Method.invoke(Method.java:
498)
at org.apache.ibatis.logging.jdbc.ResultSetLogger.invoke(ResultSetLogger.java:
69)
at com.sun.proxy.$Proxy73.getObject(Unknown Source)
at org.apache.ibatis.type.LocalDateTimeTypeHandler.getNullableResult(LocalDateTimeTypeHandler.java:
38)
at org.apache.ibatis.type.LocalDateTimeTypeHandler.getNullableResult(LocalDateTimeTypeHandler.java:
28)
at org.apache.ibatis.type.BaseTypeHandler.getResult(BaseTypeHandler.java:
81)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.applyAutomaticMappings(DefaultResultSetHandler.java:
521)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.getRowValue(DefaultResultSetHandler.java:
402)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleRowValuesForSimpleResultMap(DefaultResultSetHandler.java:
354)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleRowValues(DefaultResultSetHandler.java:
328)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleResultSet(DefaultResultSetHandler.java:
301)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleResultSets(DefaultResultSetHandler.java:
194)
at org.apache.ibatis.executor.statement.PreparedStatementHandler.query(PreparedStatementHandler.java:
65)
at org.apache.ibatis.executor.statement.RoutingStatementHandler.query(RoutingStatementHandler.java:
79)
at com.baomidou.mybatisplus.core.executor.MybatisSimpleExecutor.doQuery(MybatisSimpleExecutor.java:
67)
at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:
324)
at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:
156)
at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:
109)
at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:
83)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:
147)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:
140)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:
62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:
43)
at java.lang.reflect.Method.invoke(Method.java:
498)
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:
433)
...
39 more

View Code


hikari
对比下

可以看到,差别还是比较大的

从异常堆栈信息,我们得知,调用栈是:
Mybatis
->
数据库连接池
->
mysql-connector-java

当数据库连接池是
druid 1.1.16
时,在调用栈的第二环(
druid
)就异常了

而当数据库连接池是
hikari 3.4.5
时,在调用栈的第三环才异常,而在第二环(
hikari
)并未异常

那就来看看
druid
为何会异常

为何异常

相信看了
都说了能不动就别动,非要去调整,出生产事故了吧
的小伙伴,能够很快定位到关键代码

对,你想的没错,就是从异常堆栈中找关键位置

我们就从
LocalDateTimeTypeHandler.java:38
开始,来看看异常是怎么产生的

打个断点,然后再
debug


F7
,进入
org.apache.ibatis.logging.jdbc.ResultSetLogger#invoke

红框框住的代码,大家能看懂吗?我给大家拆分下

method
信息如下

rs
对象

method.invoke(rs, params);
作用是不是明显了?

就是反射调用
DruidPooledResultSet
的方法:
getObject(String columnLabel, Class<T> type)

跟进去看下该方法的具体实现

哦豁,直接抛出异常
SQLFeatureNotSupportedException

原因是不是找到了:
druid 1.1.16
不支持根据JAVA类型获取列值

如何修复

1、降低
Mybatis
版本


Mybatis
版本降低至
3.5.0
或以下

因为项目中用的是
Mybatis-Plus
,我们将其降至
3.1.0

运行
com.qsl.OrderTest#orderListAllTest
,没异常,结果也正确

此时各个组件的版本:
Mybatis-Plus 3.1.0
(即
Mybatis 3.5.0
),
druid 1.1.16

mysql-connector-java 5.1.26

为何将
Mybatis
的版本调整至
3.5.0
就可以了?

这其实跟
Mybatis 3.5.1

LocalDateTimeTypeHandler.java
的调整有关

Mybatis 3.5.0
依赖下游组件的
getTimestamp(String columnLabel)

getTimestamp(
int
columnIndex)


Mybatis 3.5.1
依赖下游组件的
getObject(
int
columnIndex, Class<T> type)


getObject(String columnLabel, Class<T> type)

2、升级
druid
版本

升级到哪个版本,这个需要看
druid
从哪个版本开始支持
LocalDateTime

根据官方说明,从
1.1.18
开始支持

我们来看下
1.1.18
相较于上一个版本(
1.1.16
,没有
1.1.17
),对
DruidPooledResultSet
调整了什么

我们将
Mybatis-Plus
改回成
3.1.1

Mybatis 3.5.1
),然后将
druid
升级到
1.1.18

再执行
com.qsl.OrderTest#orderListAllTest
,你会发现还是有异常!!!

但是先别慌,该异常

是不是很眼熟?

不是在
都说了能不动就别动,非要去调整,出生产事故了吧
已经解决过了吗?

那怎么修?

有小伙伴会说:这个我会,将
mysql-connector-java
升级到
5.1.37

就不能一步到位升级到
5.1.42
?,
5.1.37

NullPointerException
呀!!!

此时各个组件的版本:
Mybatis-Plus 3.1.1
(即
Mybatis 3.5.1
),
druid 1.1.18

mysql-connector-java 5.1.42

Hikari 为何没问题

此刻相信大家会有一个问题:为何
hikari
没有
druid
的那个问题(
SQLFeatureNotSupportedException

我们来分析下,
hikari
版本是
3.4.5

它的
HikariProxyResultSet
有实现
getObject(
int
columnIndex, Class<T> type)


getObject(String columnLabel, Class<T> type)

具体实现交给了下游,也就是交给了
mysql-connector-java

那它是一开始就是这么实现的,还是在
3.4.5
或之前的某个版本调整成这样的了?

因为
HikariProxyResultSet
是动态生成的,没有现成的源代码,我也想帮你们分析,可我暂时做不到呀

等我了解了
HikariCP
动态代理实现机制,我再来给你们分析,暂时算我欠你们的!

你们要实在是觉的不爽,来打我呀

总结

遇到异常不要害怕,异常堆栈是很有用的信息

遇到开源组件的问题,
github
上搜它的相关
issue
,往往能事半功倍

还是那句话:能不动就不要动,改好没绩效,改出问题要背锅,吃力不讨好,又不是不能跑

前言

EF Core是我们.NET开发中比较常用的一款ORM框架,今天我们分享一款可以直接在Visual Studio中查看EF Core查询计划调试器可视化工具(帮助开发者分析和优化数据库查询性能):EFCore.Visualizer。

值得推荐的.NET ORM框架



对于还不知道怎么选择.NET ORM框架的同学可以看下面这两篇文章,希望对你会有所帮助

前言

cpu使用率100%
问题,是一个让人非常头疼的问题。因为出现这类问题的原因千奇百怪,最关键的是它不是必现的,有可能是系统运行了一段时间之后,在突然的某个时间点出现问题。

今天特地把我和同事,之前遇到过的cpu使用率100%的问题,总结了一下,给有需要的朋友一个参数。

1 一次性获取的数据太多

我之前参与过餐饮相关的业务系统开发,当时我所在的团队是菜品的下游业务。

当时菜品系统有菜品的更新,会发
kafka
消息,我们系统订阅该
topic
,就能获取到最近更新的菜品数据。

同步菜品数据的功能,上线了一年多的时候,没有出现过什么问题。

但在某一天下午,我们收到了大量CPU100%的报警邮件。

追查原因之后发现,菜品系统出现了bug,我们每次获取到的都是全量的菜品数据,并非增量的数据。

一次性获取的数据太多。

菜品修改还是比较频繁的,也就是说我们系统,会频繁的读取和解析大量的数据,导致CPU不断飙升。

其根本原因是频繁的
full gc

2 kafka自动确认

之前我们的餐饮子系统中间,是通过消息中间件:
kafka
进行通信的。

上游系统中产生了数据,写入db之后,然后把相关业务单据的id,通过kafka消息发送到broker上。

下游系统订阅相关topic的消息,获取业务单据的id,然后调用上游系统的业务查询接口,获取相关业务数据。

刚开始为了方便,我们消费订单消息时,kafka的确认机制,使用的是
自动确认
(可以少写点代码)。

刚开始问题不大。

随着业务的发展,用户量越来越多,每天产生的kafka消息也越来越多。

终于开始爆出了cpu使用率100%的问题。

后来,我们把kafka的consumer,消费消息后改成手动确认,cpu使用率100%的问题就被解决了。

3 死循环

在实际工作中,可能每个开发都写过
死循环
的代码。

死循环有两种:

  1. 在while、for、forEach循环中的死循环。
  2. 无限递归。

这两种情况,程序会不停的运行,使用
寄存器
保存
循环次数
或者
递归深度
,一直占用cpu,导致cpu使用率飙升。

在使用JDK1.7时,还有些死循环比如多线程的环境下,往HashMap中put数据,可能会导致
链表
出现
死循环

就会导致cpu不断飙高。

4 多线程导数据

之前我们组有位同事做了一个供应商excel数据导入功能。

该功能上线之后发现excel中数据只要稍微多一点,导入的耗时时间就会很长。

因为导入供应商相关的业务逻辑有些复杂,涉及了多张表,而且是单线程中一条条按顺序导入的。

那位同事为了提升导入数据的性能,将
单线程
导入,改成了使用线程池的
多线程
导入。

这样改造之后,excel数据导入的速度确实提升了很多。

但上线之后,却带来另外一个问题,即:CPU使用率一路飙升。

多线程导入数据,如果线程数量比较多,会存在大量线程
上下文切换
的过程,这个过程非常消耗CPU资源。

最近就业形式比较困难,为了感谢各位小伙伴对苏三一直以来的支持,我特地创建了一些工作内推群, 看看能不能帮助到大家。

你可以在群里发布招聘信息,也可以内推工作,也可以在群里投递简历找工作,也可以在群里交流面试或者工作的话题。

进群方式

添加苏三的私人微信:su_san_java,备注:博客园+所在城市,即可加入。

5 同步大量文件

我之前参与过游戏平台的开发。

游戏厂商的游戏接入我们平台,我们帮他们推广,赚了钱进行分成。

每一款游戏都有一个定制化的官网,域名、图片和样式都不一样。

当时出于性能考虑,我们当时使用了
FreeMarker
模板引擎,为每一款游戏都生成专门的
html
的静态官网。

当时提供了十几个不同的模板,可以给游戏的运营同学选择。

原本是没啥问题的。

但有一次节日活动,为了增加一些喜庆的元素,在每一个模板文件中都加了一些样式。

这就需要把所有游戏的官网,用新的模板重新生成一次了。

生成完毕之后,需要把所有的html文件,一次性同步到web服务器的指定目录下。

由于涉及到了大量文件的同步,导致存放文件的那台应用服务器CPU飙升的很高。

6 死锁

为了防止并发场景中,多个线程修改公共资源,导致的数据异常问题。

很多时候我们会在代码中使用
synchronized
或者
Lock
加锁。

这样多个线程进入临界方法或者代码段时,需要竞争某个对象或者类的锁,只有抢到相应的锁,才能访问临界资源。其他的线程,则需要等待,拥有锁的线程释放锁,下一次可以继续竞争那把锁。

有些业务场景中,某段代码需要线程获取多把锁,才能完成业务逻辑。

但由于代码的bug,或者释放锁的顺序不正确,可能会引起
死锁
的问题。

例如:

"pool-4-thread-1" prio=10 tid=0x00007f27bc11a000 nid=0x2ae9 waiting on condition [0x00007f2768ef9000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for  <0x0000000090e1d048> (a java.util.concurrent.locks.ReentrantLock$FairSync)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:186)

比如线程a拥有锁c,需要获取锁d,才能完成业务逻辑。

而刚好此时线程b拥有锁d,需要获取锁c,也能完成业务逻辑。

线程a等待线程b释放锁,而线程b等待线程a释放锁,两个线程都持有对方需要的锁,无法主动释放,就会出现死锁问题。

死锁会导致CPU使用率飙升。

7 正则匹配

不知道你使用过正则表达式没有?

有时候我们为了验证用户输入的手机号、邮箱、身份证号、网页地址是否合法。

通常情况下,会使用正则表达式,例如:

^([hH][tT]{2}[pP]://|[hH][tT]{2}[pP][sS]://)(([A-Za-z0-9-~]+).)+([A-Za-z0-9-~/])+$

这个正则表达式可以分为三个部分:

  • 第一部分匹配 http 和 https 协议。
  • 第二部分匹配 www. 字符。
  • 第三部分匹配许多字符。

一个写的不好的正则表达式,就可以导致cpu使用率一下子飚升。

其实这里导致 CPU 使用率高的关键原因就是:Java 正则表达式使用的引擎实现是
NFA自动机
,这种正则表达式引擎在进行字符匹配时会发生
回溯

而一旦发生回溯,那其消耗的时间就会变得很长,有可能是几分钟,也有可能是几个小时,时间长短取决于回溯的次数和复杂度。

我们写的正则表达式,要尽量减少回溯。

最近就业形式比较困难,为了感谢各位小伙伴对苏三一直以来的支持,我特地创建了一些工作内推群, 看看能不能帮助到大家。

你可以在群里发布招聘信息,也可以内推工作,也可以在群里投递简历找工作,也可以在群里交流面试或者工作的话题。

进群方式

添加苏三的私人微信:su_san_java,备注:博客园+所在城市,即可加入。

8 耗时计算

有时候,我们的业务系统需要实时计算数据,比如:电商系统中需要实时计算优惠后的最终价格。

或者需要在代码中,从一堆数据中,统计汇总出我们所需要的数据。

如果这个实时计算或者实时统计的场景,是一个非常耗时的操作,并且该场景的请求并发量还不小。

就可能会导致cpu飙高。

因为实时计算需要消耗cpu资源,如果一直计算,就会一直消耗cpu资源。

最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙扫描下发二维码关注一下,您的支持是我坚持写作最大的动力。
求一键三连:点赞、转发、在看。
关注公众号:【苏三说技术】,在公众号中回复:面试、代码神器、开发手册、时间管理有超赞的粉丝福利,另外回复:加群,可以跟很多BAT大厂的前辈交流和学习。