最近接到了一个构建数据仓库同步功能的需求,需要支持从前端界面配置数据源以及源表与目标表,并且能够按照设定的执行策略定时进行数据同步操作。这对我来说是一个全新的挑战,因为我过去的工作主要集中在基本的CRUD操作上。接到产品经理分配的需求后,我立即着手开发,想到了以后可能会很很多个表需要在同一时间进行同步,为了提高性能就用上了多线程,利用线程池来管理线程,在短短两天内完成了编码工作。当我回顾自己编写的“优雅”代码时,心中不禁涌起一股自豪感。经过多次严格的测试,一点问题都没有。简直就是牛而逼之”天才程序员”非我莫属。因此迅速将代码提交推送让同事整合他负责业务模块,当他配置完成任务之后进行测试运行,不知道是不是他运气太差了,代码到他手上之后就报错了,多个任务同时执行时总有某个线程在执行同步任务时会报错;不说那么多了,先贴上我写的“优雅”代码。

以下是Quartz的执行逻辑,利用多线程执行任务,线程池固定5个线程,平平无奇没啥好说的

@Component
public class SimpleJob implements Job {
    private static final Logger log = Logger.getLogger(SimpleJob.class.getName());

    @Resource
    private IDataSyncService dataSyncService;

    // 创建一个固定大小的线程池
    private static ExecutorService fixedThreadPool;

    @PostConstruct
    public void init() {
        // 初始化线程池
        fixedThreadPool = Executors.newFixedThreadPool(5);
        log.info("线程池已初始化");
    }

    @PreDestroy
    public void destroy() {
        // 关闭线程池
        if (fixedThreadPool != null && !fixedThreadPool.isShutdown()) {
            fixedThreadPool.shutdown();
            log.info("线程池已关闭");
        }
    }

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        log.info("每分钟定时任务执行");

        // 从数据同步服务中扫描任务
        List<ZsSyncTask> zsSyncTasks = dataSyncService.scanTask();

        // 使用线程池并行执行每个任务
        for (ZsSyncTask task : zsSyncTasks) {
            fixedThreadPool.submit(() -> {
                try {
                    dataSyncService.sync(task.getId());
                } catch (Exception e) {
                    log.log(Level.SEVERE, "同步任务失败", e);
                }
            });
        }
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.

以下是业务层的代码,代码逻辑也是比较简单

  1. 扫描同步任务
  2. 执行符合执行策略的任务
  3. 查询任务详情和获取数据源配置
  4. 初始化数据源和获取连接、创建Statement
  5. 执行sql分页查询和写入数仓目标表
  6. 关闭数据库连接和Statement
@Service
public class DataSyncServiceImpl implements IDataSyncService {
    private static final Logger log = Logger.getLogger(DataSyncServiceImpl.class.getName());

    /**
     * 单次最大查询条数
     */
    private final static Integer MAX_QUERY_ROWS = 1000;
    /**
     * 数据同步Mapper
     */
    @Resource
    private SyncDataMapper syncDataMapper;
    /**
     * 数据源管理Mapper
     */
    @Resource
    private IZsSyncDbService zsSyncDbService;
    /**
     * 同步任务Mapper
     */
    @Resource
    private IZsSyncTaskService zsSyncTaskService;
    /**
     * 数据源管理
     */
    @Resource
    private DynamicDataSourceManager dataSourceManager;

    @Override
    public List<ZsSyncTask> scanTask() {
        log.info("扫描同步任务");
        List<ZsSyncTask> zsSyncTasks = zsSyncTaskService.scanTask();
        log.info("扫描同步任务结束,符合执行条件:" + zsSyncTasks.size() + "条");
        return zsSyncTasks;
    }

