SpringBoot+Mybatis多数据源实战:TDengine与MySQL混合查询优化方案

SpringBoot+Mybatis多数据源实战:TDengine与MySQL混搭的物联网数据存储方案 本文针对物联网海量时序数据场景,详细介绍了SpringBoot与Mybatis整合TDengine和MySQL双数据源的实战方案。通过分析关系型数据库在时序数据存储上的瓶颈,阐述了引入时序数据库TDengine的必要性,并提供了完整的多数据源配置、事务管理、特色SQL编写及性能优化指南,旨在构建高性能、低成本的物联网数据存储架构。 阅读详情

1. 为什么我们需要TDengine+MySQL的混合架构?

如果你正在开发一个物联网项目,比如智能工厂的设备监控、智慧农业的传感器网络,或者车联网的数据平台,那你肯定遇到过这个头疼的问题:数据量太大了,而且长得还不太一样。我做了这么多年物联网项目,发现大家最纠结的就是怎么存数据、怎么查数据。业务数据,比如用户信息、设备档案、订单记录,用MySQL这种关系型数据库来存,那是天经地义,增删改查都方便,事务支持也好。但那些源源不断涌进来的传感器数据,温度、湿度、位置、电压,每秒可能产生成千上万条,而且主要是写和查,很少去改和删,这种数据塞进MySQL,时间一长,单表几千万甚至上亿条,查个当天的数据都要等好几秒,系统能不卡吗?

我见过一个真实的案例,一个环境监测系统,几千个传感器每分钟上报一次数据。一年下来,MySQL里一张表就堆了二十多亿条记录。前端页面想查某个设备最近24小时的数据曲线,那个查询进度条转得人心慌,用户体验极差。更别提那种需要跨设备、跨时间范围做聚合分析的场景了,数据库服务器CPU直接飙满,搞不好整个服务都挂掉。这时候,时序数据库(TSDB)的优势就体现出来了。TDengine就是专门为这种场景设计的,它用“一个设备一张表”或者“一类设备一张表”的超级表模型,把数据按时间和设备天然地分区存储。查询时,它可以直接定位到具体设备的子表,效率极高,面对海量时间序列数据,查询速度能比MySQL快几十甚至上百倍。

但是,我们不可能把所有的数据都搬到TDengine里。用户管理、权限配置、设备元信息这些强关联、需要复杂事务的业务数据,还是得放在MySQL里。这就带来了一个很现实的需求:我们的Java应用,也就是那个SpringBoot服务,需要同时连接两个数据库,并且能根据业务逻辑,灵活地从不同的库里去读写数据。你可能会想,那我启动两个服务,一个连MySQL,一个连TDengine,不就行了?理论上可以,但在资源紧张的服务器上(比如很多小公司用的2核4G或4核8G的云服务器),跑两个Java应用,内存占用翻倍,部署和维护成本也上去了。最经济、最优雅的方案,就是在一个SpringBoot应用里,通过配置多数据源,同时驾驭MySQL和TDengine。这就是我们今天要聊的实战方案的核心价值:用一套代码,一个服务进程,搞定两种截然不同的数据库,让它们各司其职,发挥最大效能。

2. 项目搭建与环境准备

2.1 依赖引入与关键配置

万事开头难,我们先从项目的“地基”——pom.xml开始。这里面的依赖项一个都不能错,特别是TDengine的JDBC驱动,版本要对得上你的TDengine服务端。

<dependencies>
    <!-- SpringBoot Web基础 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- Mybatis整合SpringBoot -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.2</version> <!-- 注意版本与SpringBoot匹配 -->
    </dependency>
    <!-- 分页神器PageHelper -->
    <dependency>
        <groupId>com.github.pagehelper</groupId>
        <artifactId>pagehelper-spring-boot-starter</artifactId>
        <version>1.4.1</version>
    </dependency>
    <!-- TDengine官方JDBC驱动 -->
    <dependency>
        <groupId>com.taosdata.jdbc</groupId>
        <artifactId>taos-jdbcdriver</artifactId>
        <version>3.2.8</version> <!-- 请务必与服务器版本一致 -->
    </dependency>
    <!-- MySQL驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <!-- 数据库连接池(这里用HikariCP,SpringBoot默认) -->
    <!-- 如果想用Druid,可以引入下面的依赖并调整配置 -->
    <!--
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.2.16</version>
    </dependency>
    -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

这里有个小坑我踩过:TDengine的JDBC驱动版本(比如3.2.8)一定要和服务器上安装的TDengine版本兼容。如果不确定,可以去TDengine的官方GitHub仓库查看版本对应关系。用错了版本,连接的时候会报一些莫名其妙的错误。

接下来是application.yml(我喜欢用yml,比properties更清晰)。这里我们要配置两套数据源,分别指向MySQL和TDengine。

server:
  port: 8080

