使用上一篇文章的思路来实现数据库表全量数据同步,遇到了一个奇葩的问题,在本地跑代码数据条数对上了,但是生产上线的时候跑数据居然条数对不上,于是乎,我进行了思考,最大的问题有以下几个原因:
1.1)数据量太大,线程池的参数设置不合理,开的线程太多会导致数据库最大连接数不够而报一个最大连接数不够的异常,从而多出来的处理不了的连接超时就会被数据库丢弃了。
1.2)数据库的参数性能不一样导致,发送过去插入的数据处理不过来,发送给数据库插入的批次数据越多,由于数据库性能不高处理不过来,导致数据库端阻塞,参数设置(最大连接数据,缓冲区大小等等参数)不合适,在大批量插入场景需要优化,优化数据库系统参数需要重启,所以该方案不适合生产环境。
1.3)线程提交到线程池执行是异步都在一个主线程中,都是默认springBoot的事务隔离级别,就会导致,主线程执行完毕,但是子线程没有执行完毕,会导致所有的子线的程事务在同一个事务主线程事务里面,这个主线程的事务提交的数据量就会很大,主线程执行完后子线程还在执行,不断的往mysql数据库发送数据,导致mysql端阻塞处理不过来,而丢了一些批次的数据。
核心线程数设置为4个,最大线程数设置为8个足够了,设置太多的话已经超过了mysql数据库机器性能了,一般机器就8核cpu,所以设置4-8个线程处理数据足够了,设置了太多的线程,一个是消耗资源不说,二一个是增大mysql的压力(使用过多的线程给mysql端发送大量的数据)。
package xxxxx;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
@Slf4j
public class EventListenerExecutor {
public static ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
static {
executor.setCorePoolSize(4);
// 配置最大线程数
executor.setMaxPoolSize(8);
// 配置缓存队列大小
executor.setQueueCapacity(10000);
// 空闲线程存活时间
executor.setKeepAliveSeconds(15);
executor.setThreadNamePrefix("event-listener");
// 线程池拒绝任务的处理策略:这里采用了CallerRunsPolicy策略,当线程池没有处理能力的时候,该策略会直接在execute方法的调用线程中运行被拒绝的任务;如果执行程序已关闭,则会丢弃该任务
// executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
//自定义数据策略
executor.setRejectedExecutionHandler((r, executor) -> {
try {
executor.getQueue().put(r);
} catch (InterruptedException e) {
log.error("线程处理拒绝策略失败:{}",e.getMessage());
e.printStackTrace();
}
});
// 等待所有任务结束后再关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);
// 设置线程池中任务的等待时间,如果超过这个时候还没有销毁就强制销毁,以确保应用最后能够被关闭,而不是被没有完成的任务阻塞
executor.setAwaitTerminationSeconds(60);
executor.initialize();
}
/**
* 直接执行 不给返回值
* @param task
*/
public static void execute(Runnable task) {
executor.execute(task);
}
/**
* 执行一哈 给返回值
* @param task 定时处理
* @return
* @param
*/
public static <T> Future<T> submit(Callable<T> task){
return executor.submit(task);
}
}
mysql的驱动连接的url需要加:rewriteBatchedStatements=true参数
关于rewriteBatchedStatements这个参数介绍:
MySQL的JDBC连接的url中要加rewriteBatchedStatements参数,并保证5.1.13以上版本的驱动,才能实现高性能的批量插入。
MySQL JDBC驱动在默认情况下会无视executeBatch()语句,把我们期望批量执行的一组sql语句拆散,一条一条地发给MySQL数据库,批量插入实际上是单条插入,直接造成较低的性能。
只有把rewriteBatchedStatements参数置为true, 驱动才会帮你批量执行SQL
另外这个选项对INSERT/UPDATE/DELETE都有效
多数据源配置参看上一篇文章:
spring:
datasource:
p6spy: true
dynamic:
datasource:
master:
username: xxx
password: xxxxx
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://xxxx:3306/xxxxxx?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=convertToNull&rewriteBatchedStatements=true
old_db:
url: jdbc:mysql://xxxxxx:3306/xxxxx?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowMultiQueries=true&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
username: xxxxxxx
password: xxxx
driver-class-name: com.mysql.cj.jdbc.Driver
set global max_allowed_packet=1024 *1024 * 512; # 单个packet可以允许的最大值
set global max_connections = 60000; # 并发连接请求量比较大,建议调高此值,以增加并行连接数量
set global innodb_lock_wait_timeout=16 * 1024; # 事务锁超时时间,默认50s,超过就报错
set global bulk_insert_buffer_size=512 * 1024 * 1024; # 加快insert插入效率
set global wsrep_max_ws_size=1024*1024*1024*4; # 避免事务大小超过限制,最大4G
以上参数如果是在Navicat里面本次会话设置的,使用Navicat执行批量插入sql脚本只对本次会话有效,mysql重启后或会话关闭后失效,永久配置需要在my.cnf配置文件中修改,然后重启数据库即可,永久配置可以参考网上教程。
使用CountDownLatch减法计数器是为了让主线程等待每个子线程都执行完在往下执行,数据插入的公共方法上新开一个事务,一批数据开启一个新的事务,就相当于一个线程来执行数据提交,隔离性好,不会相互影响,特别是id不唯一的情况下会在一个事务中会相互影响的,小批次提高了效率。
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void dealData(CopyOnWriteArrayList creditsRecordVOS, CountDownLatch countDownLatch) {
if (CollectionUtil.isNotEmpty(creditsRecordVOS)) {
EventListenerExecutor.execute(() -> {
doWork(creditsRecordVOS);
countDownLatch.countDown();
});
}
}
执行批量插入成功后,睡1s是为了给数据库一个缓冲的时间,让已经提交到mysql的数据执行完后在发送下一批数据过去,不至于发送大量数据导致mysq那边处理不过来而交通拥挤被挤出了赛道。
Integer cr = creditRecordMapper.insertBatchSomeColumn(result);
if (cr > 0) {
log.info("插入积分记录ok");
Thread.sleep(1000);
}
McMembersServiceImpl类如下:
package xxxxx;
import cn.hutool.core.collection.CollectionUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.cursor.Cursor;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
@DS("old_db")
@Service
public class McMembersServiceImpl extends ServiceImpl<McMembersMapper, McMembers> implements McMembersService {
@Autowired
private SqlSessionFactory sqlSessionFactory;
@Autowired
private CreditRecordService creditRecordService;
@Override
public void dealData() {
int pageSize = 20000;
AtomicInteger index = new AtomicInteger(1);
AtomicInteger totalIndex = new AtomicInteger(0);
Long total2 = this.baseMapper.getTotal2();
log.info("total2:{}",total2);
Long pageCount2 = (total2 + pageSize - 1) / pageSize; //推荐写法
log.info("pageCount2:{}", pageCount2);
CountDownLatch countDownLatch = new CountDownLatch(Math.toIntExact(pageCount2));
try (SqlSession sqlSession = sqlSessionFactory.openSession();
Cursor<CreditsRecordVO> pageCursor = sqlSession.getMapper(McMembersMapper.class).getCursorData()) {
List<CreditsRecordVO> creditsRecordVOS = new ArrayList<>();
for (CreditsRecordVO creditsRecordVO : pageCursor) {
creditsRecordVOS.add(creditsRecordVO);
totalIndex.getAndIncrement();
log.info("total:{}", totalIndex.get());
if (index.getAndIncrement() == pageSize) {
CopyOnWriteArrayList<CreditsRecordVO> creditsRecordVO2 = new CopyOnWriteArrayList<>(creditsRecordVOS);
log.info("creditsRecordVO2.size:{}", creditsRecordVO2.size());
creditRecordService.dealData(creditsRecordVO2, countDownLatch);
creditsRecordVOS.clear();
log.info("清空list:{}", creditsRecordVOS.size());
index.set(0);
}
}
if (CollectionUtil.isNotEmpty(creditsRecordVOS)) {
CopyOnWriteArrayList<CreditsRecordVO> creditsRecordVO3 = new CopyOnWriteArrayList<>(creditsRecordVOS);
log.info("creditsRecordVO3.size:{}", creditsRecordVO3.size());
creditRecordService.dealData(creditsRecordVO3, countDownLatch);
creditsRecordVOS.clear();
log.info("清空list2:{}", creditsRecordVOS.size());
index.set(0);
}
countDownLatch.await();
} catch (Exception e) {
log.error("游标查询异常:{}", e.getMessage());
}
log.info("total:{}", totalIndex.get());
}
@Override
public Long getTotal2() {
return baseMapper.getTotal2();
}
}
CreditRecordServiceImpl类如下:
package xxxxxxx;
import cn.hutool.core.collection.CollectionUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.nacos.common.utils.MD5Utils;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.BeanUtils;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAdjusters;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
public class CreditRecordServiceImpl extends ServiceImpl<CreditRecordMapper, CreditRecord> implements CreditRecordService {
private final TransactionDefinition transactionDefinition;
private final DataSourceTransactionManager transactionManager;
private final CreditRecordMapper creditRecordMapper;
private final MemberMapper memberMapper;
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void dealData(CopyOnWriteArrayList<CreditsRecordVO> creditsRecordVOS, CountDownLatch countDownLatch) {
if (CollectionUtil.isNotEmpty(creditsRecordVOS)) {
EventListenerExecutor.execute(() -> {
doWork(creditsRecordVOS);
countDownLatch.countDown();
});
}
}
private void doWork(CopyOnWriteArrayList<CreditsRecordVO> creditLogVOS) {
try {
List<CreditRecord> result = new ArrayList<>();
for (CreditsRecordVO creditLogVO : creditLogVOS) {
CreditRecord creditRecord = new CreditRecord();
creditRecord.setId(Long.valueOf(creditLogVO.getId()));
creditRecord.setContent(creditLogVO.getRemark() != null ? creditLogVO.getRemark() : "");
if (Objects.nonNull(creditLogVO.getNum())) {
creditRecord.setNum(BigDecimal.valueOf(creditLogVO.getNum()));
if (creditLogVO.getNum() > 0) {
creditRecord.setFlowType(1);
} else if (creditLogVO.getNum() < 0) {
creditRecord.setFlowType(1);
}
} else {
continue;
}
creditRecord.setCreditType(creditLogVO.getCredittype());
creditRecord.setStatus(1);
if (Objects.nonNull(creditLogVO.getUid())) {
creditRecord.setMemberId(Long.valueOf(creditLogVO.getUid()));
} else {
continue;
}
Integer createtime = creditLogVO.getCreatetime();
if (Objects.nonNull(createtime)) {
LocalDateTime localDateTime = LocalDateTime.ofInstant(
Instant.ofEpochSecond(createtime), ZoneOffset.ofHours(8)
);
creditRecord.setCreateTime(localDateTime);
}
creditRecord.setBalanceSnapshot(BigDecimal.valueOf(-1));
creditRecord.setDeleted(0);
creditRecord.setIsExp(0);
creditRecord.setFromId(0L);
creditRecord.setOpType(0);
creditRecord.setOrderNo("");
creditRecord.setUpdateTime(LocalDateTime.now());
result.add(creditRecord);
}
log.info("插入数据条数:{}", result.size());
Integer cr = creditRecordMapper.insertBatchSomeColumn(result);
if (cr > 0) {
log.info("插入积分记录ok");
Thread.sleep(1000);
}
} catch (Exception e) {
e.printStackTrace();
log.error("插入积分记录异常:{}", ExceptionUtils.getMessage(e));
}
}
}
这个是一个经过实战的套路,最近重构了一个会员模块涉及到会员表数据(4百多万)、粉丝表数据(3百多万)、关联映射表数据(4百多万)还有积分日志记录数据(1千多万),需要清洗同步到新的表中,前三个表是有关联的白表总的数据量有1千多万,后面的1千多万,总的2千多万数据同步时间只需要20分钟就可以搞定的,性能和效率还是杠杠的,执行时间如下图所示:
如果这个问题没有处理好,生产少的的会员数据就需要重新去反查,使用sql的join查出少了的数据重新补上去,由于用户数据同步少了,老用户登录系统就会变成一个新用户,就会带来一系列的问题,之前用老账号下的单子,登录后是一个新用户,关联不到订单了,所以需要去修复各种业务数据,数据补完后需要把新用户相关删除,然后清除redis的用户相关的数据,什么token/access_token/用户信息了,数据量大,那生产redis的用户相关的缓存数据也大的,这些由于数据同步的问题带来的后续这种蛋疼的问题,也是让人头疼,所以写了一篇避坑提效的文章,希望对你有帮助,请一键三连,么么哒,如何批量删除redis中的用户信息,请看下一篇文章!