    /**
     * 同步数据
     * @param taskId
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void sync(Long taskId) {
        Date nowDate = new Date();
        log.info(String.format("开始同步任务ID:%d", taskId));
        try {
            // 查询任务详情
            ZsSyncTask syncTask = zsSyncTaskService.getById(taskId);

            // 记录最后一次执行时间
            syncTask.setLastExecuteTime(nowDate);

            // 计算下一次任务执行时间
            syncTask.setNextExecuteTime(IntervalTypeEnum.valueOf(syncTask.getIntervalType()).calculateNextTime(LocalDateTime.ofInstant(nowDate.toInstant(), ZoneId.systemDefault()), syncTask.getIntervalQty()));

            // 更新任务下次执行时间
            zsSyncTaskService.updateById(syncTask);

            // sql安全检测,防止数据库sql被修改后执行
            SqlInjectionUtils.checkSyncTaskEntitySql(syncTask);

            // 查询数据源详情
            ZsSyncDb syncDb = zsSyncDbService.getById(syncTask.getDbId());

            // 初始化数据库链接
            initSourceDbConnection(syncDb.getDbUrl(), syncDb.getUserName(), syncDb.getUserPassword(), syncDb.getDriver());

            // 初始化查询sql语句
            String sql = syncTask.getCustomQuerySql();

            // 最后一次查询到的数据条数
            Integer lastQueryRows = 0;

            // 页码
            int pageNum = 1;
            do {
                // 分页
                String limitSql = String.format("%s LIMIT %s,%s", sql, (pageNum - 1) * MAX_QUERY_ROWS, MAX_QUERY_ROWS);
                log.info(String.format("sql:%s, 页码:%d", limitSql, pageNum));

                // 执行查询
                List<Map<String, Object>> rows = executeQuery(limitSql);
                log.info(String.format("本次查询数据条数:%d", rows.size()));

                // 记录本次查询的数据条数
                lastQueryRows = rows.size();

                // 页数+1
                pageNum ++;

                if (rows != null && !rows.isEmpty()) {
                    // 写入新数据
                    syncDataMapper.batchInsert(syncTask.getTargetTbName(), rows);
                }
            } while (MAX_QUERY_ROWS.equals(lastQueryRows)); // 如果本次查询到的数据条数等于单次最大查询条数,则继续查询
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 关闭数据库连接
            closeSourceDbConnection();
        }
    }

    /**
     * 数据库链接对象
     */
    private Connection sourceConn;
    private Statement sourceStmt;

    /**
     * 初始化数据库连接
     * @param dbUrl
     * @param username
     * @param password
     * @param driver
     */
    private void initSourceDbConnection(String dbUrl, String username, String password, String driver) {
        try {
            // 获取数据库链接
            DataSource dataSource = dataSourceManager.getDataSource(dbUrl, username, password, driver);

            // 获取连接
            sourceConn = dataSource.getConnection();

            // 创建Statement
            sourceStmt = sourceConn.createStatement();
        } catch (SQLException e) {
            throw new RuntimeException("初始化数据库连接失败:" + e.getMessage());
        }
    }

    /**
     * 关闭数据库链接
     */
    private void closeSourceDbConnection() {
        if (sourceStmt != null) {
            try {
                sourceStmt.close();
            } catch (SQLException e) { /* 忽略异常 */ }
        }
        if (sourceConn != null) {
            try {
                sourceConn.close();
            } catch (SQLException e) { /* 忽略异常 */ }
        }
    }

