记录 RuoYi-Vue 项目集成 Sharding-JDBC 遇到的问题与解决办法

目录

  • 前提说明
    • 环境
    • 需求背景
  • 遇到的问题与解决办法
    • 问题1、LocalDateTime转换报错
      • 问题描述
      • 解决办法
    • 问题2、初始化分表数据,数据量过大,造成内存溢出
      • 问题描述
      • 解决方法
        • 代码如下:
    • 问题3、`count()`查询结果不正确
      • 问题描述
      • 解决方法
        • 代码如下:
    • 问题4、已分表的表需要和其他表联查,并且需要分页,会报错
      • 问题描述
      • 解决方法
        • 代码如下:
    • 问题5、数据统计 sql中用到`group by`和子查询,会报错
      • 问题描述
      • 解决方法
        • 方法① 使用视图查询
        • 方法② 使用多线程分别查询,再合并结果

前提说明

环境

整体框架为 RuoYi-Vue
数据库 MySQL
Sharding-JDBC 依赖版本 4.1.1

<!-- sharding-jdbc分库分表 -->
<dependency><groupId>org.apache.shardingsphere</groupId><artifactId>sharding-jdbc-core</artifactId><version>4.1.1</version>
</dependency>

具体集成过程及代码 请参考 RuoYi文档 集成sharding-jdbc实现分库分表
我这里仅记录遇到的问题与解决办法

需求背景

有一个维护了4年的项目,数据量有300万,且存储的是长字符串居多,查询速度缓慢,单表容量达到近30G,整个系统都需要围绕这个表开展业务;有多表联查,有数据统计;时间长久,当时开发项目的同事已跳槽,本着能不改结构就不改结构的原则,选择分库分表的方案


遇到的问题与解决办法

问题1、LocalDateTime转换报错

问题描述

java 实体类中的字段类型为LocalDateTime,mysql 表中字段类型为datetime,使用Sharding-JDBC之后报错java.time.LocalDateTime cannot be cast to java.sql.Timestamp

解决办法

请参考我的另外一篇文章 shardingsphere+mybatis LocalDateTime转换报错java.time.LocalDateTime cannot be cast to java.sql.Timestamp

问题2、初始化分表数据,数据量过大,造成内存溢出

问题描述

初始化分表数据时,从1张表分别存储到10张表中,数据量过大,容易造成内存溢出

解决方法

使用JDBC流式查询,不会一下子把所有数据获取到内存中,可以有效减少内存占用
将查询到的数据循环存入redis消息队列中
再使用多线程消费redis消息队列中的数据,插入到分表数据源中

