jOOQ 与 Spring 的一些注意事项

jOOQ 全称 Java Object Oriented Querying,即面向 Java 对象查询。它是 Data Geekery 公司研发的 DA 方案 (Data Access Layer),是一个 ORM 框架。

使用 jOOQ,既不像 Hibernate 等框架封装过高,无法触及 SQL 底层;也不像 MyBatis 等,配置太过繁琐。同时还是 Type Safe 的框架,编译时即可最大程度的发现问题。

不过在 jOOQ 配合 String Cloud 使用的时候,还是踩了几个小坑,特别说明一下。随时补充新遇到的问题。

一、事物问题

jOOQ 默认有一套自己的流式 API,来支持事物。不过,在 Spring 里面,我们使用的最多的还是 @EnableTransactionManagement@Transactional 注解。使用这二者可以开启 Spring 内置的基于注解和 Proxy 的事物处理机制,相对更灵活,更优雅。使用起来也更简单。

但是在跟 jOOQ 联动的时候,实际使用却发现事物始终不生效。但是奇怪的是,不论是打断点调试还是加日志,都能发现异常正常抛出了,也被 Spring 正常捕获了,Transaction 的 Rollback 也调用了,但是实际上事物就是没有撤销。

在多次排查 Spring 本身的配置问题后,突然想到问题可能处在 jOOQ 上。经过查找相关文档发现,由于我们的 SQL 都是通过 jOOQ 的 DSLContent 构建并执行的,所以默认情况下并不会受 Spring Transaction Manager 的管理。这里我们需要在配置 jOOQ 的时候,特别配置一下,才能让 @Transactional 注解生效。参考官网的样例,XML 配置如下:

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="
            http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd">

    <!-- This is needed if you want to use the @Transactional annotation -->
    <tx:annotation-driven transaction-manager="transactionManager"/>

    <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close" >
        <!-- These properties are replaced by Maven "resources" -->
       <property name="url" value="${db.url}" />
       <property name="driverClassName" value="${db.driver}" />
       <property name="username" value="${db.username}" />
       <property name="password" value="${db.password}" />
    </bean>

    <!-- Configure Spring's transaction manager to use a DataSource -->
    <bean id="transactionManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>

    <!-- Configure jOOQ's ConnectionProvider to use Spring's TransactionAwareDataSourceProxy,
         which can dynamically discover the transaction context -->
    <bean id="transactionAwareDataSource"
        class="org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy">
        <constructor-arg ref="dataSource" />
    </bean>

    <bean class="org.jooq.impl.DataSourceConnectionProvider" name="connectionProvider">
        <constructor-arg ref="transactionAwareDataSource" />
    </bean>

    <!-- Configure the DSL object, optionally overriding jOOQ Exceptions with Spring Exceptions -->
    <bean id="dsl" class="org.jooq.impl.DefaultDSLContext">
        <constructor-arg ref="config" />
    </bean>
    
    <bean id="exceptionTranslator" class="org.jooq.example.spring.exception.ExceptionTranslator" />
    
    <!-- Invoking an internal, package-private constructor for the example
         Implement your own Configuration for more reliable behaviour -->
    <bean class="org.jooq.impl.DefaultConfiguration" name="config">
        <property name="SQLDialect"><value type="org.jooq.SQLDialect">H2</value></property>
        <property name="connectionProvider" ref="connectionProvider" />
        <property name="executeListenerProvider">
            <array>
                <bean class="org.jooq.impl.DefaultExecuteListenerProvider">
                    <constructor-arg index="0" ref="exceptionTranslator"/>
                </bean>
            </array>
        </property>
    </bean>
    
    <!-- This is the "business-logic" -->
    <bean id="books" class="org.jooq.example.spring.impl.DefaultBookService"/>
</beans>

核心要点在这段:

