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);
}
}
这里有几个关键点:
@Primary注解:当Spring容器中存在多个同类型的Bean(比如两个DataSource)时,被标记为@Primary的Bean会被优先注入。我们把MySQL相关的Bean都标记为主,避免一些自动装配的歧义。@MapperScan的sqlSessionFactoryRef属性:这明确指定了扫描到的com.yourdomain.data.dao.mysql包下的Mapper接口,要使用我们这里定义的mysqlSqlSessionFactoryBean。这是实现Mapper与数据源绑定的关键。- 事务管理器
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);
}
推荐的高效做法:批量查询。
- 先从MySQL查出所有设备ID列表。
- 将这个ID列表作为条件,向TDengine发起一次查询,利用TDengine的子表查询能力,一次性获取所有设备的最新数据。
- 在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:连接的空闲超时和最大生命周期。定期回收连接,可以防止网络中断导致的“僵尸连接”。
监控与诊断:
- 应用侧:启用SpringBoot Actuator的
/actuator/metrics/hikaricp.connections.*端点,监控连接池状态。 - MySQL侧:定期检查
SHOW STATUS LIKE 'Threads_connected';和SHOW VARIABLES LIKE 'max_connections';,确保连接数在健康范围内。 - TDengine侧:在
taos客户端使用SHOW CONNECTIONS;查看当前连接。如果发现连接数异常增长,要检查应用是否有连接泄露(比如忘了关闭ResultSet、Statement)。
一个常见的坑是:在TDengine的Mapper XML中,如果使用了动态SQL(如<if>标签)且条件不满足,可能会导致生成的SQL语句在TDengine语法下有问题(比如WHERE后面直接跟ORDER BY),从而执行失败。虽然MyBatis会抛出异常,但如果事务配置不当或异常未被捕获,可能会导致连接没有正确返回到连接池。因此,务必确保Service层有健全的异常处理逻辑。
6. 避坑指南与性能调优
6.1 常见配置陷阱
- MyBatis全局配置冲突:在
application.yml中配置了mybatis.config-location,又在DataSourceConfig的SqlSessionFactoryBean里设置了setConfigLocation。这可能会导致其中一个配置覆盖另一个,或者出现意想不到的行为。建议:在多数据源场景下,放弃全局的mybatis.config-location配置,将配置(如驼峰映射、插件等)分别设置到每个SqlSessionFactoryBean上,或者使用Java Config方式配置。 - Mapper XML路径扫描重复或遗漏:确保
DataSourceMysqlConfig中setMapperLocations扫描的路径classpath:mybatis/mysql/mapper/*.xml,与DataSourceTaosConfig中扫描的路径classpath:mybatis/taos/mapper/*.xml没有重叠。如果XML文件放错了目录,对应的Mapper接口就找不到SQL语句。 - TDengine驱动版本不匹配:这是最头疼的问题之一。客户端驱动版本与服务端版本不兼容,可能报“invalid connection”、“unsupported feature”等错误。务必保持版本一致,最好使用TDengine官方文档推荐的版本组合。
- 时区问题:在
jdbc-url中,我们指定了timezone=UTC-8。如果你的应用服务器、MySQL服务器、TDengine服务器不在同一个时区,时间戳就可能出现错乱。最佳实践:所有服务器统一使用UTC时间,在应用层按需转换为本地时间显示。
6.2 查询性能优化
-
TDengine查询优化:
- 利用时间分区:TDengine按时间自动分区。查询时尽量带上明确的时间范围(
WHERE ts >= 'xxx' AND ts < 'xxx'),这能帮助数据库快速定位数据块,避免全表扫描。 - 善用标签过滤:
WHERE条件中优先使用TAG列进行过滤,效率远高于普通数据列。例如WHERE imei_point = 'device001'。 - 避免
SELECT *:只查询需要的列。TDengine是列式存储,查询特定列比查询所有列效率更高。 - 子查询与JOIN谨慎使用:TDengine对子查询和JOIN的支持和优化不如传统关系型数据库,复杂关联查询建议在应用层内存中处理,或者重新设计数据模型。
- 利用时间分区:TDengine按时间自动分区。查询时尽量带上明确的时间范围(
-
混合查询优化:
- 异步与并行:对于不相关的MySQL查询和TDengine查询,可以考虑使用Spring的
@Async注解或CompletableFuture让它们并行执行,减少接口总响应时间。 - 缓存策略:对于不常变化的MySQL业务数据(如设备信息、用户信息),积极使用Redis等缓存。对于TDengine的聚合查询结果(如某设备过去24小时的平均温度),如果实时性要求不高,也可以定时计算后缓存起来。
- 批处理:向TDengine写入数据时,务必使用批插入(
INSERT INTO table1 VALUES (?,?,?), (?,?,?)...),单条插入的性能极差。MyBatis的foreach标签可以很好地支持批量插入。
- 异步与并行:对于不相关的MySQL查询和TDengine查询,可以考虑使用Spring的
6.3 写在最后:架构思考
折腾完这一套配置,代码跑起来,两个数据库各司其职,是不是感觉挺有成就感?但我最后想分享一点比技术实现更重要的东西:架构的简洁性。
这个方案的核心思想是“让专业的数据库做专业的事”。TDengine负责海量时序数据的写入和高性能查询,MySQL负责关系型业务数据的强一致事务处理。它们通过一个统一的SpringBoot应用来协调。这种架构在中小型物联网项目中非常实用,平衡了性能、成本和开发复杂度。
但是,随着业务规模扩大,你可能会遇到新的挑战:比如需要跨MySQL和TDengine做复杂的关联分析,或者数据量进一步爆炸。那时候,你可能需要考虑引入数据仓库(如ClickHouse)、流处理平台(如Flink)或者更专业的分析型数据库。不过,那都是后话了。在项目早期,采用我们今天讨论的SpringBoot+Mybatis多数据源整合TDengine和MySQL,无疑是一个快速、稳健的起点。它能帮你扛住初期的数据压力,把精力集中在业务逻辑开发上,而不是天天和数据库慢查询作斗争。
2376



被折叠的 条评论
为什么被折叠?



