HikariCP 与 SQLTimeoutException

最近碰到一个问题:项目的数据库连接池使用的是 HkiariCP,对每个 SQL 语句的执行超时时间设置为 30秒,结果有个 SQL 超时了,抛出异常 SQLTimeoutException,应用层回滚事务时抛出了连接已关闭的异常。但事实上事务却提交了。

写了个简单的代码来模拟生产场景:
在 Spring 的声明式事务内,有一个 insert 操作,然后是一个 update 操作,在数据库客户端执行 select for update 把要更新的行锁住,这样 update 操作就会超时。

多次调试发现 HikariCP 在碰到 SQL 异常时有个检查机制,满足特定条件的异常会直接关闭底层数据库连接,Spring 拿到的是连接的代理,由于连接已关闭,自然没法回滚事务,会碰到连接已关闭异常。

继续阅读

Spring 事务原理与集成 MyBatis 事务管理

1. 事务管理器抽象

一个事务管理器只需要三个基本的能力:获取一个事务、提交事务、回滚事务。

public interface PlatformTransactionManager extends TransactionManager {
    // 获取一个事务
    TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
            throws TransactionException;

    // 提交事务
    void commit(TransactionStatus status) throws TransactionException;

    // 回滚事务
    void rollback(TransactionStatus status) throws TransactionException;
}

DataSourceTransactionManagerAutoConfiguration 配置类导入了数据库事务管理器 DataSourceTransactionManager

2. 事务同步回调钩子

事务同步回调钩子让我们有机会在事务的各个阶段加入一些协调的动作。

public interface TransactionSynchronization extends Flushable {
    /** Completion status in case of proper commit. */
    int STATUS_COMMITTED = 0;

    /** Completion status in case of proper rollback. */
    int STATUS_ROLLED_BACK = 1;

    /** Completion status in case of heuristic mixed completion or system errors. */
    int STATUS_UNKNOWN = 2;

    default void suspend() {}
    default void resume() {}
    default void flush() {}
    default void beforeCommit(boolean readOnly) {}
    default void beforeCompletion() {}
    default void afterCommit() {}
    default void afterCompletion(int status) {}
}

继续阅读

LMAX 架构–笔记

原文 The LMAX Architecture

原文介绍了 LMAX 支持高性能、低延迟的架构,还介绍了 Disruptor 这个框架的设计缘由。

LMAX 用 3Ghz 的 CPU 单线程处理达到 600w TPS,意味着要在 500 个时钟周期内处理完一个事务。(dual-socket quad-core,32GB RAM)

LMAX 整体架构:

Business Logic Processor,BLP

只是简单的 Java 代码,不依赖于任何框架。单线程执行,全内存操作, 顺序地获取输入的消息。

要操作的数据全在内存里。好处有两点:快 和 简化了编程(没有对象/关系映射)。

用 Event Sourcing 事件溯源机制来保证 BLP 的状态是可以重建的,输入事件由 input disruptor 来进行持久化。事件溯源机制可以采用快照的方式来缩短重建需要的时间。

LMAX 采用多个 BLP 同时处理同样的事件(两个在同一个数据中心,第三个在灾备中心),但只有一个 BLP 的输出是有效的。当存活的 processor 失败时,系统切换到另一个。

事件溯源的另一个好处是诊断方便,可以把事件拷贝到开发环境进行重放。

继续阅读

MyBatis Mapper 代理实现数据库调用原理

1. Mapper 代理层执行

Mapper 代理上执行方法调用时,调用被委派给 MapperProxy 来处理。

public class MapperProxy<T> implements InvocationHandler, Serializable {
    private final SqlSession sqlSession;
    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethod> methodCache;

    public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
        this.sqlSession = sqlSession;
        this.mapperInterface = mapperInterface;
        this.methodCache = methodCache;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (Object.class.equals(method.getDeclaringClass())) {
            try {
                return method.invoke(this, args);
            } catch (Throwable t) {
                throw ExceptionUtil.unwrapThrowable(t);
            }
        }
        // 接口里声明的方法,转换为 MapperMethod 来调用
        final MapperMethod mapperMethod = cachedMapperMethod(method);