<!-- Configure jOOQ's ConnectionProvider to use Spring's TransactionAwareDataSourceProxy,
         which can dynamically discover the transaction context -->
    <bean id="transactionAwareDataSource"
        class="org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy">
        <constructor-arg ref="dataSource" />
    </bean>

    <bean class="org.jooq.impl.DataSourceConnectionProvider" name="connectionProvider">
        <constructor-arg ref="transactionAwareDataSource" />
    </bean>

这里要注意,一定要用 Spring 的 TransactionAwareDataSourceProxy 包装一层前面配置的 DataSource 对象。否则,jOOQ 拿到的就是一个没有被托管的原始 DataSource,那么就不会被 @Transactional 注解所管控。

对应的 Java 方式配置要点如下:

package de.maoxian.config;

import javax.sql.DataSource;

import org.jooq.ConnectionProvider;
import org.jooq.DSLContext;
import org.jooq.SQLDialect;
import org.jooq.impl.DataSourceConnectionProvider;
import org.jooq.impl.DefaultConfiguration;
import org.jooq.impl.DefaultDSLContext;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@EnableTransactionManagement
public class DbConfig {
    @Bean
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource.maoxian")
    public DataSource dataSource() {
        return DataSourceBuilder.create().build();
    }

    @Primary
    @Bean
    public DataSourceTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean
    public DSLContext dslContext(@Qualifier("maoxian-jooq-conf") org.jooq.Configuration configuration) {
        return new DefaultDSLContext(configuration);
    }

    @Bean("maoxian-jooq-conf")
    public org.jooq.Configuration jooqConf(ConnectionProvider connectionProvider) {
        return new DefaultConfiguration().derive(connectionProvider).derive(SQLDialect.MYSQL);
    }

    @Bean
    public ConnectionProvider connectionProvider(TransactionAwareDataSourceProxy transactionAwareDataSource) {
        return new DataSourceConnectionProvider(transactionAwareDataSource);
    }

    @Bean
    public TransactionAwareDataSourceProxy transactionAwareDataSourceProxy(DataSource dataSource) {
        return new TransactionAwareDataSourceProxy(dataSource);
    }
}

重点在 ConnectionProvider 的配置。此处的 ConnectionProvider 在创建时,必须使用被 Spring 包过的 DataSource。如果直接使用 DataSource 而不是 TransactionAwareDataSourceProxy 则注解失效。


参考文档:

https://www.jooq.org/doc/latest/manual/getting-started/tutorials/jooq-with-spring/

AWS Lightsail 修改 DB 参数

AWS 推出的 Lightsail Database 是目前比较实惠的托管 DB 方案。不过相对来说,Lightsail 的控制面板功能较少,很多参数无法修改。经过搜索发现,其实我们有很多参数可以调,只是需要通过命令行的方式来调整。

准备工作

安装 aws-cli 工具

https://aws.amazon.com/cli/

https://lightsail.aws.amazon.com/ls/docs/en_us/articles/lightsail-how-to-set-up-and-configure-aws-cli

Linux:

sudo apt-get install awscli

MacOS:

brew install awscli

Python(通用):

pip install awscli

设置 Access Key

https://lightsail.aws.amazon.com/ls/docs/en_us/articles/lightsail-how-to-set-up-access-keys-to-use-sdk-api-cli

先在 AWS 控制台新建用户或 Key:

https://console.aws.amazon.com/iam/home#/users

然后执行:

aws configure

按照提示依次输入:

AWS Access Key ID 控制台中创建的 Key
AWS Secret Access Key 控制台中创建的 Key 对于的 Secret
Default region name 可用区,https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.RegionsAndAvailabilityZones.html
Default output format 输出格式,建议 json

获取现有设置

aws lightsail get-relational-database-parameters --relational-database-name DatabaseName > current_params.json

注意 DatabaseName 替换为创建 DB 时设置的名称。就是 Lightsail 控制面板里显示的那个。

执行成功后打开 json 文件,可以看到所有变量。

注意每个变量有几个属性:

Allowed values 允许的变量范围
Apply method 变量的生效时间。immediate 表示立即生效,pending-reboot 表示重启后生效
Apply type 底层引擎支持的生效方式。dynamic 动态,可以立即生效,static 静态,必须重启后才能生效
Data type 数据类型
Description 变量描述
Is modifiable 能否修改
Parameter name 变量名

