多线程和HTTP异步
场景
基于一个jvm下的批量执行业务。为了提高用户体验度。再多服务器多应用的情景下,使用多线程和HTTP异步。
注重点
①数据重复消费
②数据一致性
③HTTP数据交互
业务实例
一、线程池配置
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import com.syw.common.utils.Threads;
/**
* 线程池配置
*
* @author syw
**/
@Configuration
@EnableAsync
public class ThreadPoolConfig implements AsyncConfigurer
{
// 核心线程池大小
private int corePoolSize = 50;
// 最大可创建的线程数
private int maxPoolSize = 200;
// 队列最大长度
private int queueCapacity = 1000;
// 线程池维护线程所允许的空闲时间
private int keepAliveSeconds = 300;
@Bean(name = "threadPoolTaskExecutor")
public ThreadPoolTaskExecutor threadPoolTaskExecutor()
{
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setMaxPoolSize(maxPoolSize);
executor.setCorePoolSize(corePoolSize);
executor.setQueueCapacity(queueCapacity);
executor.setKeepAliveSeconds(keepAliveSeconds);
executor.initialize();
// 线程池对拒绝任务(无线程可用)的处理策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
}
二、具体业务实现
1.添加redis缓存锁,避免数据被重复消费。
注:非高并发场景,高并发场景可考虑redis分布式锁。
//唯一key
String order_key ="";
for(Object object: List){
order_key =object.getId();
//判断redis是否存在key值
if(!redisCache.redisTemplate.hasKey(order_key)){
//根据具体业务存储key值和设置失效时长。
redisCache.redisTemplate.opsForValue().set(order_key,1,2, TimeUnit.DAYS);
}else{
throw new CustomException("业务数据被执行或正在执行!");
}
}
2.进入多线程方法
System.out.println("....线程池检测开始");
//任务单执行
for(Object object:List){
service.updateOrderStateByOrderId(object);
}
System.out.println("....线程池检测结束");
3.需执行线程业务实例
通过@Async(“threadPoolTaskExecutor”)注解,和交给spring管理的线程名,启动线程。
因为在具体的业务场景中,一个用户的数据变动,可能在其他业务场景中也在变动,而往往我们记录明细或者日志需要当前场景下用户的数据。这里可以考虑在sql查询语句中使用排他锁for update,需要注意的是,排他锁可能会造成数据库阻塞。
排他锁属于行级锁,排他锁需要开启事务@Transactional才能使用,主要目的是限制其他场景或者线程下的对同一数据同时进行操作,只有当开启事务的业务结束时,其他场景对此数据操作才能进行。
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.util.*;
@Service
@Slf4j
public class AsyncTaskService {
/**
*
*
* @param taskOrders
* @param state
*/
@Async("threadPoolTaskExecutor")
@Transactional
public void updateOrderStateByOrderId(Object object) {
System.out.println("线程ID:" + Thread.currentThread().getId() + "线程名字:" + Thread.currentThread().getName() + "执行异步任务:");
log.info("执行批量任务:" + taskOrders.getId());
//业务块
Synchronized(this){
}
//http异步数据交互,因为开启了事务,在多数据库交互时,为了避免回滚造成的数据不一致性,http请求应该放在业务的最后,保证当前应用业务执行完毕。
HttpClientUtilAsync.sendPostAsync(requestUrl,param);
}
}
//查询用户对象添加排他锁
<select id="selectTaskBeforUserByIdAsync" parameterType="Integer" resultMap="TaskBeforUserResult">
<include refid="selectTaskBeforUserVo"/>
where id = #{id} for update
</select>
4.http异步
package com.syw.merchant.api.web.config;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.concurrent.FutureCallback;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
import org.apache.http.impl.nio.client.HttpAsyncClients;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.CountDownLatch;
@Slf4j
@Component
public class HttpClientUtilAsync {
public static HttpClientUtilAsync httpClientUtilAsync;
@Resource
private TaskOrdersMapper taskOrdersMapper;
@PostConstruct
public void init(){
httpClientUtilAsync=this;
//在异步中应用实例时,需初始化赋值。
httpClientUtilAsync.taskOrdersMapper=this.taskOrdersMapper;
}
/**
* 异步
* @param requestUrl
* @param params
* @return
*/
public static String sendPostAsync(String requestUrl,String params){
try{
final String[] resData = new String[1];
CloseableHttpAsyncClient client = HttpAsyncClients.createDefault();
client.start();
final CountDownLatch latch = new CountDownLatch(1);
HttpPost post = new HttpPost(requestUrl);
//设置请求头
post.addHeader("Content-type", "application/json; charset=utf-8");
// post.setHeader("Accept", "application/json");
StringEntity stringEntity = new StringEntity(params);
post.setEntity(stringEntity);
client.execute(post, new FutureCallback<HttpResponse>() {
@Override
public void completed(HttpResponse httpResponse) {
latch.countDown();
//响应内容
int a = httpResponse.getStatusLine().getStatusCode();
System.out.println("状态码:"+a);
if (a == 200) {
HttpEntity entity = httpResponse.getEntity();
try {
resData[0] = EntityUtils.toString(entity);
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("成功!");
} else {
}
}
//请求失败处理
@Override
public void failed(final Exception e) {
log.info("批量回款,请求失败:orderId"+orderId+"响应信息"+e.getMessage());
latch.countDown();
}
}
//请求取消后处理
@Override
public void cancelled() {
latch.countDown();
}
});
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
//关闭
try {
client.close();
} catch (IOException ignore) {
ignore.printStackTrace();
}
}catch(Exception e){
e.printStackTrace();
}
return "";
}
}
后记
本想开篇写个长点的博客记录下。但一提笔就词穷了。我们总是在步履蹒跚的学着一切,生活、事业、、、