    /**
     * 执行SQL查询
     * @param sql
     * @return
     */
    private List<Map<String, Object>> executeQuery(String sql) throws SQLException {
        ResultSet rs = null;
        try {
            // 从源数据库查询数据
            rs = sourceStmt.executeQuery(sql);

            // 获取ResultSet的元数据
            ResultSetMetaData metaData = rs.getMetaData();

            // 总查询到列数
            int columnCount = metaData.getColumnCount();

            // 遍历结果集
            List<Map<String, Object>> rows = new ArrayList<>(rs.getRow());
            while (rs.next()) {
                Map<String, Object> row = new HashMap<>();
                for (int i = 1; i <= columnCount; i++) {
                    // 获取列名
                    String columnName = metaData.getColumnName(i);

                    // 获取列值
                    Object value = rs.getObject(i);
                    row.put(columnName, value);
                }
                rows.add(row);
            }
            return rows;
        } finally {
            // 关闭资源
            if (rs != null) {
                try {
                    rs.close();
                } catch (Exception e) { /* 忽略异常 */ }
            }
        }
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.
  • 137.
  • 138.
  • 139.
  • 140.
  • 141.
  • 142.
  • 143.
  • 144.
  • 145.
  • 146.
  • 147.
  • 148.
  • 149.
  • 150.
  • 151.
  • 152.
  • 153.
  • 154.
  • 155.
  • 156.
  • 157.
  • 158.
  • 159.
  • 160.
  • 161.
  • 162.
  • 163.
  • 164.
  • 165.
  • 166.
  • 167.
  • 168.
  • 169.
  • 170.
  • 171.
  • 172.
  • 173.
  • 174.
  • 175.
  • 176.
  • 177.
  • 178.
  • 179.
  • 180.
  • 181.
  • 182.
  • 183.
  • 184.
  • 185.
  • 186.
  • 187.
  • 188.
  • 189.
  • 190.

因为不想浪费系统资源,所以每一次任务执行完成之后都会关闭数据库连接和Statement,恰巧就是因为这个操作导致代码在多线程的时候报错No operations allowed after statement closed。这时候号称“天才程序员”的我也是懵而逼之,每一个线程执行的时候都会重新创建,而且关闭的操作是在主函数进行,为什么会被关闭呢?后来想了想可能因为多线程的原因,但是为什么另外一个线程执行完任务之后关闭数据库连接和Statement会影响到另外一个线程?属实是把我给难到了,急忙叫来公司的大佬来帮忙调试代码。大佬就是大佬,一下子就看出了问题所在。虽然我的Service是可重入的,但是我在Service里面定义了Connection和Statement全局变量,然后导致了线程不安全的问题,相互影响和干扰。当线程A在使用Statement时,线程B正好执行完任务之后关闭了Statement,所以导致报错异常。

水货程序员之CRUD工程师踩坑多线程_Java程序员

将实例的全局变量改为主函数的局部变量,每一个线程都有自己的Connection和Statement副本,自己进行管理,通过参数的方式传递给子函数,而不是直接this,以下是修改后的代码

@Service
public class DataSyncServiceImpl implements IDataSyncService {
    private static final Logger log = Logger.getLogger(DataSyncServiceImpl.class.getName());

    /**
     * 单次最大查询条数
     */
    private final static Integer MAX_QUERY_ROWS = 1000;
    /**
     * 数据同步Mapper
     */
    @Resource
    private SyncDataMapper syncDataMapper;
    /**
     * 数据源管理Mapper
     */
    @Resource
    private IZsSyncDbService zsSyncDbService;
    /**
     * 同步任务Mapper
     */
    @Resource
    private IZsSyncTaskService zsSyncTaskService;
    /**
     * 数据源管理
     */
    @Resource
    private DynamicDataSourceManager dataSourceManager;

    @Override
    public List<ZsSyncTask> scanTask() {
        log.info("扫描同步任务");
        List<ZsSyncTask> zsSyncTasks = zsSyncTaskService.scanTask();
        log.info("扫描同步任务结束,符合执行条件:" + zsSyncTasks.size() + "条");
        return zsSyncTasks;
    }

    /**
     * 同步数据
     * @param taskId
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void sync(Long taskId) {
        Date nowDate = new Date();
        log.info(String.format("开始同步任务ID:%d", taskId));
        DataSource dataSource = null;
        Connection connection = null;
        Statement statement = null;
        try {
            // 查询任务详情
            ZsSyncTask syncTask = zsSyncTaskService.getById(taskId);

            // 记录最后一次执行时间
            syncTask.setLastExecuteTime(nowDate);

            // 计算下一次任务执行时间
            syncTask.setNextExecuteTime(IntervalTypeEnum.valueOf(syncTask.getIntervalType()).calculateNextTime(LocalDateTime.ofInstant(nowDate.toInstant(), ZoneId.systemDefault()), syncTask.getIntervalQty()));

            // 更新任务下次执行时间
            zsSyncTaskService.updateById(syncTask);

            // sql安全检测,防止数据库sql被修改后执行
            SqlInjectionUtils.checkSyncTaskEntitySql(syncTask);

            // 查询数据源详情
            ZsSyncDb syncDb = zsSyncDbService.getById(syncTask.getDbId());
			
            // 初始化数据库连接
            dataSource = dataSourceManager.getDataSource(syncDb.getDbUrl(), syncDb.getUserName(), syncDb.getUserPassword(), syncDb.getDriver());
            connection = dataSource.getConnection();
            statement = connection.createStatement();

            // 初始化查询sql语句
            String sql = syncTask.getCustomQuerySql();

            // 最后一次查询到的数据条数
            Integer lastQueryRows = 0;

            // 页码
            int pageNum = 1;
            do {
                // 分页
                String limitSql = String.format("%s LIMIT %s,%s", sql, (pageNum - 1) * MAX_QUERY_ROWS, MAX_QUERY_ROWS);
                log.info(String.format("sql:%s, 页码:%d", limitSql, pageNum));

                // 执行查询
                List<Map<String, Object>> rows = executeQuery(limitSql);
                log.info(String.format("本次查询数据条数:%d", rows.size()));

                // 记录本次查询的数据条数
                lastQueryRows = rows.size();

                // 页数+1
                pageNum ++;

                if (rows != null && !rows.isEmpty()) {
                    // 写入新数据
                    syncDataMapper.batchInsert(syncTask.getTargetTbName(), rows, statement);
                }
            } while (MAX_QUERY_ROWS.equals(lastQueryRows)); // 如果本次查询到的数据条数等于单次最大查询条数,则继续查询
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 关闭数据库连接
            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException e) { /* 忽略异常 **/ }
            }