代码如下:
/*** 初始化分表数据 使用redis mq 处理*/
@Override
public void initShardingDataByRedisMQ(Boolean isSlave) {log.info("开始初始化Slave分表数据");// 切换到分表数据源DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.SLAVE_SHARDING.name());long l = System.currentTimeMillis();CompletableFuture arr[] = new CompletableFuture[6];arr[0] = CompletableFuture.runAsync(() -> {//删除redis队列redisCache.deleteObject("sharding_data_old_mq"); //获取旧数据库数据源DataSource sourceDataSource = (DataSource) SpringUtils.getBean("oldSlaveDataSource");try {@Cleanup Connection sourceConnection = sourceDataSource.getConnection();@Cleanup Statement statement = sourceConnection.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);statement.setFetchSize(Integer.MIN_VALUE);@Cleanup ResultSet resultSet = statement.executeQuery("SELECT * FROM old_data ");while (resultSet.next()) {// 发送到redis队列redisCache.pushObject("sharding_data_old_mq", convertResultSetToObject(resultSet, OldData.class)); // convertResultSetToObject方法是将 resultSet 转成对应的实体类}} catch (IllegalAccessException e) {// 处理异常,例如记录日志或抛出自定义异常e.printStackTrace();} catch (InstantiationException e) {// 处理异常,例如记录日志或抛出自定义异常e.printStackTrace();} catch (SQLException e) {// 处理异常,例如记录日志或抛出自定义异常e.printStackTrace();}});for (int i = 1; i < 6; i++) {arr[i] = CompletableFuture.runAsync(() -> {DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.SLAVE_SHARDING.name());// 消费redis 消息队列 如果没获取到数据就 等待10秒,如果还是获取不到就跳出循环,如果 获取到数据就插入到对应的表里while (true) {// 从redis消息队列获取一个数据OldData oldData = redisCache.popObject("sharding_data_old_mq", 10, TimeUnit.SECONDS);if (oldData == null) {break;} else {oldDataMapper.insertOldDataHaveId(oldData); // 插入数据 不自动生成id}}DynamicDataSourceContextHolder.clearDataSourceType();});}CompletableFuture.allOf(arr).join();// 切换回主数据源DynamicDataSourceContextHolder.clearDataSourceType();log.info("初始化Slave分表数据完成,耗时:{}", System.currentTimeMillis() - l);
}public static <T> T convertResultSetToObject(ResultSet resultSet, Class<T> clazz) throws SQLException, IllegalAccessException, InstantiationException {T obj = clazz.newInstance();ResultSetMetaData metaData = resultSet.getMetaData();int columnCount = metaData.getColumnCount();// 将ResultSet对象的列名和值存到map中,再将map转换为json字符串,最后将json字符串转换为实体类对象Map<String, Object> rowData = new HashMap<>();for (int i = 1; i <= columnCount; i++) {rowData.put(StrUtil.toCamelCase(metaData.getColumnLabel(i)), resultSet.getObject(i));}String jsonStr = JSONObject.toJSONString(rowData);obj = JSONObject.parseObject(jsonStr, clazz);return obj;
}

redisCache中消息队列相关方法代码如下:

/*** 发送消息 基本的对象,Integer、String、实体类等** @param key 消息的键值* @param value 消息的值*/
public <T> void pushObject(final String key, final T value)
{redisTemplate.opsForList().leftPush(key, value);
}
/*** 获取消息,可以对消息进行监听,没有超过监听事件,则返回消息为null。* rightPop:1.key,2.超时时间,3.超时时间类型** @param key 缓存键值* @return 缓存键值对应的数据*/
public <T> T popObject(final String key, long timeout, TimeUnit unit)
{try {ListOperations<String, T> operation = redisTemplate.opsForList();return operation.rightPop(key, timeout, unit);} catch (RedisCommandTimeoutException e) {// 超时可能是因为队列中被消费完了log.warn("redis popObject timeout,key:{}", key);return null;}
}

问题3、count()查询结果不正确

问题描述

我这边是使用Mybatis作为数据库操作持久化框架,需要分表的表是分成了10个表,在使用count()查询时获取到的结果不像是总数量,只是其中一个表的数量
sql是非常基础的查询条数的sql,例如查询总条数 select count(0) from old_data

解决方法

用多线程分别查询10个分表的条数再累加到一起
(如果有更好的方法请指教)

代码如下:
/*** 分表后获取条数** @param param* @return 条数*/
@Override
public Integer getOldDataListShardingCount(OldDataListPageParam param) {List<Integer> countList = new ArrayList<>();CompletableFuture arr[] = new CompletableFuture[10];for (int i = 0; i < 10; i++) {int index = i;arr[i] = CompletableFuture.supplyAsync(() -> {Integer count = oldDataMapper.selectOldDataListCount(param, "old_data_" + index);if (count == null) {count = 0;}countList.add(count);return count;});}CompletableFuture.allOf(arr).join();return countList.stream().mapToInt(Integer::intValue).sum();
}

oldDataMapper.selectOldDataListCount 代码如下:

Integer selectOldDataListCount(@Param("param") OldDataListPageParam param, @Param("tableName") String tableName);

oldDataMapper.selectOldDataListCount 对应的xml代码如下:

<select id="selectOldDataListCount" resultType="int">select count(0) from ${tableName}<where>···</where>
</select>

问题4、已分表的表需要和其他表联查,并且需要分页,会报错

问题描述

使用Mybatis PlusIPage或者PageHelper处理分页查询时,都会先查询总条数,单表查询时没有问题,生成的sql例如 select count(*) from table1,但是联表查询时,生成的sql是将原始sql作为子查询然后获取条数,生成的sql例如 select count(*) from ( select t1.id, t2.value from table1 t1 left join table2 t2 on t1.id = t2.t1_id ) tb,在使用 Sharding-JDBC 分表查询时,不能识别子查询中的表,就会报错找不到表 (我的原始表不在分表数据源中,如果原始表和分表在同一个数据源中,那就会查询原始表,这样就起不到分表查询的这样了)

解决方法

用多线程分别查询10个分表的条数再累加到一起,这样就可以获取到正确的总条数,然后再使用查询到的总条数计算生成limit拼接到查询sql中,这样就避免了原始sql作为子查询的问题
(如果有更好的方法请指教)

代码如下:
/*** 查询分页列表** @param param 查询参数* @return 分页列表*/
@Override
public PageResponse<OldData> selectOldDataPageList(OldDataListPageParam param) {Integer total = getOldDataListShardingCount(param); // 此方法具体代码请看问题3if (total != null) {//重新封装数据返回给前台PageResponse pageResponse=new PageResponse<OldData>();pageResponse.setList(Lists.newArrayList());pageResponse.setTotal(0);return pageResponse;}List<OldData> list = oldDataMapper.selectOldDataListByLimit(param, generateLimitClause(param.getPageNum(), param.getPageSize(), total));//重新封装数据返回给前台PageResponse pageResponse=new PageResponse<OldData>();pageResponse.setList(list);pageResponse.setTotal(total);return pageResponse;
}/*** 生成limit** @param pageNum* @param pageSize* @param total* @return*/
public String generateLimitClause(int pageNum, int pageSize, int total) {int offset = (pageNum - 1) * pageSize;int limit = Math.min(pageSize, total - offset);return String.format("LIMIT %d, %d", offset, limit);
}

oldDataMapper.selectOldDataListByLimit 代码如下:

List<OldData> selectOldDataListByLimit(@Param("param") OldDataListPageParam param, @Param("limit") String limit);

oldDataMapper.selectOldDataListByLimit 对应的xml代码如下:

<select id="selectOldDataListByLimit" resultType="int">select···from old_data a left join old_data_info b on a.id = b.d_id<where>···</where><if test="limit != null and limit != ''">${limit}</if>
</select>

问题5、数据统计 sql中用到group by和子查询,会报错

问题描述

在做数据统计时,有一些数量需要通过sql的count()sum() 等函数查询,更复杂的还会包含联表查询、子查询、group by 等,这样sql 使用 Sharding-JDBC 分表查询肯定是会出问题的,例如上面的问题3问题4

解决方法

方法① 使用视图查询

将10个分表使用UNION连接select语句,针对不同的业务查询不同的字段,能少则少
优势:操作简单,代码改动量少
劣势:查询效率低

方法② 使用多线程分别查询,再合并结果

类似问题3的写法
优势:查询效率高
劣势:代码修改量大

(如果有更好的方法请指教)


持续更新中,有问题请评论~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/812943.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

界面控件DevExpress WinForms/WPF v23.2 - 富文本编辑器支持内容控件

众所周知内容控件是交互式UI元素(文本字段、下拉列表、日期选择器)&#xff0c;用于在屏幕上输入和管理信息。内容控件通常在模板/表单中使用&#xff0c;以标准化文档格式和简化数据输入。DevExpress文字处理产品库&#xff08;Word Processing Document API、WinForm和WPF富文…

Redis中的集群(三)

集群 槽指派 记录节点的槽指派信息。 clusterNode结构的slots属性和numslot属性记录了节点负责处理哪些槽: struct clusterNode { // ... unsigned char slots[16384/8];int numslots; // ... }slots属性是一个二进制位数组(bit array)&#xff0c;这个数组的长度位16384/8…

centos 7 sshd服务无法自动随机启动

centos 7 sshd 服务无法伴随主机启动而启动&#xff0c;而使用systemctl start sshd可以启动&#xff0c;很奇怪。 后来使用Kimi查询&#xff0c;有提示“检查系统启动服务的顺序和状态” systemctl list-dependencies <service>确保所有依赖服务都已正常启动。 查看本…

2024年认证杯SPSSPRO杯数学建模C题(第一阶段)云中的海盐全过程文档及程序

2024年认证杯SPSSPRO杯数学建模 C题 云中的海盐 原题再现&#xff1a; 巴黎气候协定提出的目标是&#xff1a;在 2100 年前&#xff0c;把全球平均气温相对于工业革命以前的气温升幅控制在不超过 2 摄氏度的水平&#xff0c;并为 1.5 摄氏度而努力。但事实上&#xff0c;许多…

Vue Router 路由生命周期钩子/路由导航守卫

文章目录 简介一、全局钩子二、路由独享的钩子三、组件内的钩子Vue 2 (Vue Router 3) 中Vue 3 (Vue Router 4)中 四、执行顺序五、错误处理Vue Router 4&#xff08;适用于 Vue 3&#xff09;中Vue Router 3&#xff08;适用于 Vue 2&#xff09;中 简介 Vue Router 提供了路由…

中仕公考:三支一扶期满后有编制吗?

三支一扶两年的期限到达之后&#xff0c;会自动获得编制吗? 完成三支一扶项目的服务期限后&#xff0c;参与人员必须通过正式的考试才能获得编制&#xff0c;而并不是期满后自动获得编制。但是&#xff0c;三支一扶服务期满人员在参加公务员考试中可依照其身份享受加分的优惠…

中国软件商业模式

很多IT技术人、风险投资人这些年进入SaaS产业&#xff0c;写了大量的文章来诊断中国软件产业。 我只是看过去十年的中国软件百强&#xff0c;这是事实上在中国这片土地上长出来并且跑出来的厂商。实践是检验一切真理的唯一标准。说美国怎么样怎么样&#xff0c;中国甲方企业和中…

QA测试开发工程师面试题满分问答12: 用户上传照片如何设计测试用例并进行测试

针对用户上传照片的功能&#xff0c;以下是一些从 QA 角度设计测试用例的示例&#xff0c;涵盖了前端功能点、后端功能点、缓存、异常处理、资源占用、并发和网络等维度&#xff1a; 前端功能点&#xff1a; a. 用户界面&#xff1a;验证上传照片的用户界面是否易于使用和导航&…

标准孔板简单适应性强

即使生活一地鸡毛&#xff0c;但仍然要觉得未来可期&#xff0c;做自己而不是解释自己&#xff0c;只要能变好&#xff0c;慢点又如何&#xff0c;愿我们都是苦尽甘来的人&#xff0c;熬得住就出众&#xff0c;熬不住就出局&#xff0c;鹤壁永成矿山&#xff0c;在行业坚持十余…

基于matlab动态化绘制一个彩色边框的爱心

一、版本1 % 定义爱心曲线的参数方程 t linspace(0, 2*pi, 100); x 16*sin(t).^3; y 13*cos(t) - 5*cos(2*t) - 2*cos(3*t) - cos(4*t);% 创建图形 figure; axis equal; axis off; title(爱心);% 循环遍历每个点&#xff0c;绘制不同颜色的线段 for i 1:length(t)-1% 清除…

Docker 镜像 用普通用户启动服务

Docker镜像内用普通用户启动服务 Docker是一种用于构建、封装和分发应用程序的开源平台。它利用容器化技术将应用程序及其依赖项打包到一个可移植的容器中&#xff0c;从而实现快速部署和可伸缩性。 在Docker中&#xff0c;通过使用Docker镜像可以创建容器&#xff0c;镜像是容…

Selenium+TestNG学习笔记

------------------TestNG-------------------- 1.层级 suite -》test-》class-》method 建议层级 class对应一个测试用例&#xff0c;suite对应一个测试集 2. testNG中的PO模式 3.运行多个测试类的测试用例 通过suite来进行管理;suite在testNG中可以通过xml 来进行编写管理…

Vue.js组件精讲 第4章 组件的通信2:派发与广播——自行实现dispatch和broadcast方法

上一讲的 provide / inject API 主要解决了跨级组件间的通信问题&#xff0c;不过它的使用场景&#xff0c;主要是子组件获取上级组件的状态&#xff0c;跨级组件间建立了一种主动提供与依赖注入的关系。然后有两种场景它不能很好的解决&#xff1a; 父组件向子组件&#xff0…

【Hello算法】 > 第 2 关 >数据结构 之 数组与链表

数据结构 之 数组与链表 1&#xff1a;Understanding data structures &#xff01;——了解数据结构——1.1&#xff1a;Classification-分类-1.2&#xff1a;Type-类型- 2&#xff1a;Arrays are the bricks that make up the wall of data structures *——数组是组成数据结…

django基于python的法院执法案件管理系统

本课题使用Python语言进行开发。代码层面的操作主要在PyCharm中进行&#xff0c;将系统所使用到的表以及数据存储到MySQL数据库中&#xff0c;方便对数据进行操作本课题基于WEB的开发平台&#xff0c;设计的基本思路是&#xff1a; 框架&#xff1a;django/flask 后端&#xff…

算法题解记录10+++缺失的第一个正数

题目描述&#xff1a; 给你一个未排序的整数数组 nums &#xff0c;请你找出其中没有出现的最小的正整数。 请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。 示例 1&#xff1a; 输入&#xff1a;nums [1,2,0] 输出&#xff1a;3 解释&#xff1a;范围 […

Spring与Spring Boot的区别:从框架设计到应用开发

这是我自己开发的一款小程序&#xff0c;感兴趣的可以体验一下&#xff1a; 进入正题&#xff1a; 在Java开发领域&#xff0c;Spring和Spring Boot都是备受推崇的框架&#xff0c;它们为开发人员提供了丰富的功能和便捷的开发体验。然而&#xff0c;许多人对它们之间的区别仍…

MySQL的基础操作(二)

目录 一.数据库约束 1.主键约束 (Primary Key) 2.唯一约束 (Unique) 3.外键约束 (Foreign Key): 4.检查约束(Check) 5.默认约束 (Default) 二.聚合查询 1.简单聚合函数 2.GROUP BY子句 3.HAVING子句 三.联合查询 1.内连接 2.左连接 3.右连接 4.子查询 5.合并查询…

大数据实训进行时:数据标注项目

数据标注项目 培训目的 让同学们先熟悉理论知识&#xff0c;如&#xff1a;识别障碍物是否满足拉框的要求&#xff0c;如何进行拉框&#xff1b;熟悉标注操作&#xff0c;培养出能够进入正式项目的人员 培训地点 理论&#xff1a;学术报告厅、阶梯教室 实操&#xff1a;1实…

【WPF应用42】WPF中的 GroupBox 控件详解

在 Windows Presentation Foundation (WPF) 中&#xff0c;控件是构建用户界面 (UI) 的基础。WPF 提供了丰富的控件库&#xff0c;其中包括 GroupBox 控件&#xff0c;它用于将相关的 UI 元素组织到逻辑分组中。在本博客文章中&#xff0c;我们将详细介绍 GroupBox 控件的功能、…