Análise técnica do projeto - pool de threads + processamento assíncrono

usar plano de fundo

Envolvendo análise de dados do sistema e leitura e gravação de resultados, a quantidade de dados é grande e o processamento serial é lento, portanto , operações em lote são executadas e várias tarefas não interferem umas nas outras;

Assíncrono pela primeira vez

alguns conceitos

  • Depois que uma chamada de método síncrona é iniciada, o chamador deve aguardar até que a chamada de método retorne antes de prosseguir com as ações subsequentes.
  • Uma chamada de método assíncrona é mais como uma passagem de mensagem. Uma vez iniciada, a chamada de método retornará imediatamente e o chamador poderá continuar as operações subsequentes. O método assíncrono geralmente é executado "real" em outro thread. todo o processo sem atrapalhar o trabalho do chamador

Por que usar assíncrono

Geral: Melhore o desempenho e a tolerância a falhas

  • O primeiro motivo: tolerância a falhas e robustez. Se houver uma anormalidade na gravação de dados, os dados de processamento não podem ser anormais devido à anormalidade na gravação de dados; porque o registro do usuário é a função principal e os pontos de envio são uma função secundária, mesmo se o envio de pontos é anormal, é necessário lembrar ao usuário que o registro foi bem-sucedido, para compensar os pontos anormais posteriormente.
  • O segundo motivo é melhorar o desempenho. Por exemplo, leva 20 milissegundos para registrar um usuário e 50 milissegundos para enviar pontos. Se for usado síncrono, o tempo total será de 70 milissegundos. Se for usado assíncrono, não há necessidade de esperar para pontos, então leva 20 milissegundos.

Implementação assíncrona - baseada no pool de threads

1. A anotação @Async para executar tarefas assíncronas exige que habilitemos manualmente a função assíncrona. A maneira de habilitá-la é adicionar @EnableAsync
2. ThreadPoolTaskExecutor é implementado manualmente

Conhecendo o pool de threads-ThreadPool

Tipos de pools de threads (principalmente quatro)

1. newCachedThreadPool: usado para criar um pool de threads , adequado para cenários com cargas leves e executar tarefas assíncronas de curto prazo. (A tarefa pode ser executada rapidamente, porque o tempo de execução da tarefa é curto, pode terminar rapidamente e não causará troca excessiva de CPU) 2. newFixedThreadPool :
Crie um pool de threads de tamanho fixo , porque a fila de bloqueio ilimitada é usada, portanto, o número real de threads nunca mudará. É adequado para cenários de carga pesada e limita o número atual de threads. (Certifique-se de que o número de threads seja controlável e não cause muitos threads, resultando em uma carga de sistema mais séria)
3. newSingleThreadExecutor: Crie um pool de threads de thread único , adequado para a necessidade de garantir a ordem de execução de várias tarefas.
4. newScheduledThreadPool: adequado para executar tarefas atrasadas ou periódicas .

Configurações de parâmetros principais

A programação simultânea JAVA
pode ver que o tamanho da fila e o número máximo de encadeamentos do conjunto de encadeamentos padrão são ambos o valor máximo de Integer, o que obviamente deixará certos riscos ocultos para o sistema.

pergunta: o número de tarefas por segundo, assumindo 500~1000
taskcost: o tempo gasto em cada tarefa, assumindo 0,1s
responsetime: o tempo máximo de resposta permitido pelo sistema, assumindo 1s

Para tarefas com uso intensivo de CPU,
tente usar um pool de threads menor . O número máximo de threads = número de núcleos de CPU + 1.
Como as tarefas intensivas da CPU tornam a taxa de uso da CPU muito alta, se muitos threads forem abertos, isso causará troca excessiva de CPU .
Tarefas intensivas de E/S (este projeto)
podem usar um pool de threads um pouco maior , o número máximo de threads = 2 * número de núcleos de CPU.
**A taxa de uso da CPU de tarefas intensivas de E/S não é alta,** então a CPU pode ter outros encadeamentos para processar outras tarefas enquanto espera pela E/S e fazer uso total do tempo da CPU.
O número de threads principais ao mesmo tempo = o número máximo de threads * 20% .

Etapas de execução do pool de threads

1. Quando o tamanho do pool for menor que corePoolSize, crie um novo thread e processe a solicitação.
2. Quando o tamanho do pool for igual a corePoolSize, coloque a solicitação no workQueue (QueueCapacity) e os threads ociosos no pool irão para o workQueue para buscar tarefas e processá-las.
3. Quando o workQueue Quando a tarefa não puder ser colocada, crie um novo thread no pool e processe a solicitação. Se o tamanho do pool atingir o maximumPoolSize , use RejectedExecutionHandler para lidar com a rejeição .
4. Quando o número de threads no pool for maior que corePoolSize , os threads redundantes aguardarão por keepAliveTime por um longo tempo Destrói-se

Implementação

Aplicação 1 - Método de anotação **@Async**

