提示:真实项目中,如何使用shardingSphere;shardingSphere的面试题,真实分库分表的细节问题、真实开发中分库分表的问题及解决方案、分库分表后事务失效怎么解决,分库分表后@Transactional失效、,mq多节点下无法保证消息的有序性、分片后的分页、shardingSphere的分页、洗数据时洗不动怎么办。
文章目录
前言
背景是负责的项目中有个表,主键Id已经新增到3亿+,真实数据也已经1亿+。在某些特殊场景下,一次insert需要耗时14秒。因此要优化这个表,也就是分库分表。
我这里多说一句:
如果你的表数据量占大约20GB时,加字段/洗数据,应该是在2-3分钟内结束;如果你的表数据量占大约20GB时,加字段/洗数据,应该是在30-40分钟内结束;如果表数据量占20GB以上的空间,就不推荐再加上字段,或者洗数据了。就建议加新的关联表去实现业务吧。前一阵,有一个大约1亿+的表加字段,加了7,8个小时,都没加上新字段。最后是拿的一个没用的字段rename了一下,才算是解决,不然那期需求相当于没做。
一、问题及解决
从网上搜文章的时候,感觉很简单,引入个依赖,加几个配置,完事了, 几乎不用你做啥。但真正往自己项目中实现的时候,一个很坑的点就是版本问题。用哪个版本能跟你项目的版本适配。哪个版本可以支持批量操作。
1、版本适配
我项目中引入的是公司内部自己封装的一套版本,是基于spring-boot-starter1.1.10的,最开始使用的sharding5.x,然后发现不支持加解密组件,因此最终使用的sharding3.x。
解决方案:
将shardingSphere和springbootstart兼容。修改版本后解决:spring-boot-starter1.1.10的时候,使用sharding-jdbc-core3.1.0可以
2、单个insert好使,批量insert报错
改完以后,发现批量新增报错,单个新增好使。(shardingjdbc4.x不支持批量,5.x以后支持),也得注意一下。
解决方案
最终使用的是shardingSphere3.x
3、部分复杂的sql会报错
其他不涉及到分片的表(我给a表加的分片,但是b表的查询报错了),部分复杂的sql会报错,报resultmap接收的一个类型不对。不接入shardingSphere后,则不报错。
解决方案:
需要加入多数据源解决
4、事务,分库后@Transactional失效了
因为接入了多数据源,所以又产生了事务问题。因为事务的commit、rollback都是针对于connection来的,你现在俩链接了,至少@Transaction是无效的
解决方案
针对事务,市面上主流的有好几种解决方案:
- 分布式事务seata
- 手动提交、回滚事务
- 写一个方法, 判断俩事务是否全部成功、失败来决定是提交还是回滚。
因为seata,在公司内部的项目中未使用过,为了生产数据的安全性,没敢使用seata,毕竟生产环境数据丢了,那直接灾难了。所以写了个方法,判断这俩事务,如果同事成功,则commit,一个失败,则rollback。由于不方便贴代码,因此就写一个思路:不涉及分片表的,用一个datasource1,根据这个datasource去new DataSourceTransactionManager(datasource1),单独的分片表,用datasource2去new DataSourceTransactionManager(datasource2),然后@Transactional,修改为自定义的@ShardingTransactional, 然后aop去处理,如果俩sql都成功了,就提交事务,俩sql有一个失败,则同时回滚。
5、启动报错(内网、外网驱动对sharding的适配)
5、使用shardingsphere的时候,启动报错,是因为大写的“Mysql”和小写的“mysql”不匹配,导致直接报错了。
解决方案
解决方案是重写的io包,其实有一种更好的方案,是直接往这个枚举里面加一个(https://gitee.com/xchao/j-im/blob/master/jim-core/src/main/java/org/jim/core/utils/DynamicEnumUtil.java)
毕竟还有外网、内网,也需要考虑到
jdbc:mysql://localhost:3306/mytest
jdbc:nsd://localhost:3306/mytest
6、数据准确性
由于是生产数据,还是要重视一下的。因此我们就打算,主表(未分片的表)、分片表,并行运行。并且每次查询,同时查询主表、分片表,并对比除时间以外的,其他关键字段的值(其实就是封装一个util,然后忽略几个字段,然后对比。其实可以优化为比较几个关键字段,毕竟要一样只会一模一样,要不一样,几个关键字段已经不一样了)。然后也是aop实现,只要查到了主表的select语句,就aop同步执行一下分表后的语句,然后拿到结果集,去对比。一致的直接通过,不一致的记录到一个表里,去对比。
6.1、校验数据一致性时,遇到的问题及解决
上面的6的方案,去比较数据一致性的时候,有个事务的问题:像insert、update、delete操作,涉及到事务,只能是移动到代码的最后一行发送。不然本地报错回滚了,mq已经发送了,分片表那边消费完新增/更新/删除完了,就数据不一致了。那最后比的时候,肯定对不上。
解决方案
解决方案:将insert、update、delete操作,移动到代码的最后一行发送mq
6.2、mq多节点下无法保证消息的有序性
多节点下无法保证消息的有序性。由于我们是mq来实现的数据对比,但是生产环境有8个service节点。那就会有一个问题:像复杂流程的。比如a、b、c、十来步操作的,比如我第2步先delete然后在insert。到第8步了,符合某个场景,又删了一遍。(正常业务如果是编辑后提交的话,就是先删除,在提交日历。但是特殊场景,点对点驳回,就从审批中变成保存未提交了,又得删一遍日历表。就是mq的顺序有序性。我先发mq1去删除、新增。又发了mq2,去删除。多个节点下,我可能mq的2都删了,mq1的新增才执行完。导致脏数据了。)
解决方案
解决方案:1、从业务上避免,比如说我先进行校验,如果不满足,不往下走了,自然不会先发送、后撤回了。2、rabbitmq有一个配置,加上那个配置后(就一行配置,我忘了是啥了),即使你生产环境有8个节点,8个相同队列的mqlistener,那么也只会有一个人消费。3、将mq分组,group完了以后,能达到类似于单节点的效果。4、将8个service的节点改为1个service节点(哈哈,是不是过于简单粗暴了)
7、分页
分页这玩意,是真没法搞。尤其是自己写分片的那种。(比如,每页20条,让查第10页数据。听起来还好?再关联查询,再分片查询。别的不说,单单分片查询,让你查第二页,可不是每个分片查第10-20条数据,而是把每个分片表的前20条拿出来,然后在排序,再拿到排序后的第10-20条数据。这第2页还好,那第200页呢,把每个分片表的前100页通通拿出来,在分页?那2000页呢!)集成的shardingSphere,也不好搞。单表查询还好,一旦关联查询,并且还涉及到分片表。只能改业务了或者改sql了(改业务就是改app、改pc端的页面,去掉某个查询条件或者字段;改sql就是从关联查询,改为不关联查询,然后子查询关联表,取出来结果)。
解决方案
解决方案:修改业务,让相关的业务点,不在那么强关联。
8、分片后union太多
- 3.x 是单表查,(select *from my_test_2018;select *from my_test_2019;)
- 5.x 是union all 全部的分片表(select *from my_test_2018 union all select *from my_test_2019;)
如果查询条件恰好不涉及到分片查询时,那现象可就是:select *from my_test_2018 union all select *from my_test_2019 union … my_test_2020 union …my_test_2024。从2018到2024,小20张表呢,这么搞,那效率真不一定能快。
解决方案
通过业务分割,强制带上分片表的查询条件。比如说默认带上createTime=2024,或者根据当前业务/查询其他关联表登方式,能确定到涉及分片方案的字段,来保证只查某个表或某些表,避免全量查询全部的分片表。
9、洗数据
洗数据这个,有俩方案推荐:
- 洗数据的时候,最好直接给一个id自增初始值。(ALTER TABLE table_name AUTO_INCREMENT = 100;
这个代码是说把主键自增的初始值设置为100)。因为你一旦上线,业务就一直在跑,就是程序再跑,业务数据在自增,如果从0开始的话,那程序自己自增的id分别为:
1,2,3。。。 然后洗数据的时候,id也会1,2,3, 然后就会主键重复,从而插入失败。insert ino
tablename_2024 select * from tablename where createTime = ‘2024’; - 洗数据的时候不带id,让自增,不太推荐。没发根据id区分了。(方案1可以明确,比如说id在20w呢的,就是旧数据,20w以后的,为新数据)
create table my_test_2024 like my_test;
create table my_test_2025 like my_test;
...
# 发版前,获取截止到目前为止,最大的id,需要记录该id!!!
select id from my_test order by id desc limit 1;
# 洗数据,可以发版前一天洗
insert into my_test_2018
select * from my_test where createTime < '2019-01-01' and createTime >= '2018-01-01';
# 发版后处理发版期间的数据
insert into my_test_2018
select * from my_test where createTime < '2019-01-01' and createTime >= '2018-01-01' and id > createTime ;
二、延伸
总之,还是需要真正的开发一通,经得住线上系统的压力,才能够暴露出来足够多的问题,才能真正的给后续开发积累宝贵经验。比如说:
- shardingsphere的时候,比如说强隔离。是jdbc:nds,你直接变成了jdbc:mysql 。你的查询是好使了,,没问题,那行。那么shardingsphere自己有没有什么定时,去扫描某个包,然后去做了些别的操作,别的操作会不会报错,从而影响数据的准确性。
- 比如你的分片,有分片条件的,就是对应的24表、23表,要是分片条件不涉及的,能不能让他尽量涉及到。业务上能不能给个默认值或者默认区间,或者从技术手段上能不能给过滤掉一部分一定不包含数据的分片表
- 双写、双读的开关要不要nacos动态配置一下。双写就是主表也写一份,分片表也写一份;双读就是主表也读、分片表也读。单写主表单读主表单写分片表单读分片表要不要都安排上。
总结
分表的方式很多,我甚至还遇到过,在controller层次分表的操作。之前跟远光调接口的时候,他那边,必须是我们传参2022,2023才能查不同的表。当时我们的业务流程是,远光发送数据给A系统,A系统发送数据给B系统。然后A系统和远光交互的时候,需要公司id关联。我们这边的id和远光的不一致。需要匹配。我们是建立了一个中间表。然后参数还需要我们自己传2022、2023。如果传2019就报错了,因为远光没有2019的表。但是用户上传的发票可能ocr识别出来的日期确实是2019。所以很奇怪,居然还有在controller层传参分片的这种分表。大部分的分表,都是shardingjdbc加配置,框架就帮我们分表了。即使是自己分表。那也是service层自己做不同的dao。像这种直接提出去到controller层的,确实是奇怪。只能说,经历的还是少,还是需要不断的拓宽我们的视野。