业务场景
如果,我先用Key*,去模糊匹配Redis的key,返回出来的key,有3000个,这时候我需要一个for循环读取key的数据,3000条,并且把读取出来的数据,写入同一个List。只用一个for循环同步处理,就会花费很长时间,但是,如果使用多线程异步去读取,那么,时间会大大缩减。
代码实现
SpringBoot应用中需要添加@EnableAsync注解,来开启异步调用,一般还会配置一个线程池,异步的方法交给特定的线程池完成,如下:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 线程池初始化类
*
* @author huangJunHao
* @date 2022/3/18
*/
@Configuration
@EnableAsync
public class AsyncConfiguration {
@Value(value = "${threadPool.corePoolSize}")
private int corePoolSize;
@Value(value = "${threadPool.maxPoolSize}")
private int maxPoolSize;
@Value(value = "${threadPool.queueCapacity}")
private int queueCapacity;
@Value(value = "${threadPool.keepAliveSeconds}")
private int keepAliveSeconds;
@Value(value = "${threadPool.threadNamePrefix}")
private String threadNamePrefix;
@Bean("doSomethingExecutor")
public Executor doSomethingExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数:线程池创建时候初始化的线程数
executor.setCorePoolSize(corePoolSize);
// 最大线程数:线程池最大的线程数,只有在缓冲队列满了之后才会申请超过核心线程数的线程
executor.setMaxPoolSize(maxPoolSize);
// 缓冲队列:用来缓冲执行任务的队列
executor.setQueueCapacity(queueCapacity);
// 允许线程的空闲时间60秒:当超过了核心线程之外的线程在空闲时间到达之后会被销毁
executor.setKeepAliveSeconds(keepAliveSeconds);
// 线程池名的前缀:设置好了之后可以方便我们定位处理任务所在的线程池
executor.setThreadNamePrefix(threadNamePrefix);
// 缓冲队列满了之后的拒绝策略:由调用线程处理(一般是主线程)
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
使用的方式非常简单,在需要异步的方法上加@Async注解
import com.alibaba.fastjson.JSONObject;
import com.watsons.onstore.common.dto.Result;
import com.watsons.onstore.common.utils.EmptyUtil;
import com.watsons.onstore.common.utils.ResultUtil;
import com.watsons.onstore.redis.config.RedisKey;
import com.watsons.onstore.redis.pojo.User;
import com.watsons.onstore.redis.service.IRedisService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Future;
/**
* 测试类
*
* @author huangJunHao
* @date 2022/3/18
*/
@RestController
public class TestController {
@Autowired
private IRedisService redisService;
@Autowired
private AsyncService asyncService;
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private List<User> userList;
/**
* 测试插入redis
*/
@RequestMapping("/set/redis/list")
void setRedisList() {
for (int i = 0; i < 1000; i++) {
String key = RedisKey.getRedisKey("6666", RedisKey.PICK_ORDER_LIST, i + "", System.currentTimeMillis() + "");
User user = new User();
user.setEmpNo("EmpNo" + i);
user.setEmail("Email" + i);
redisService.set(key, JSONObject.toJSONString(user));
}
}
/**
* 测试根据模糊key获取redis数据,返回一个List
*/
@RequestMapping("/get/redis/list/thread")
Result getRedisListThread() {
//要把list变成线程安全的容器,否则会数据不正确
userList = Collections.synchronizedList(new ArrayList<>());
String key = RedisKey.getRedisKey("6666", RedisKey.PICK_ORDER_LIST, "*");
Set<String> keys = redisService.keys(key);
System.out.println("keys:" + keys.size());
List<Future> futureList = new ArrayList<>();
if (EmptyUtil.notEmpty(keys)) {
int j = 1;
for (String s : keys) {
try {
j++;
//下面这个方法在方法上使用了@Async注解的
Future future = asyncService.doSomething(s, j);
futureList.add(future);
} catch (Exception e) {
e.printStackTrace();
}
}
//判断进程是否全部结束
while (true) {
if (null != futureList) {
boolean isAllDone = true;
for (Future future : futureList) {
if (null == future || !future.isDone()) {
isAllDone = false;
}
}
if (isAllDone) {
break;
}
}
}
}
System.out.println("userList:" + userList.size());
return ResultUtil.renderSuccess(userList);
}
@Service
public class AsyncService {
@Async("doSomethingExecutor")
Future doSomething(String s, int i) {
//logger.info("开始获取user,{}", i);
try {
User entity = redisService.getEntity(s, User.class);
userList.add(entity);
} catch (Exception e) {
e.printStackTrace();
}
//返回需要用AsyncResult类,我也没试过其他的
return new AsyncResult(null);
}
}
/**
* 测试根据模糊key获取redis数据,返回一个List
*/
@RequestMapping("/get/redis/list")
Result getRedisList() {
String key = RedisKey.getRedisKey("6666", RedisKey.PICK_ORDER_LIST, "*");
try {
List<User> list = redisService.getFuzzyList(key, User.class);
System.out.println("getRedisList:" + list.size());
return ResultUtil.renderSuccess(list);
} catch (Exception e) {
e.printStackTrace();
}
return ResultUtil.renderSuccess();
}
}
结论
getRedisList:这是一个同步方法,从redis获取1000条数据耗时20S
getRedisListThread:这是一个异步方法,从redis获取1000条数据耗时1.5S
注意事项
@Async注解会在以下几个场景失效,也就是说明明使用了@Async注解,但就没有走多线程。
1、异步方法使用static关键词修饰;
2、异步类不是一个Spring容器的bean(一般使用注解@Component和@Service,并且能被Spring扫描到);
3、SpringBoot应用中没有添加@EnableAsync注解;
4、在同一个类中,一个方法调用另外一个有@Async注解的方法,注解不会生效。原因是@Async注解的方法,是在代理类中执行的。
需要注意的是: 异步方法使用注解@Async的返回值只能为void或者Future及其子类,当返回结果为其他类型时,方法还是会异步执行,但是返回值都是null