这里我们用最大连接数举例:

{
    "allowedValues": "1-100000",
    "applyMethod": "pending-reboot",
    "applyType": "dynamic",
    "dataType": "integer",
    "description": "The number of simultaneous client connections allowed.",
    "isModifiable": true,
    "parameterName": "max_connections",
    "parameterValue": "{DBInstanceClassMemory/12582880}"
}

可以看到,最大连接数是一个动态变量,整型,可修改。默认是实例内存大小/12582880,也就是 1G 内存约 80 个链接。实际比这个数值少,没有具体深究。

修改设置

找到了对于的参数,就可以修改了。修改参数使用的指令是:

aws lightsail update-relational-database-parameters --relational-database-name DatabaseName --parameters "parameterName=ParameterName,parameterValue=NewParameterValue,applyMethod=ApplyMethod"

DatabaseName 是实例名,ParameterName 替换为要修改的变量,NewParameterValue 替换为变量的值,ApplyMethod 替换为想要的生效方式。

比如,我们修改最大连接数到 1000,重启后生效,对应的命令为:

aws lightsail update-relational-database-parameters --relational-database-name DatabaseName --parameters "parameterName=max_connections,parameterValue=1000,applyMethod=pending-reboot"

成功后会收到这样的响应:

{
    "operations": [
        {
            "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
            "resourceName": "DatabaseName",
            "resourceType": "RelationalDatabase",
            "createdAt": 1570000000.000,
            "location": {
                "availabilityZone": "ap-northeast-1a",
                "regionName": "ap-northeast-1"
            },
            "isTerminal": true,
            "operationDetails": "",
            "operationType": "UpdateRelationalDatabaseParameters",
            "status": "Succeeded",
            "statusChangedAt": 1570000000.000
        }
    ]
}

看到 status Succeeded 就是设置成功啦!

官方文档:

https://lightsail.aws.amazon.com/ls/docs/en_us/articles/amazon-lightsail-updating-database-parameters

 

MySQL 低版本对 TIMESTAMP 字段 DEFAULT 值设置的一个问题

今天在线上改表的时候,遇到了一个问题。改表完成之后,MyBatis Select 改表前插入的数据会抛异常。排查后发现是因为一个 TIMESTAMP  类型的字段值被写成了 0000-00-00 00:00:00  ,无法被转换成 java 的 java.sql.Timestamp 类型,导致出错。

但是之前在使用 MySQL 的时候一直没有遇到过这个问题,于是搜索了一下,发现了这么一个链接:

https://bugs.mysql.com/bug.php?id=68040

这里反馈了一个 MySQL 本身的 Bug,会导致 ALTER table  的时候,TIMESTAMP  类型的值没有按照预期的正确设置,而是被设置成了 0000-00-00 00:00:00  。该问题在 MySQL 5.6.11 中被修复了。官方的 Release Note 在这里:

https://dev.mysql.com/doc/relnotes/mysql/5.6/en/news-5-6-11.html#mysqld-5-6-11-bug

  • ALTER TABLE tbl_name ADD COLUMN col_name TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP inserted 0000-00-00 00:00:00 rather than the current timestamp if the alteration was done in place rather than by making a table copy. (Bug #68040, Bug #16076089)

为了确认该问题,特地找了两台不同版本的 MySQL 来尝试一下:

没有问题的版本:

mysql> SHOW VARIABLES LIKE "%version%";
+-------------------------+--------------------+
| Variable_name           | Value              |
+-------------------------+--------------------+
| innodb_version          | 5.6.28             |
| protocol_version        | 10                 |
| slave_type_conversions  |                    |
| version                 | 5.6.28             |
| version_comment         | 20170228           |
| version_compile_machine | x86_64             |
| version_compile_os      | Linux              |
+-------------------------+--------------------+
7 rows in set (0.01 sec)

mysql> ALTER TABLE time_test ADD COLUMN update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;
Query OK, 0 rows affected (0.02 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> SELECT * FROM time_test;
+----+------+---------------------+
| id | name | update_time         |
+----+------+---------------------+
|  1 | 111  | 2018-01-01 00:00:01 |
|  2 | 222  | 2018-01-01 00:00:01 |
+----+------+---------------------+
2 rows in set (0.01 sec)

mysql>

有问题的版本:

mysql> SHOW VARIABLES LIKE "%version%";
+-------------------------+------------------------------+
| Variable_name           | Value                        |
+-------------------------+------------------------------+
| innodb_version          | 1.1.8                        |
| protocol_version        | 10                           |
| slave_type_conversions  |                              |
| version                 | 5.5.24                       |
| version_comment         | MySQL Community Server (GPL) |
| version_compile_machine | x86_64                       |
| version_compile_os      | Linux                        |
+-------------------------+------------------------------+
7 rows in set (0.07 sec)

mysql> ALTER TABLE time_test ADD COLUMN update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;
Query OK, 4 rows affected (0.06 sec)
Records: 4  Duplicates: 0  Warnings: 0

mysql> SELECT * FROM time_test;
+----+------+---------------------+
| id | name | update_time         |
+----+------+---------------------+
|  1 | 111  | 0000-00-00 00:00:00 |
|  2 | 222  | 0000-00-00 00:00:00 |
|  3 | 333  | 0000-00-00 00:00:00 |
|  4 | 444  | 0000-00-00 00:00:00 |
+----+------+---------------------+
4 rows in set (0.09 sec)

mysql>

 

MySQL 分区表的一些问题

最近在使用 MySQL 分区表的时候,研究了一下多列 Range 分区,也就是

PARTITION BY RANGE COLUMNS(`a`, `b`, `c`) (
    PARTITION p1 VALUES LESS THAN (0, 0, MAXVALUE),
    PARTITION p2 VALUES LESS THAN (10, 10, MAXVALUE),
    PARTITION p3 VALUES LESS THAN (20, 20, MAXVALUE)
)

在多列的情况下,MySQL 的分区策略和单列略有不同,这也是比较坑的地方,查遍所有文档都没人提到。。。

先说说单列 Range 分区。比如,如果这么写:

PARTITION BY RANGE(`a`) (
    PARTITION p1 VALUES LESS THAN (0),
    PARTITION p2 VALUES LESS THAN (10),
    PARTITION p3 VALUES LESS THAN (20)
)

那么,p1 中的数据是 a 值小于 0 的,注意,是小于,不包括 0 。然后,p2 中的数据是 a 值在 [0, 10) 之间的,注意右边是开区间,不包括 10 。同样的,p3 中的数据是 a 值在 [10, 20) 之间的,不包括 20 。

也就是说,如果有这么一条数据:

INSERT INTO test_table (`a`, `b`, `c`) VALUES (10,10,20);

由于 a=10,所以会落入 p3 分区。

再来看多列分区,使用第一个多列分区语句,执行 INSERT,会发现,数据插入到了 p2 分区,而不是想象中的 p3 分区。

这里么的原因,就涉及到 MySQL 内部的比较了。当使用单列分区时,MySQL 的比较方法是:

if a < 0  then p1
if a < 10 then p2
if a < 20 then p3

当采用多列分区的时候,比较方法就相应的变成了:

if (a,b,c) < (0 , 0 , MAXVALUE) then p1
if (a,b,c) < (10, 10, MAXVALUE) then p2
if (a,b,c) < (20, 20, MAXVALUE) then p3

那咱们再来看看直接执行这个比较会怎么样:

mysql> SELECT 10 < 10;
+---------+
| 10 < 10 |
+---------+
|       0 |
+---------+
1 row in set (0.01 sec)

mysql> SELECT 9 < 10;
+--------+
| 9 < 10 |
+--------+
|      1 |
+--------+
1 row in set (0.01 sec)

mysql> SELECT (10,10) < (10,10);
+-------------------+
| (10,10) < (10,10) |
+-------------------+
|                 0 |
+-------------------+
1 row in set (0.00 sec)

mysql> SELECT (10,9) < (10,10);
+------------------+
| (10,9) < (10,10) |
+------------------+
|                1 |
+------------------+
1 row in set (0.00 sec)

惊喜来了!(10,10) < (10,10) 毫不意外的被判定为 false ,但是 (10,9) < (10,10) 确是 true 的!

再来一些尝试:

mysql> SELECT (11,9) < (10,10);
+------------------+
| (11,9) < (10,10) |
+------------------+
|                0 |
+------------------+
1 row in set (0.00 sec)

mysql> SELECT (9,11) < (10,10);
+------------------+
| (9,11) < (10,10) |
+------------------+
|                1 |
+------------------+
1 row in set (0.01 sec)

mysql> SELECT (9,10) < (10,10);
+------------------+
| (9,10) < (10,10) |
+------------------+
|                1 |
+------------------+
1 row in set (0.01 sec)

惊呆了,(9,11) < (10,10) 居然也是 true !

来,实际测试一下:

CREATE TABLE `test_table` (
    `a` INT(20) NOT NULL,
    `b` INT(11) NOT NULL
) ENGINE=INNODB DEFAULT CHARSET=UTF8MB4
PARTITION BY RANGE COLUMNS(`a`, `b`) (
    PARTITION p1 VALUES LESS THAN (0, 0),
    PARTITION p2 VALUES LESS THAN (10, 10),
    PARTITION p3 VALUES LESS THAN (20, 20)
);

INSERT INTO `test_table` VALUES (10,10);
INSERT INTO `test_table` VALUES (10,9);
INSERT INTO `test_table` VALUES (9,11);

执行之后发现,第一条记录毫不意外的在 p3 ,但是第二条记录和第三条记录却都在 p2 !

那么这时候执行查询会发生什么呢?

mysql> SELECT * FROM `test_table`;
+----+----+
| a  | b  |
+----+----+
| 10 |  9 |
|  9 | 11 |
| 10 | 10 |
+----+----+
3 rows in set (0.00 sec)

mysql> EXPLAIN PARTITIONS SELECT * FROM `test_table` WHERE a=10;
+------+-------------+------------+------------+------+---------------+------+---------+------+------+-------------+
| id   | select_type | table      | partitions | type | possible_keys | key  | key_len | ref  | rows | Extra       |
+------+-------------+------------+------------+------+---------------+------+---------+------+------+-------------+
|    1 | SIMPLE      | test_table | p2,p3      | ALL  | NULL          | NULL | NULL    | NULL |    3 | Using where |
+------+-------------+------------+------------+------+---------------+------+---------+------+------+-------------+
1 row in set (0.00 sec)

mysql> EXPLAIN PARTITIONS SELECT * FROM `test_table` WHERE b=10;
+------+-------------+------------+------------+------+---------------+------+---------+------+------+-------------+
| id   | select_type | table      | partitions | type | possible_keys | key  | key_len | ref  | rows | Extra       |
+------+-------------+------------+------------+------+---------------+------+---------+------+------+-------------+
|    1 | SIMPLE      | test_table | p1,p2,p3   | ALL  | NULL          | NULL | NULL    | NULL |    5 | Using where |
+------+-------------+------------+------------+------+---------------+------+---------+------+------+-------------+
1 row in set (0.00 sec)

可以看到,当我们使用多列中的第一列做查询时,MySQL 会识别出 p1 分区一定没有数据,所以优化中直接去掉了这个分区,但是搜索了 p2 p3 两个分区。

这也是疑惑点之一,按照 MySQL 的规则,似乎 p2 也一定不会有数据,为啥要搜 p2

再来看下面一个查 b 的语句,会发现根本没有用分区,直接全分区搜索。。。看来 MySQL 也知道可能有一些 b 值并不是存在相应的分区中,需要全表扫描。

具体原因可能需要深入分析 MySQL 源码,这里就先说这么一个需要注意的现象,防止踩坑。。。