spring:
  datasource:
    # MySQL 数据源配置
    mysql:
      jdbc-url: jdbc:mysql://你的MySQL地址:3306/你的业务数据库?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
      username: 你的用户名
      password: 你的密码
      driver-class-name: com.mysql.cj.jdbc.Driver
      hikari:
        connection-timeout: 30000
        maximum-pool-size: 10
        minimum-idle: 5
    # TDengine 数据源配置
    taos:
      jdbc-url: jdbc:TAOS://你的TDengine地址:6030/你的时序数据库?charset=UTF-8&locale=en_US.UTF-8&timezone=UTC-8
      username: root
      password: taosdata # 默认密码,生产环境一定要改!
      driver-class-name: com.taosdata.jdbc.TSDBDriver
      hikari:
        connection-timeout: 30000
        maximum-pool-size: 15 # TDengine查询可能更频繁,连接池可以稍大
        minimum-idle: 5

# MyBatis配置
mybatis:
  # 注意:这里配置的type-aliases-package和mapper-locations是全局默认的。
  # 在多数据源场景下,我们通常会在各自的配置类里单独指定,所以这里可以不配,或者配一个公共的。
  # 如果配置了,要确保不会引起冲突。
  type-aliases-package: com.yourdomain.data.domain
  configuration:
    map-underscore-to-camel-case: true # 下划线转驼峰,强烈建议开启

# PageHelper分页插件配置
pagehelper:
  helper-dialect: mysql # 默认方言,对于TDengine的分页需要特殊处理,后面会讲
  reasonable: true
  support-methods-arguments: true

配置写完了,记得先别急着启动。去检查一下你的MySQL和TDengine服务是不是都正常跑着,网络能不能通。特别是TDengine,默认端口是6030,记得在服务器防火墙放行这个端口。

2.2 数据库与表结构设计

两边数据库的库和表都得先建好。MySQL这边很简单,就是常规的业务表。我们假设有个boot_user用户表。

CREATE DATABASE IF NOT EXISTS `boot_example` DEFAULT CHARACTER SET utf8mb4;

USE `boot_example`;