        // 与 Spring 集成时此处的 sqlSession 仍然 SqlSessionTemplate
        return mapperMethod.execute(sqlSession, args);
    }

    private MapperMethod cachedMapperMethod(Method method) {
        MapperMethod mapperMethod = methodCache.get(method);
        if (mapperMethod == null) {
            mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
            methodCache.put(method, mapperMethod);
        }
        return mapperMethod;
    }
}

MapperMethod 根据 mapperInterface.getName() + "." + method.getName() 从 Configuration 对象里找到对应的 MappedStatement,从而得到要执行的 SQL 操作类型(insert/delete/update/select/flush),然后调用传入的 sqlSession 实例上的相应的方法。

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    if (SqlCommandType.INSERT == command.getType()) {
        // 把参数转换为 SqlSession 能处理的格式
        Object param = method.convertArgsToSqlCommandParam(args);

        // 在 sqlSession 上执行并处理结果
        result = rowCountResult(sqlSession.insert(command.getName(), param));
    } else if (SqlCommandType.UPDATE == command.getType()) {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.update(command.getName(), param));
    ...省略

如果上述方法传入的是 SqlSessionTemplate,那么这些方法调用会被 SqlSessionInterceptor 拦截,加入与 Spring 事务管理机制协作的逻辑,具体可以看这篇文章MyBatis 事务管理,这里不再展开,最终会调用到 DefaultSqlSession 实例上的方法。

2. 会话层的执行过程

SqlSession 里声明的所有方法的第一个参数如果是 String statement,则都是 mapperInterface.getName() + "." + method.getName(),表示要调用的 SQL 语句的标识符。通过它从 configuration 找到 MappedStatement

会话层最主要的逻辑是进行参数的包装,获取对应的 MappedStatement,然后调用持有的 Executor 的方法去执行。

继续阅读

MyBatis Mapper 代理创建过程

本文主要关注与 SpringBoot 集成时的初始化过程。

1. 核心组件

  • Configuration:MyBatis所有的配置信息都保存在Configuration对象之中,配置文件中的大部分配置都会存储到该类中。
  • SqlSession:MyBatis 的顶层 API,表示与数据库交互的会话,完成数据库增删改查操作。
  • Executor:执行器是 MyBatis 调度的核心,负责 SQL 语句的生成和查询缓存的维护。
  • StatementHandler:封装了 JDBC Statement 操作,如设置参数等。
  • ParameterHandler:负责将用户传递的参数转换成 JDBC Statement 所对应的数据类型。
  • ResultSetHandler:负责将 JDBC 返回的 ResultSet 结果集转换成 List 类型的集合。
  • TypeHandler:负责将 Java 数据类型和 jdbc 数据类型之间的映射与转换。

  • MapperFactoryBean:与 Spring 集成时表示一个 Mapper 原型的工厂 bean,用以创建最终的代理。
  • MapperProxy:与一个 mapperInterface 对应,维护了 Map<Method, MapperMethod> 映射。
  • MapperAnnotationBuilder:基于注解驱动的 MappedStatement 解析器,解析接口类的每个方法,封装成 MappedStatement。
  • MappedStatement:维护了一条 <select|update|delete|insert> 节点的封装。
  • SqlSource:负责根据用户传递的 parameterObject 动态生成 SQL 语句,将信息封装成 BoundSql 对象。
  • BoundSql:表示动态生成的 SQL 语句以及相应的参数信息。

整体架构如下图:
mybatis-整体架构

继续阅读

MyBatis 事务管理

1. 运行环境 Enviroment

当 MyBatis 与不同的应用结合时,需要不同的事务管理机制。与 Spring 结合时,由 Spring 来管理事务;单独使用时需要自行管理事务,在容器里运行时可能由容器进行管理。

MyBatis 用 Enviroment 来表示运行环境,其封装了三个属性:

public class Configuration {
    // 一个 MyBatis 的配置只对应一个环境
    protected Environment environment;
    // 其他属性 .....
}

public final class Environment {
    private final String id;
    private final TransactionFactory transactionFactory;
    private final DataSource dataSource;
}

2. 事务抽象

MyBatis 把事务管理抽象出 Transaction 接口,由 TransactionFactory 接口的实现类负责创建。