Artigo de referência
Para chamadas de método assíncrono, a anotação @Async é fornecida desde o Spring 3. Só precisamos marcar essa anotação no método para implementar chamadas assíncronas.
Além disso, também precisamos de uma classe de configuração para habilitar funções assíncronas por meio da anotação do driver do módulo Enable @EnableAsync.

@Configuration
@EnableAsync
public class ThreadPoolConfig {

}

O pool de threads deve ser declarado manualmente por meio do construtor de ThreadPoolExecutor, usando a anotação @Async, e o pool de threads SimpleAsyncTaskExecutor é usado por padrão , que não é um pool de threads real .

Configuração do pool de threads

Artigo de referência Autor: Piaomiao Jam
não pode realizar a reutilização de encadeamento usando este conjunto de encadeamentos, e um novo encadeamento será criado toda vez que for chamado. Se o sistema continuar a criar threads, eventualmente fará com que o sistema ocupe muita memória e cause um erro OutOfMemoryError

// 核心线程池大小
private int corePoolSize = ;

// 最大可创建的线程数
private int maxPoolSize = ;

// 队列最大长度
private int queueCapacity = ;

// 线程池维护线程所允许的空闲时间
private int keepAliveSeconds = ;

classe executiva

@Component
public class UserDataHandler {
	
 
	private static final Logger LOG = LoggerFactory.getLogger(SyncBookHandler.class);
	/**
	 * @param userdataList 一段数据集合
	 * @param pageIndex 段数
	 * @return Future<String> future对象
	 * @since JDK 1.8
	 */
	@Async
	public Future<String> syncUserDataPro(List<UserData> userdataList,int pageIndex){
		
 
			//声明future对象-主要是为了返回处理信息
		 	Future<String> result = new AsyncResult<String>("");
		 	//循环遍历该段旅客集合
			if(null != userdataList && userdataList.size() >0){
				for(UserData userdata: userdataList){
					try {
						//数据入库操作
						// 针对每一个获取到的切割子段,进行操作 同时进行
					} catch (Exception e) {
						
						//记录出现异常的时间,线程name
						result = new AsyncResult<String>("fail,time="+System.currentTimeMillis()+",thread id="+Thread.currentThread().getName()+",pageIndex="+pageIndex);
						continue;
					}
				}
			}
			return result;
		}

Os seguintes pontos devem ser observados aqui:
1. A implementação da operação assíncrona de dados em lote precisa dividir os dados, portanto, há fatias de loop aqui. 2.
Depois de usar a anotação Async, void não retorna um valor ou apenas um valor do tipo Future pode ser retornado, caso contrário a anotação é inválida; O caso é retornar informações de execução, então use Future.
3. Quanto ao cenário de exceção de banco de dados, depende se os requisitos de negócios específicos exigem transações e reversões, aqui meu cenário de negócios permite perda de dados;

use-CountDownLatch

Objetivo: Garantir que todos os threads anteriores sejam executados antes de ir para a próxima etapa.
Embora a interface de solicitação assíncrona seja implementada, a eficiência foi bastante aprimorada. No entanto, devido à chamada assíncrona, o resultado do processamento será retornado antes do processamento dos dados. Devemos esperar que todos os encadeamentos em syncUserDataPro terminem antes de retornar ao método da tarefa de encadeamento de chamada atual

List<List<UserData>> lists = Lists.partition(res, 10);
CountDownLatch countDownLatch = new CountDownLatch(lists.size());
for (List<UserData> list : lists) {
      while (iterator.hasNext()) {
            UserData userData = iterator.next();
            this.userDataService.asyncinsertUserDataList(userData, countDownLatch);
      }
}
countDownLatch.await(); //保证之前的所有的线程都执行完成,才会走下面的;
log.info("完成!!共耗时:{} m秒", (endTime - startTime));
  	@Override
    @Async("threadPoolTaskExecutor")
    public void asyncinsertUserDataList(UserData userData, CountDownLatch countDownLatch)
    {
        try  {
//            log.info("start executeAsync");
            userDataMapper.insertUserData(userData);
//            log.info("end executeAsync");
        } catch(Exception e){
            e.printStackTrace();
        } finally {
            // 无论上面程序是否异常必须执行 countDown,否则 await 无法释放
            countDownLatch.countDown();
        }
    }

Suplemento - Teoria CountDownLatch

CountDownLatch permite que os threads de contagem sejam bloqueados em um lugar até que todas as tarefas dos threads tenham sido executadas.

Ao criar um objeto CountDownLatch, você precisa especificar um valor de contagem inicial, que representa o número de threads que precisam aguardar. Sempre que um thread conclui sua tarefa, ele chama o método countDown() de CountDownLatch e o valor do contador é decrementado em um. Quando o valor do contador se torna 0, os threads em espera são ativados e retomam suas tarefas.

Para princípios subjacentes mais detalhados, consulte o conhecimento relacionado ao AQS

Acho que você gosta

Origin blog.csdn.net/weixin_54594861/article/details/130442978
Recomendado
Clasificación