今日指数项目集成多线程采集数据注意事项
一. 配置线程池
配置参数
# 定时任务线程池基础参数
task:
pool:
corePoolSize: 5 # 核心线程数
maxPoolSize: 20 # 设置最大线程数
keepAliveSeconds: 300 # 设置线程活跃时间,单位秒
queueCapacity: 100 # 设置队列容量
配置实体类提取参数
@ConfigurationProperties(prefix = "task.pool")
@Data
public class TaskThreadPoolInfo {
/**
* 核心线程数(获取硬件):线程池创建时候初始化的线程数
*/
private Integer corePoolSize;
private Integer maxPoolSize;
private Integer keepAliveSeconds;
private Integer queueCapacity;
}
核心配置类
@Configuration
public class TaskExecutePoolConfig {
@Autowired
private TaskThreadPoolInfo poolSource;
@Bean(name = "threadPoolTaskExecutor" , destroyMethod = "shutdown")
public ThreadPoolTaskExecutor threadPoolTaskExecutor(){
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 配置核心线程数
executor.setCorePoolSize(poolSource.getCorePoolSize());
// 配置最大线程数
executor.setMaxPoolSize(poolSource.getMaxPoolSize());
// 配置队列数
executor.setQueueCapacity(poolSource.getQueueCapacity());
// 配置活跃时间
executor.setKeepAliveSeconds(poolSource.getKeepAliveSeconds());
// 初始化
executor.initialize();
return executor;
}
}
二. 思路分析
为什么要集成多线程在项目中已有说明 , 这里就不多赘述 , 讲一下集成多线程有哪些注意事项
首先在原方案所采取的策略
在这里循环获取每个步长的数据后直接执行插入数据库的操作
可能的问题
-
数据库连接竞争
- 如果多个线程同时尝试访问数据库(尤其是进行写操作),可能会导致数据库连接池中的连接被迅速耗尽,进而影响应用的性能甚至导致错误。
-
数据一致性问题
- 如果多个线程同时向数据库写入数据,且这些操作之间存在依赖关系或需要维护某种顺序,可能会导致数据不一致。虽然您这里看起来是批量插入独立的数据集,但如果有其他事务性操作依赖这些插入的结果,则可能出现问题。
-
事务隔离级别
- 并发写入时,不同的数据库事务隔离级别(如读未提交、读已提交、可重复读、序列化)会影响数据的可见性和一致性。
-
死锁
- 如果数据库表有复杂的索引或外键约束,多个线程同时尝试写入数据可能会导致死锁。
-
无法获取数据是否插入成功的信息
这里我提供一种解决方案 , 就是通过爬虫获取所有数据 , 将这些数据封装进一个集合中 ,再集中存处
三. 代码实现
在这里也会有另外一些问题
- 用什么类型的集合存储数据 ?
- 这里因为采用的是多线程 , 集合必然要使用线程安全性的 , 当然也可以用不安全的通过使用锁进行保护也可以 , 但是这会降低效率
- 如何判断所有线程全部执行完毕 , 再进行下一步处理
- 使用CountDownLatch 对代码进行同步
public void getStockRtIndex() {
// 将股票编码信息保存到Redis
List<List<String>> partition = (List<List<String>>) redisTemplate.opsForValue().get("Stock_Codes");
if (partition == null) {
List<String> allCodes = stockBusinessMapper.getStockCodes();
allCodes = allCodes.stream().map(item -> item.startsWith("6") ? StockConstant.SHSTOCK + item : StockConstant.SZSTOCK + item).collect(Collectors.toList());
partition = Lists.partition(allCodes, 10);
redisTemplate.opsForValue().set("Stock_Codes", partition, 1, TimeUnit.DAYS);
}
ConcurrentLinkedQueue<StockRtInfo> dataSource = new ConcurrentLinkedQueue<>();
CountDownLatch latch = new CountDownLatch(partition.size());
partition.forEach(item -> {
threadPoolTaskExecutor.execute(() -> {
try {
String url = stockInfoConfig.getMarketUrl() + String.join(",", item);
//2.3 resetTemplate发起请求
ResponseEntity<String> exchange = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
int statusCodeValue = exchange.getStatusCodeValue();
if (statusCodeValue != 200) {
log.error("当前时间点{} , 数据采集失败 , 状态码{}", DateTime.now().toString("yyyy-MM-dd HH-mm-ss"), statusCodeValue);
latch.countDown(); // 即使失败也要计数减一
return;
}
String jsData = exchange.getBody();
List<StockRtInfo> list = parserStockInfoUtil.parser4StockOrMarketInfo(jsData, ParseType.ASHARE);
log.info("采集个股数据: {}", list);
list.forEach(dataSource::add);
}finally {
latch.countDown();
}
});
});
try {
latch.await(); // 等待所有线程完成
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 处理中断异常
log.error("数据采集有误 , 线程等待被中断{}", e);
}
List<StockRtInfo> finalDataSource = dataSource.stream().collect(Collectors.toList());
int num = stockRtInfoMapper.insertSelfStock(finalDataSource );
if (num > 0) {
log.info("当前时间点{} , 个股数据插入成功", DateTime.now().toString("yyyy-MM-dd HH-mm-ss"));
rabbitTemplate.convertAndSend("innerMarketExchange" , "stock.inner" , new Date());
} else {
log.error("当前时间点{} , 个股数据插入失败", DateTime.now().toString("yyyy-MM-dd HH-mm-ss"));
}
}