CREATE TABLE `boot_user` (
  `user_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
  `user_name` varchar(50) NOT NULL COMMENT '用户名',
  `user_nickname` varchar(50) DEFAULT NULL COMMENT '用户昵称',
  `user_password` varchar(100) NOT NULL COMMENT '密码',
  `user_sex` tinyint(1) DEFAULT '0' COMMENT '性别(0未知,1男,2女)',
  `user_create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

TDengine这边就有点讲究了。TDengine的核心概念是超级表和子表。超级表是模板,定义了表结构和标签;每个具体的设备会对应一张子表,子表继承超级表的结构,并拥有自己唯一的标签值。

假设我们要存储设备的环境数据(温湿度),先创建超级表device_env_imei:

-- 在TDengine客户端执行
CREATE DATABASE IF NOT EXISTS td_device_log KEEP 365 DAYS 10 BLOCKS 6;
USE td_device_log;

-- 创建超级表
CREATE STABLE IF NOT EXISTS device_env_imei (
    ts TIMESTAMP, -- 时间戳,TDengine的主键必须是时间戳
    imei NCHAR(64), -- 设备IMEI,虽然标签里也有,但字段里可以再存一份
    temp FLOAT, -- 温度
    hum FLOAT -- 湿度
) TAGS (imei_point NCHAR(64)); -- 标签,这里用imei_point来标识设备

-- 注意:我们不需要手动为每个设备创建子表。
-- 当用INSERT语句插入数据,并指定TAGS值时,TDengine会自动创建对应的子表。
-- 例如:INSERT INTO imei_001 USING device_env_imei TAGS ('001') VALUES (now, '001', 25.5, 60.0);

这种设计妙在哪里?想象一下,你有10万个设备。在MySQL里,你可能得建一张大宽表,里面有device_id, ts, temp, hum等字段,然后所有设备的数据都往里塞,索引会变得无比庞大。在TDengine里,相当于自动为每个设备(imei_point='001')创建了独立的子表imei_001。查询某个设备的数据时,数据库能直接定位到那张小表,速度自然飞快。而且,TDengine针对时间序列数据做了大量压缩优化,存储空间也能节省不少。

3. 多数据源配置的核心代码

这是整个方案最核心、也最容易出错的部分。我们需要明确地告诉Spring:我有两个数据源,一个叫mysqlDataSource,一个叫taosDataSource,它们对应的MyBatis会话工厂、事务管理器都要分开。

3.1 MySQL数据源配置

我们先配置主数据源,通常把MySQL设为主数据源(@Primary),因为业务操作可能更频繁。

package com.yourdomain.data.config;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
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.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;

import javax.sql.DataSource;

@Configuration
// 指定这个数据源对应的Mapper接口包路径和使用的SqlSessionFactory
@MapperScan(basePackages = "com.yourdomain.data.dao.mysql",
           sqlSessionFactoryRef = "mysqlSqlSessionFactory")
public class DataSourceMysqlConfig {

    // 1. 创建数据源Bean,读取spring.datasource.mysql前缀的配置
    @Bean(name = "mysqlDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.mysql")
    @Primary // 标记为主数据源
    public DataSource mysqlDataSource() {
        return DataSourceBuilder.create().build();
    }

    // 2. 创建SqlSessionFactory,并绑定到上面的数据源
    @Bean(name = "mysqlSqlSessionFactory")
    @Primary
    public SqlSessionFactory mysqlSqlSessionFactory(
            @Qualifier("mysqlDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource); // 注入数据源

        // 设置Mapper XML文件的位置
        bean.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources("classpath:mybatis/mysql/mapper/*.xml"));

        // 可选:如果你有独立的MyBatis全局配置文件
        // bean.setConfigLocation(new PathMatchingResourcePatternResolver()
        //        .getResource("classpath:mybatis/mybatis-config.xml"));

        return bean.getObject();
    }

    // 3. 创建事务管理器,同样绑定到MySQL数据源
    @Bean(name = "mysqlTransactionManager")
    @Primary
    public DataSourceTransactionManager mysqlTransactionManager(
            @Qualifier("mysqlDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

这里有几个关键点:

  1. @Primary注解:当Spring容器中存在多个同类型的Bean(比如两个DataSource)时,被标记为@Primary的Bean会被优先注入。我们把MySQL相关的Bean都标记为主,避免一些自动装配的歧义。
  2. @MapperScan的sqlSessionFactoryRef属性:这明确指定了扫描到的com.yourdomain.data.dao.mysql包下的Mapper接口,要使用我们这里定义的mysqlSqlSessionFactoryBean。这是实现Mapper与数据源绑定的关键。
  3. 事务管理器TransactionManager:每个数据源都需要自己独立的事务管理器。这样我们才能在Service层的方法上,通过@Transactional(transactionManager = “mysqlTransactionManager”)来指定使用哪个数据源的事务。

3.2 TDengine数据源配置

TDengine的配置几乎是一样的套路,只是去掉@Primary,并指向不同的配置前缀和路径。

package com.yourdomain.data.config;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
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.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;

import javax.sql.DataSource;

@Configuration
@MapperScan(basePackages = "com.yourdomain.data.dao.taos",
           sqlSessionFactoryRef = "taosSqlSessionFactory")
public class DataSourceTaosConfig {

    @Bean(name = "taosDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.taos")
    public DataSource taosDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "taosSqlSessionFactory")
    public SqlSessionFactory taosSqlSessionFactory(
            @Qualifier("taosDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        // 注意:这里Mapper XML的路径是`taos/mapper`,与MySQL的`mysql/mapper`分开
        bean.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources("classpath:mybatis/taos/mapper/*.xml"));
        return bean.getObject();
    }

    @Bean(name = "taosTransactionManager")
    public DataSourceTransactionManager taosTransactionManager(
            @Qualifier("taosDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

划重点:一定要把两个数据源的Mapper接口(BootUserMapper和DeviceEnvImeiMapper)和XML文件(BootUserMapper.xml和DeviceEnvImeiMapper.xml)放在不同的包路径下(dao.mysql vs dao.taos, mapper/mysql vs mapper/taos)。这样@MapperScan才能正确地将它们分别引导到对应的SqlSessionFactory上。如果混在一起,Spring就懵了,不知道该用哪个工厂来创建这些Mapper的代理对象。

4. 分页查询的优化实战

分页是业务系统的高频操作,但在多数据源和异构数据库环境下,需要一些特别的处理。

4.1 MySQL分页:使用PageHelper的便利

对于MySQL,我们可以直接用PageHelper这个神器,它通过拦截器机制,能非常方便地实现物理分页。

首先,在Service层使用PageHelper:

@Service
public class BootUserServiceImpl implements BootUserService {
    @Autowired
    private BootUserMapper bootUserMapper; // 这个Mapper会自动使用mysqlSqlSessionFactory

    @Override
    public PageInfo<BootUser> selectUserListByPage(int pageNum, int pageSize) {
        // 关键就这一行!这行代码后面的第一个MyBatis查询方法会被自动分页。
        PageHelper.startPage(pageNum, pageSize);
        List<BootUser> userList = bootUserMapper.selectUserList();
        // 用PageInfo包装结果,里面包含了分页的详细信息(总条数、总页数等)
        return new PageInfo<>(userList);
    }
}

对应的Mapper XML里,就是普通的查询SQL,不需要写LIMIT:

<select id="selectUserList" resultMap="userResultMap">
    SELECT user_id, user_name, user_nickname
    FROM boot_user
    ORDER BY user_create_time DESC
</select>

PageHelper会在执行这个SQL前,自动根据数据库方言(我们在配置里指定了helper-dialect: mysql)重写SQL,加上LIMIT ?, ?。非常省心。但是记住,PageHelper.startPage(pageNum, pageSize)必须紧跟在查询方法调用之前,且只对紧随其后的第一个查询生效。

4.2 TDengine分页:手动计算Offset

到了TDengine这里,情况就不同了。TDengine虽然有LIMIT和OFFSET语法,但PageHelper的MySQL方言拦截器对它不生效。所以我们需要手动计算分页参数。

首先,我们通常需要两个查询:一个查总数,一个查分页数据。

Mapper接口定义:

public interface DeviceEnvImeiMapper {
    // 查询某个设备在时间范围内的数据总条数
    int countByImeiAndTimeRange(@Param("imei") String imei,
                                 @Param("startTime") Long startTime,
                                 @Param("endTime") Long endTime);
    // 分页查询数据
    List<DeviceEnvImei> selectByImeiAndTimeRangeWithPage(@Param("imei") String imei,
                                                          @Param("startTime") Long startTime,
                                                          @Param("endTime") Long endTime,
                                                          @Param("offset") int offset,
                                                          @Param("limit") int limit);
}

Mapper XML实现:

<!-- 查询总数 -->
<select id="countByImeiAndTimeRange" resultType="int">
    SELECT COUNT(*)
    FROM imei#{imei} <!-- 动态表名,对应子表 -->
    WHERE ts >= #{startTime} AND ts < #{endTime}
</select>

<!-- 分页查询数据 -->
<select id="selectByImeiAndTimeRangeWithPage" resultMap="deviceEnvResultMap">
    SELECT ts, imei, temp, hum
    FROM imei#{imei}
    WHERE ts >= #{startTime} AND ts < #{endTime}
    ORDER BY ts DESC
    LIMIT #{limit} OFFSET #{offset} <!-- TDengine原生分页语法 -->
</select>

Service层的逻辑就需要我们手动组装了:

@Service
@Transactional(transactionManager = "taosTransactionManager") // 指定TDengine的事务管理器
public class DeviceEnvImeiServiceImpl implements DeviceEnvImeiService {
    @Autowired
    private DeviceEnvImeiMapper deviceEnvImeiMapper;

    @Override
    public PageInfo<DeviceEnvImei> queryDeviceDataWithPage(String imei, Long startTime, Long endTime, int pageNum, int pageSize) {
        // 1. 计算offset
        int offset = (pageNum - 1) * pageSize;
        // 2. 查询总数
        int total = deviceEnvImeiMapper.countByImeiAndTimeRange(imei, startTime, endTime);
        // 3. 查询分页数据
        List<DeviceEnvImei> list = deviceEnvImeiMapper.selectByImeiAndTimeRangeWithPage(
                imei, startTime, endTime, offset, pageSize);
        // 4. 构造PageInfo对象(为了保持接口返回格式统一)
        PageInfo<DeviceEnvImei> pageInfo = new PageInfo<>(list);
        pageInfo.setPageNum(pageNum);
        pageInfo.setPageSize(pageSize);
        pageInfo.setTotal(total);
        // 计算总页数
        int pages = (int) Math.ceil((double) total / pageSize);
        pageInfo.setPages(pages);
        return pageInfo;
    }
}

优化点:对于TDengine,查询总数COUNT(*)在数据量极大时也可能有开销。如果业务上对总条数不要求绝对精确,或者可以接受延迟计算,可以考虑缓存总数,或者在前端实现“加载更多”的模式,而不是传统的页码分页。

4.3 混合查询场景:业务关联时序数据

更复杂的场景来了:前端要展示一个设备列表,每条设备信息后面要附带其最新的传感器读数。这需要先查MySQL拿到设备基本信息,再根据设备ID(如IMEI)去TDengine里查每个设备的最新一条数据。

一种低效的做法(N+1查询):

List<Device> deviceList = deviceMapper.selectAllFromMySQL(); // 1次查询
for (Device device : deviceList) {
    DeviceEnv latestEnv = deviceEnvMapper.selectLatestOneFromTDengine(device.getImei()); // N次查询
    device.setLatestEnv(latestEnv);
}

推荐的高效做法:批量查询。

  1. 先从MySQL查出所有设备ID列表。
  2. 将这个ID列表作为条件,向TDengine发起一次查询,利用TDengine的子表查询能力,一次性获取所有设备的最新数据。
  3. 在Java内存中,将TDengine返回的数据按设备ID映射到对应的设备对象上。

TDengine的SQL可以这样写(假设我们需要每个设备最近1小时的最新数据):

SELECT last_row(*) FROM device_env_imei
WHERE imei_point IN ('device001', 'device002', 'device003')
  AND ts >= now - 1h
GROUP BY imei_point;

这个查询会按imei_point标签分组,并返回每个分组最新的一条记录。在Service层,我们只需要执行一次TDengine查询,就能拿到所有设备的最新状态,性能提升非常明显。

5. 事务管理与资源控制技巧

5.1 多数据源下的事务陷阱

在单数据源下,我们用一个@Transactional注解就能搞定事务。但在多数据源下,你必须明确告诉Spring,当前方法要用哪个数据源的事务管理器。

@Service
public class MixedService {
    @Autowired
    private BootUserMapper bootUserMapper; // 使用MySQL数据源
    @Autowired
    private DeviceEnvImeiMapper deviceEnvImeiMapper; // 使用TDengine数据源

    // 错误示例:这样注解,事务只会管理MySQL的操作,TDengine的操作不在其事务内。
    // @Transactional
    // public void updateUserAndInsertLog(User user, DeviceLog log) {
    //     bootUserMapper.update(user); // MySQL操作
    //     deviceEnvImeiMapper.insert(log); // TDengine操作,如果失败,MySQL的update不会回滚!
    // }

    // 正确做法:使用支持分布式事务的框架,或者避免跨库事务。
    // 方案一:拆分成两个方法,各自使用独立的事务。
    @Transactional(transactionManager = "mysqlTransactionManager")
    public void updateUser(User user) {
        bootUserMapper.update(user);
    }

    @Transactional(transactionManager = "taosTransactionManager")
    public void insertDeviceLog(DeviceLog log) {
        deviceEnvImeiMapper.insert(log);
    }

    // 外层调用
    public void businessOperation(User user, DeviceLog log) {
        updateUser(user);
        insertDeviceLog(log); // 如果这里失败,user的更新已经提交,无法回滚。
        // 对于强一致性要求,需要引入更复杂的补偿机制(如Saga)或使用Seata等分布式事务框架。
    }
}

核心原则:MySQL和TDengine之间无法实现强一致的分布式事务。TDengine本身对事务的支持也有限(主要针对单行插入的原子性)。所以,在设计业务时,要尽量让一个业务单元内的操作落在同一个数据库上。如果必须跨库,就要考虑最终一致性,通过日志、消息队列或定期对账来保证数据最终正确。

5.2 连接池与资源占用优化

在资源有限的服务器上,连接池的配置至关重要。配置不当,要么连接不够用导致请求排队,要么连接太多拖垮数据库。

HikariCP配置建议: 在application.yml中,我们已为两个数据源分别配置了Hikari。这里再强调几个关键参数:

  • maximum-pool-size:最大连接数。不要设置过大,特别是对于TDengine,每个连接都会消耗服务端资源。根据应用的实际并发量和数据库的处理能力来定,通常10-20个足够了。可以通过监控SHOW CONNECTIONS;(TDengine)或SHOW PROCESSLIST;(MySQL)来观察。
  • minimum-idle:最小空闲连接。保持几个空闲连接可以快速响应请求,但不宜过多。
  • connection-timeout:获取连接的超时时间。如果连接池耗尽,新的请求会等待这个时间,超时则抛异常。设置一个合理的值(如30秒),避免线程长时间挂起。
  • idle-timeout和max-lifetime:连接的空闲超时和最大生命周期。定期回收连接,可以防止网络中断导致的“僵尸连接”。

监控与诊断:

  1. 应用侧:启用SpringBoot Actuator的/actuator/metrics/hikaricp.connections.*端点,监控连接池状态。
  2. MySQL侧:定期检查SHOW STATUS LIKE 'Threads_connected';和SHOW VARIABLES LIKE 'max_connections';,确保连接数在健康范围内。
  3. TDengine侧:在taos客户端使用SHOW CONNECTIONS;查看当前连接。如果发现连接数异常增长,要检查应用是否有连接泄露(比如忘了关闭ResultSet、Statement)。

一个常见的坑是:在TDengine的Mapper XML中,如果使用了动态SQL(如<if>标签)且条件不满足,可能会导致生成的SQL语句在TDengine语法下有问题(比如WHERE后面直接跟ORDER BY),从而执行失败。虽然MyBatis会抛出异常,但如果事务配置不当或异常未被捕获,可能会导致连接没有正确返回到连接池。因此,务必确保Service层有健全的异常处理逻辑。

6. 避坑指南与性能调优

6.1 常见配置陷阱

  1. MyBatis全局配置冲突:在application.yml中配置了mybatis.config-location,又在DataSourceConfig的SqlSessionFactoryBean里设置了setConfigLocation。这可能会导致其中一个配置覆盖另一个,或者出现意想不到的行为。建议:在多数据源场景下,放弃全局的mybatis.config-location配置,将配置(如驼峰映射、插件等)分别设置到每个SqlSessionFactoryBean上,或者使用Java Config方式配置。
  2. Mapper XML路径扫描重复或遗漏:确保DataSourceMysqlConfig中setMapperLocations扫描的路径classpath:mybatis/mysql/mapper/*.xml,与DataSourceTaosConfig中扫描的路径classpath:mybatis/taos/mapper/*.xml没有重叠。如果XML文件放错了目录,对应的Mapper接口就找不到SQL语句。
  3. TDengine驱动版本不匹配:这是最头疼的问题之一。客户端驱动版本与服务端版本不兼容,可能报“invalid connection”、“unsupported feature”等错误。务必保持版本一致,最好使用TDengine官方文档推荐的版本组合。
  4. 时区问题:在jdbc-url中,我们指定了timezone=UTC-8。如果你的应用服务器、MySQL服务器、TDengine服务器不在同一个时区,时间戳就可能出现错乱。最佳实践:所有服务器统一使用UTC时间,在应用层按需转换为本地时间显示。

6.2 查询性能优化

  1. TDengine查询优化:

    • 利用时间分区:TDengine按时间自动分区。查询时尽量带上明确的时间范围(WHERE ts >= 'xxx' AND ts < 'xxx'),这能帮助数据库快速定位数据块,避免全表扫描。
    • 善用标签过滤:WHERE条件中优先使用TAG列进行过滤,效率远高于普通数据列。例如WHERE imei_point = 'device001'。
    • 避免SELECT *:只查询需要的列。TDengine是列式存储,查询特定列比查询所有列效率更高。
    • 子查询与JOIN谨慎使用:TDengine对子查询和JOIN的支持和优化不如传统关系型数据库,复杂关联查询建议在应用层内存中处理,或者重新设计数据模型。
  2. 混合查询优化:

    • 异步与并行:对于不相关的MySQL查询和TDengine查询,可以考虑使用Spring的@Async注解或CompletableFuture让它们并行执行,减少接口总响应时间。
    • 缓存策略:对于不常变化的MySQL业务数据(如设备信息、用户信息),积极使用Redis等缓存。对于TDengine的聚合查询结果(如某设备过去24小时的平均温度),如果实时性要求不高,也可以定时计算后缓存起来。
    • 批处理:向TDengine写入数据时,务必使用批插入(INSERT INTO table1 VALUES (?,?,?), (?,?,?)...),单条插入的性能极差。MyBatis的foreach标签可以很好地支持批量插入。

6.3 写在最后:架构思考

折腾完这一套配置,代码跑起来,两个数据库各司其职,是不是感觉挺有成就感?但我最后想分享一点比技术实现更重要的东西:架构的简洁性。

这个方案的核心思想是“让专业的数据库做专业的事”。TDengine负责海量时序数据的写入和高性能查询,MySQL负责关系型业务数据的强一致事务处理。它们通过一个统一的SpringBoot应用来协调。这种架构在中小型物联网项目中非常实用,平衡了性能、成本和开发复杂度。

但是,随着业务规模扩大,你可能会遇到新的挑战:比如需要跨MySQL和TDengine做复杂的关联分析,或者数据量进一步爆炸。那时候,你可能需要考虑引入数据仓库(如ClickHouse)、流处理平台(如Flink)或者更专业的分析型数据库。不过,那都是后话了。在项目早期,采用我们今天讨论的SpringBoot+Mybatis多数据源整合TDengine和MySQL,无疑是一个快速、稳健的起点。它能帮你扛住初期的数据压力,把精力集中在业务逻辑开发上,而不是天天和数据库慢查询作斗争。

SpringBoot项目里同时用MySQL和TDengine?一个实战案例带你搞定多数据源配置 本文通过智能家居数据平台案例,详细讲解如何在SpringBoot项目中配置MySQL和TDengine多数据源。涵盖混合架构设计原理、项目初始化依赖配置、双数据源精细化设置及实战中的疑难解决方案,帮助开发者高效处理结构化业务数据和海量时序数据。 阅读详情

相关推荐

SpringBoot+Mybatis整合TDengine数据库和Mysql数据库的Demo

记录一下使用SpringBoot+Mybatis整合TDengine数据库和Mysql数据库的Demo,并且同时支持分页返回查询数据

蚂蚁舞 2376

写给 MySQL 开发者的 TDengine 入门指南

事实证明,在时序数据场景下,无论是在存储空间、写入速度还是查询性能等各方面,TDengine 都存在数量级优势。

涛思数据 6370

别再手动配Bean了!用SpringBoot 2.7+轻松搞定MySQL与TDengine双数据源(Druid连接池版)

本文详细介绍了如何使用SpringBoot 2.7+和Druid连接池快速配置MySQL与TDengine双数据源,实现高效协同。通过智能化的YAML配置和自动装配技术,大幅减少样板代码,并提供生产级监控与调优建议,帮助开发者轻松应对多数据源场景。

weixin_28366053的博客 128

tdengine和mysql区别_TDEngine Game Framework

软件简介tdengine是一个轻量级的为在线游戏服务器打造的框架。他是由Rust为底层,lua为逻辑层的一个框架。框架依赖于数据库Redis及Mysql。Build tdengine非常简单,动手编译一个试着玩一下是个很好的开始。 您需要下载Rust选择相应的版本进行安装。 用cargobuild就可以完成编译,Windows环境如果缺少gcc的话,请安装msys2并设置环境变量。虽然 tdeng...

weixin_35618539的博客 1124

时序数据库 TDengine 到 MySQL 数据迁移同步

TDengine 适用于高吞吐的时序数据存储与查询,而 MySQL 适用于事务处理和复杂查询。使用CloudCanal进行 TDengine 到 MySQL 数据迁移同步,能够兼顾时序数据的高效存储与业务数据的灵活分析,从而实现时序数据价值的最大化。创建任务异步任务修改任务参数。

Kami‘s home 1818

避坑指南:SpringBoot整合TDengine3.2.8时Mybatis多数据源的那些坑

本文详细解析了SpringBoot整合TDengine3.2.8与MySQL多数据源配置中的常见问题,包括JDBC驱动差异、MyBatis多数据源隔离、事务管理、分页查询优化等关键场景。通过实战案例和对比表格,帮助开发者避开配置陷阱,提升物联网数据处理效率。特别针对TDengine的时区设置、分页性能等特性提供专业解决方案。

weixin_30872337的博客 246

mysql和tdengine_TDengine 与 MySQL插入数据的速度对比

注:以下图片均来自我的微信公众号“生锈的TRUENO”,欢迎各位关注!TDengine的卖点之一就是插入和查询速度要比其他数据库快10倍: 那么,我们今天就拿MySQL和它做一下对比 对比思路:写两个python脚本,每个脚本都先从CSV文件中读取若干条数据,把这些数据存入一个变量中,然后遍历这个变量,把其中的每一条记录打上时间戳插入MySQL和TDengine,并在数据插入前记录起始时间,在数据...

weixin_31146939的博客 897

从零到一:用Verilog和Quartus构建智能交通灯系统的实战指南

本文详细介绍了如何使用Verilog和Quartus Prime从零开始构建智能交通灯系统。通过分步指导,包括状态机设计、时序控制、数码管显示等核心模块实现,并结合ModelSim仿真验证,帮助FPGA初学者掌握数字电路设计的关键技术。文章还提供了实际部署到FPGA开发板的调试技巧和进阶优化方案。

weixin_29172201的博客 473

TDengine 与 MYSQL 的差异总结

- 创建用户定义函数-- 使用UDF-- 删除UDF-- MySQL 存储过程BEGIN-- 自定义逻辑END //DELIMITER;功能类别TDengineMySQL主要差异点数据模型超级表/子表模型单表模型TDengine 支持模板化表结构时序特性内置时间窗口、插值需要复杂SQL实现TDengine 原生支持时序函数写入方式多表批量写入、自动建表标准INSERT语句TDengine 支持更灵活的批量操作连接类型ASOF、窗口连接标准连接。

TDengine(老段)专注时序数据库领域 1420

Moltbot 超详细安装使用教程(初学者版)

你在外面逛街,用微信/Telegram 发消息说"帮我查一下明天的天气"它立刻帮你查好并回复你说"帮我在电脑上新建一个文件夹叫'工作文档'"它真的帮你在电脑上创建了这就是 Moltbot ——一个能真正帮你干活的 AI 助手。API Key 就像是一把"钥匙",让 Moltbot 能够使用 AI 服务。

weixin_51040479的博客 2013

告别版本冲突!在Ubuntu 20.04上优雅实现ROS Noetic与ROS2 Foxy双环境切换

本文详细介绍了在Ubuntu 20.04系统上实现ROS Noetic(ROS1)与ROS2 Foxy双环境共存与切换的完整方案。通过智能脚本设计和工作空间隔离,解决了版本冲突问题,帮助开发者高效管理不同ROS版本项目。特别适合需要同时开发ROS1和ROS2应用的机器人开发者。

weixin_28452161的博客 407

SpringBoot多数据源配置(mysql和TDengine)超详细

【代码】SpringBoot多数据源配置(mysql和TDengine)超详细。

李猿猿的博客 2542

TJA1445/TJA1465 CAN收发器硬件设计与软件配置实战指南

控制器局域网(CAN)总线是汽车电子和工业控制领域实现电子控制单元(ECU)间可靠通信的核心技术。其工作原理是将数字信号转换为差分信号进行抗干扰传输,技术价值在于构建稳定、高效的分布式实时控制系统。随着汽车电子架构向域集中式演进,对CAN总线的通信速率、网络复杂度和静态功耗提出了更高要求,这催生了支持CAN FD协议和智能电源管理的新一代高速CAN收发器。在车载网关、域控制器及工业网络节点等应用场景中,NXP的TJA1445和TJA1465系列收发器凭借其部分组网(Partial Networking)和选

weixin_29655587的博客 405

mysql和tdengine_JDBC连接本地TDengine详解(以IntelliJ IDEA为例)

前言:TDengine语法与mysql语句基本一致,也提供了丰富的应用程序开发接口,其中包括C/C++、JAVA、Python、RESTful、Go等,便于用户快速开发应用。本文主要展示JDBC连接TDengine的详细配置,连接完成后,即可使用与mysql Java-Connector一样的Java语句对TDengine进行各种操作。前几天在连接时,因为自己Java和mysql语句基础薄弱 ,踩...

weixin_29910561的博客 1157

利用GBD数据库做未来疾病负担预测

最近看到一篇文献,利用GBD数据库中的数据对疾病负担的发展趋势进行预测。觉得很有趣,就抽空用R语言复盘了一下里面关键的方法。 参考文献: Changing trends in the disease burden of esophageal cancer in China from 1990 to 2017 and its predicted level in 25 years DOI: 10.1002/cam4.3775 1. 相关R包的安装 主要用到的R包有3个,一个一个讲: 1.1 Nordpred

NickyCat的博客 5万+

PX4固件下VTOL固定翼的3种起飞方式对比:手抛vs弹射vs跑道

本文深度解析PX4固件下VTOL固定翼的三种起飞方式:手抛、弹射与跑道起飞,对比其技术差异与适用场景。通过详细的参数配置和工程实践指南,帮助开发者优化起飞性能,提升无人机在不同环境下的适应性和可靠性。文章特别针对PX4飞控系统的最新特性进行了技术探讨。

weixin_30296405的博客 87

mysql与TDengine数据库性能对比

前言 mysql是我平时使用最多的数据库,最近看 TDengine 比较火爆,就想着对比一下这两个数据库的增删改查的性能怎么样。 版本号 mysql: 5.7.13 taosd: 2.1.1.0 测试语言:go mysql: gorm库 taosd: 官方给的go-sdk golang 官方代码案例 测试代码地址:未上传 开始对比 增 并发数 增加条目数 mysql5.7 用时 mysq...

492

水文模型大合集---SWAT模型、VIC模型、HEC-HMS模型、HSPF模型、FVCOM模型、Delft3D模型、HYPE模型.......

1.1 面源污染概要1.2 SWAT模型及应用1.3 SWAT模型原理1.4 SWAT模型输入文件1.5 ArcGIS与SWAT关系。

jwwkyjspt的博客 4359

避坑指南:SpringBoot整合TDengine+MySQL多数据源的那些配置陷阱

本文深入剖析了在SpringBoot项目中整合TDengine与MySQL多数据源时常见的配置陷阱与解决方案。针对MyBatis全局配置冲突、事务管理器失效、连接池参数差异化调优以及PageHelper等第三方插件兼容性问题,提供了详细的避坑指南和最佳实践配置,帮助开发者高效、稳定地构建混合数据源应用。

k9l0m1的博客 440

Centos7.6下安装Zabbix6

一、

Overmaster博客 1904

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

超越MySQL:TDengine的时序数据处理革新与实践指南

本文介绍了时序数据库TDengine的核心概念及其在SpringBoot项目中的应用。针对物联网等高频率时序数据场景,对比MySQL的局限性,重点阐述了TDengine的超级表设计、高效压缩存储和时间窗口查询优化等特性。通过具体SQL示例和SpringBoot多数据源配置实践,展示了如何实现TDengine与MySQL的协同使用。文章强调时序数据应以时间戳为主键,推荐使用批量写入和内置时间窗口函数等最佳实践,指出TDengine与MySQL互补的架构优势,为处理海量时序数据提供了高性能解决方案。

qq_45110186的博客 898

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

基于CDC的实时同步:如何将MySQL业务数据无缝接入TDengine时序数据库

标签与度量分离:将MySQL表中的静态属性(如订单所属的userid、regioncode)映射为TDengine超级表中的标签(Tag),而将动态变化的数值(如orderamount、status)映射为普通的数据列。CDC的核心原理是“旁路监听”。实践中,Flink提供了丰富的CDC连接器,如flink-connector-mysql-cdc,它可以直接将MySQL的binlog作为无界的流式数据源,而无需像过去那样必须借助Canal或Debezium等沉重的中间件。一、传统定时同步的性能瓶颈。

putiao_的博客 326

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

Java连接TDengine和MySQL双数据源

5、DBContextHolder使用ThreadLocal将数据源连接存储在当前线程的threadlocals(ThreadLocalMap)中,在连接数据库时自动获取当前线程对于的数据源。8、修改mybatis全局配置。3、重写SqlSession。

羽茉的博客 1015

tdengine与MySQL交换数据_TDengine简单了解一下

TDengine是taosData的产品,taosData是由陶姓海归创立的,目测是用自己的姓氏命名的。。。网站:https://www.taosdata.com/TDengine号称是专为物料网设计的平台,在我看来就是一个时序数据库而已,就像influxDB,它也和influxDB进行对比,读写速度,存储空间各方面性能都强于influxDB。TDengine社区版免费,开源协议是AGPL协议,这...

weixin_42128558的博客 1200
上一篇: Ollama 内网部署实战:绕过SSL障碍与高效模型管理
下一篇: JVS-Rules规则引擎实战:5分钟搞定决策流可视化配置(附避坑指南)
github5actions
博客等级 码龄1天 0粉丝 1824原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值