public interface Transaction {
    Connection getConnection() throws SQLException;
    void commit() throws SQLException;
    void rollback() throws SQLException;
    void close() throws SQLException;
    Integer getTimeout() throws SQLException;
}

public interface TransactionFactory {
    void setProperties(Properties props);
    Transaction newTransaction(Connection conn);
    Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit);
}

Executor 的实现持有一个 SqlSession 实现,事务控制是委托给 SqlSession 的方法来实现的。

public abstract class BaseExecutor implements Executor {
    protected Transaction transaction;

    public void commit(boolean required) throws SQLException {
        if (closed) {
            throw new ExecutorException("Cannot commit, transaction is already closed");
        }
        clearLocalCache();
        flushStatements();
        if (required) {
            transaction.commit();
        }
    }

    public void rollback(boolean required) throws SQLException {
        if (!closed) {
            try {
                clearLocalCache();
                flushStatements(true);
            } finally {
                if (required) {
                    transaction.rollback();
                }
            }
        }
    }

    // 省略其他方法、属性
}

继续阅读

MyBatis 缓存

所有 insert/update/delete 语句都会导致缓存被清除。

1. 一级缓存

一级缓存是针对数据库会话的,用于优化在一次数据库会话里多次执行同样的 SQL。

有两种范围:

  • SESSION:会话级,默认的。
  • STATEMENT:语句级,每次执行完 mapper 中的语句后都清除一级缓存,其实就是禁用这个语句的一级缓存。

如果要在全局更改一级缓存的范围,需要在 MyBatis 的配置文件中设置:

<setting name="localCacheScope" value="STATEMENT"/>

2. 二级缓存

二级缓存是针对 Mapper 级别的,默认是启用的,但生效的话要对每个 Mapper 进行配置,Mapper 里没有配置的不使用二级缓存。

注意:如果在多个 Mapper 中存在对同一个表的操作,那么这几个 Mapper 的缓存数据可能会出现不一致现象。

<!-- 不启用的话在配置文件中指定如下 -->
<settings>
<setting name="cacheEnabled" value="false" />
</settings>


<!-- Mapper 文件里配置 cache 元素以生效 -->
<mapper namespace="...UserMapper">
    <!-- 默认对该 Mapper 文件里的所有查询使用二级缓存 -->
    <cache/>

    <!-- 该select语句不使用缓存 -->
    <select id="selectAbc" useCache="false">
        ...
    </select>
</mapper>

3. 源码实现

继续阅读

Spring 事务钩子

1. 应用场景说明

有多个事务发起的点 E0、E1,都调用到需要在事务里执行的方法 M0、M1,M0、M1 里都可能产生一些逻辑:一些作为事务的一部分执行、一些在事务提交失败时执行、一些在事务成功提交后执行。

2. 自行实现的一个简陋实现

应用命令模式把要根据事务是否提交成功来决定执行的逻辑封装成一个命令,在事务之外来执行。

定义了下面这样一个类,分别用于收集在事务提交成功和失败后执行的逻辑:

public class Actions {
    private List<Runnable> transactionCommitSuccessActions = new ArrayList<Runnable>();
    private List<Runnable> transactionCommitFailedActions = new ArrayList<Runnable>();
}

在开启事务前初始化一个 Actions 对象,然后事务方法调用到的方法都得传递这个对象,在事务方法结束后就可以根据事务是否成功来执行不同的队列里的逻辑。

继续阅读

SpringBoot 启动分析(五) — 上下文的刷新过程

1. SpringApplication.refreshContext

首先来看 SpringApplication 里刷新上下文的逻辑:

private void refreshContext(ConfigurableApplicationContext context) {
    refresh(context);
    if (this.registerShutdownHook) {
        try {
            context.registerShutdownHook();
        } catch (AccessControlException ex) {
            // Not allowed in some environments.
        }
    }
}

protected void refresh(ApplicationContext applicationContext) {
    Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
    ((AbstractApplicationContext) applicationContext).refresh();
}

刷新的逻辑是在 AbstractApplicationContext.refresh 方法完成的,刷新完后注册了 JVM 的关闭回调钩子。

2. AbstractApplicationContext.refresh

继续阅读