            // 关闭Statement
            if (statement != null) {
                try {
                    statement.close();
                } catch (SQLException e) { /* 忽略异常 **/ }
            }
        }
    }

    /**
     * 执行SQL查询
     * @param sql
     * @return
     */
    private List<Map<String, Object>> executeQuery(String sql, Statement statement) throws SQLException {
        ResultSet rs = null;
        try {
            // 从源数据库查询数据
            rs = statement.executeQuery(sql);

            // 获取ResultSet的元数据
            ResultSetMetaData metaData = rs.getMetaData();

            // 总查询到列数
            int columnCount = metaData.getColumnCount();

            // 遍历结果集
            List<Map<String, Object>> rows = new ArrayList<>(rs.getRow());
            while (rs.next()) {
                Map<String, Object> row = new HashMap<>();
                for (int i = 1; i <= columnCount; i++) {
                    // 获取列名
                    String columnName = metaData.getColumnName(i);

                    // 获取列值
                    Object value = rs.getObject(i);
                    row.put(columnName, value);
                }
                rows.add(row);
            }
            return rows;
        } finally {
            // 关闭资源
            if (rs != null) {
                try {
                    rs.close();
                } catch (Exception e) { /* 忽略异常 */ }
            }
        }
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.
  • 137.
  • 138.
  • 139.
  • 140.
  • 141.
  • 142.
  • 143.
  • 144.
  • 145.
  • 146.
  • 147.
  • 148.
  • 149.
  • 150.
  • 151.
  • 152.
  • 153.
  • 154.
  • 155.
  • 156.
  • 157.
  • 158.
  • 159.
  • 160.
  • 161.
  • 162.

程序员最怕什么?当然是 bug,但是如果你非要让他们不害怕的东西,那就是 bug 的修复方法。