java学习之路:血与泪的系统升级之数据迁移

2022-10-19 14:27:36

前言

在最近的工作中,入行两年的我终于理解到了一个以往都不明白的事情:为什么相当一部分程序员回家就是睡觉。
我真的太累了。。。。好吧,虽然我自己的原因占大头。接下来就说明一下前因后果,以及流泪流血的过程。

前因

接续上一篇文章,我主要还是搞V1系统到V2系统的过渡。接口重构不用我管,我主要管基础数据的迁移和接收。
例如VIP数据,订单数据,好友数据等。
当然,在这之前我从来没搞过这类的迁移,对于mysql的知识也无法灵活运用,毕竟都没实际接触到过对应的场景。(论经验的重要性!!!)

数据量

V2系统中,数据量偏大的表有这么几个:

业务表row_count
VIP1000w
订单2000w per month
各种用户关系表数千万 per table
各种历史记录表数千万 per table

系统升级后,表结构也重新设计了,因此需要将老系统的数据迁移至新表,涉及到数据逻辑的修改。

初步实现

说到表数据迁移,那就是定时任务了(来自一个mysql实战经验较少的JAVA开发的怒吼~)
于是使用xxl-job,写了40套java代码进行任务迁移(40+个任务呢)。
由于数据混乱,加上我业务理解能力可能稍差,所以这个过程可谓是改了又改,改了又改,改了又改(吐槽要说三遍)。
并且在写任务的过程中,总会出现<明明我写完逻辑可以开始迁移了,但是由于表的设计更改了,我这要返工一部分>的情况。
于是,一个多月过去了…

后期优化

首先,举个典型例子来说明问题。
订单数据:
第一次迁移2000w左右数据量,中间有个处理,通过订单项去统计当前订单总支付金额,总数等字段。
写完之后测试:卧槽,一个订单迁移就要30秒,一共2000w数据,这一个月能迁移完吗?
然后就开始找问题,修正,再测试,最终结果终于满意了,2000w订单3小时就迁移完毕。可喜可贺!

问题和处理方案总结

1.分页查询慢怎么办?

这个问题其实不难解决。依然用订单举例。
select * from order_info limit start,size;
这是大部分人的分页写法。
当然取前N页的数据不慢的。但是如果2000w的数据,要取第1000w-1001w的数据,就变成了
select * from order_info limit 10000000,10000;
大家可以想一下,在真正获取到目标数据之前,就已经扫描了一千万的数据了,能不慢吗?
因此,我们需要利用一些手段,能够更快定位到第1000条数据。

索引!

对于查询语句来说,只要能走索引快速定位到起始行,那不就相当于从第一页开始取数据了?

于是sql就变成了下面这种:
select * from order_info where id > 10000000 limit start, size;
id是该表主键。由于limit是最后从查询结果集进行限制,所以会先根据索引查询到第1000w数据到之后的所有数据,再进行limit获取。
为了保证无论查到哪条数据都能使用索引,那么我们可以考虑下面的写法:
select * from order_info where id > startId limit 0, 10000;
此时startId是动态传入的,只要条件字段有索引,那么就能极大地提高查询效率。
同时,我们还能加上一个endId做区间查询,这样就能避免取出数据总量仍然很大的问题了。
总结了一句话:

大量数据做分页查询,动索引(字段)不动分页!

2.kafka消费者频繁报错唯一约束冲突

```
Caused by: java.sql.SQLIntegrityConstraintViolationException: Duplicate entry 'xxxxx' for key 'order_no'
```

首先来看一下代码(脱敏):
Order order = new Order(); String orderNo = "xxxxx"; order. Order oldOrder= orderMapper.selectByOrderNo(orderNo); if (oldOrder==null){ orderMapper.insert(order); }else{ order.setId(oldOrder.getId()); orderMapper.updateById(order); }

这个问题找起来可谓是心酸,不多说废话,直接上分析过程。

1). 肯定另一条线程插入了同样的数据。
解决方案:分布式锁.

```
	RLock lock = redissonClient.getLock(order.getOrderNo());
	try {
		lock.lock(3L,TimeUnit.SECONDS);
		// 上述代码
		// xxxxxxxxxxxxxx
	} finally {
		if (lock != null) {
			lock.unlock();
		}
	}
	
```

心路: 这下没问题了啊哈哈。

2). 第二天: 卧槽,怎么还在报错???
难道是没锁住?再看一遍代码!没错啊,奇了怪了。行,既然还报错,我就catch住再做一次更新。于是就成了下面这样:

```
RLock lock = redissonClient.getLock(order.getOrderNo());
	try {
		lock.lock(3L,TimeUnit.SECONDS);
		try{
		// 插入和更新逻辑 
		xxxxxxx
		} catch (Exception e) {
			Long id = orderMapper.selectIdByOrderNo(order);
			orderMapper.updateById(order);
		}
	} finally {
		if (lock != null) {
			lock.unlock();
		}
	}
```

心路:我不信你还报错。我直接catch住异常做更新不可能再报错了!

3). 第三天: 卧槽卧槽卧槽,又出现了!

于是,我和组里其他人一起讨论了一下,这时我才了解到,咱们的mysql用了读写分离。

读写分离:主库写,从库读。此时会产生一个问题:主库写入后,同步到从库会有一个延迟。如果往主库写入一条数据后,立刻执行查询语句,那么将无法从从库查询出该条数据。

于是我立刻检查了一下数据入口,发现有多个入口都在往数据库插入数据。因此执行查询sql再进行插入就会出现这种问题。

解决方案:
1. 查询时使用主库数据源。(我的做法)
2. 使用insert into xxx values() on duplicate key update x1=x1,x2=x2 (待验证)

以上,就是我遇到的最严重的两个问题了。
希望各位也能从我的错误经验中汲取到觉得有意义、有价值的部分。

See U Next time~~~

  • 作者:皮~皮卡
  • 原文链接:https://blog.csdn.net/qq_44988694/article/details/121465435
    更新时间:2022-10-19 14:27:36