1. Princípio de implementação básica do agendamento de tarefas de quartzo
O Quartz é um projeto de código aberto no campo de agendamento de tarefas da OpenSymphony, que é completamente baseado na implementação de Java. Como uma excelente estrutura de agendamento de código aberto, o Quartz possui os seguintes recursos:
(1) funções de agendamento poderosas, como apoiar uma variedade de métodos de agendamento, podem atender a várias necessidades convencionais e especiais;
(2) métodos de aplicação flexíveis, como suportar várias combinações de tarefas e agendamento e suporte a vários métodos de armazenamento de agendamento de dados;
(3) Recursos distribuídos e de cluster, a Terracotta melhorou ainda mais suas funções originais após a aquisição. Este artigo será adicionado a esta parte.
1.1 elementos centrais de quartzo
Os principais elementos da programação de tarefas de quartzo são: agendador de tarefas do agendador, gatilho e tarefa de trabalho. Onde o gatilho e o trabalho são metadados para agendamento de tarefas, e o Scheduler é o controlador que realmente executa o agendamento.
O Trigger é um elemento usado para definir o horário de agendamento, ou seja, segundo o qual o tempo rege a tarefa é executada. Existem quatro tipos de gatilhos no quartzo: SimpleTrigger, Crontirgger, DateIntervalTrigger e NthincludeddayTrigger. Esses quatro gatilhos podem atender à maioria das necessidades dos aplicativos corporativos.
O trabalho é usado para representar tarefas programadas. Existem principalmente dois tipos de empregos: sem estado e com estado. Para o mesmo gatilho, os trabalhos com estado não podem ser executados em paralelo. Somente depois que a tarefa acionada por último é executada, a próxima execução pode ser acionada. Jó tem dois atributos principais: volátil e durabilidade, onde volátil significa se a tarefa é persistida no armazenamento do banco de dados, enquanto a durabilidade significa se a tarefa é retida quando não há associação de gatilho. Ambos são persistidos ou preservados quando o valor é verdadeiro. Um trabalho pode ser associado a vários gatilhos, mas um gatilho só pode associar um trabalho.
O Scheduler é criado pelo Scheduler Factory: DirectSchedulerFactory ou StdsChedulerFactory. A segunda fábrica, StdschedulerFactory, é usada com mais frequência porque o DirectSchedulerFactory não é conveniente o suficiente para usar, e muitas configurações detalhadas de codificação manual são necessárias. Existem três tipos principais de agendador: RemotEMBeansCheduler, Remotescheduler e Stdscheduler.
A relação entre os elementos centrais do quartzo é mostrada na Figura 1.1:
Figura 1.1 Diagrama de relacionamento do elemento central
1.2 Visualização do thread de quartzo
No quartzo, existem dois tipos de threads, threads de agendadores e threads de execução de tarefas, onde os threads de execução de tarefas geralmente usam um pool de threads para manter um grupo de threads.
Figura 1.2 Visualização do thread de quartzo
Existem dois threads principais para o agendador: threads que executam agendamento e threads regulares que executam o Triggger incorreto. As pesquisas regulares de encadeamento de despacho todos os gatilhos armazenados. Se houver um gatilho que precisar ser acionado, ou seja, o tempo do próximo gatilho atingiu, ele obtém um encadeamento ocioso do pool de threads de execução de tarefas para executar a tarefa associada ao gatilho. O encadeamento de falhas de ignição digitaliza todos os desencadeadores para ver se há um triggger fiscalizado. Nesse caso, é tratado separadamente de acordo com a política de falha de ignição (fogo agora ou aguarde o próximo incêndio).
1.3 armazenamento de dados de trabalho de quartzo
Os gatilhos e trabalhos no quartzo precisam ser armazenados antes que possam ser usados. Existem dois métodos de armazenamento no Quartz: Ramjobstore e Jobstoresuport, onde as lojas do Ramjobstore acionam e os trabalhos na memória, enquanto o JobStorsOpport lojas gatilhos e trabalhos no banco de dados com base no JDBC. O Ramjobstore é um acesso muito rápido, mas como todos os dados serão perdidos após a parada do sistema, o JobStorsupport deve ser usado em aplicativos de cluster.
2. Princípio do cluster de quartzo 2.1 Arquitetura de cluster de quartzo
Cada nó em um cluster de quartzo é um aplicativo de quartzo independente, que por sua vez gerencia outros nós. Isso significa que você precisa iniciar ou interromper cada nó separadamente. No cluster de quartzo, os nós de quartzo independentes não se comunicam com outro nó ou nó de gerenciamento, mas, em vez disso, percebem outro aplicativo de quartzo através da mesma tabela de banco de dados, como mostra a Figura 2.1.
Figura 2.1 Arquitetura de cluster de quartzo
2.2 Tabelas de banco de dados relacionadas ao cluster de quartzo
Como o cluster de quartzo depende do banco de dados, é necessário primeiro criar a tabela de banco de dados de quartzo. O pacote de liberação de quartzo inclui scripts SQL para todas as plataformas de banco de dados suportadas. Esses scripts SQL são armazenados no diretório <Quartz_home>/docs/dbtables. A versão de quartzo 1.8.4 usada aqui tem um total de 12 tabelas. O número de tabelas pode ser diferente em versões diferentes. O banco de dados é MySQL e use Tables_mysql.sql para criar uma tabela de banco de dados. Todas as tabelas são mostradas na Figura 2.2 e uma breve introdução a essas tabelas é mostrada na Figura 2.3.
Figura 2.2 Tabelas geradas no quartzo 1.8.4 no banco de dados MySQL
Figura 2.3 Introdução à tabela de dados de quartzo
2.2.1 Tabela de status do agendador (qrtz_scheduler_state)
Descrição: as informações da instância do nó no cluster, o Quartz lê as informações desta tabela regularmente para determinar o status atual de cada instância no cluster.
Instância_name: o nome configurado por org.quartz.scheduler.instanceId no arquivo de configuração. Se definido como automático, o quartzo gerará um nome com base no nome da máquina física e na hora atual.
last_checkin_time: última hora de check-in
CheckIn_Interval: Tempo de intervalo de check-in
2.2.2 Tabela de associação de gatilho e tarefas (QRTZ_FIRED_TRIGERS)
Armazena informações de status relacionadas às informações de gatilho e execução acionadas do trabalho associado.
2.2.3 Tabela de informações de gatilho (qrtz_triggers)
Trigger_name: Nome do gatilho, o usuário pode personalizar o nome à vontade, sem requisitos forçados
Trigger_group: o nome do grupo de gatilho, que o usuário pode personalizar à vontade, e não há requisito forçado.
Job_name: Chave estrangeira de QRTZ_JOB_DETAILS TABLE JOB_NAME
Job_group: qrtz_job_details tabela job_group
trigger_state: o status do gatilho atual está definido como adquirido. Se estiver definido para esperar, o trabalho não será acionado.
Trigger_cron: tipo de gatilho, usando a expressão de Cron
2.2.4 Tabela de detalhes da tarefa (qrtz_job_details)
Nota: Salve os detalhes do trabalho, a tabela precisa ser inicializada pelo usuário de acordo com a situação real
Job_Name: o nome do trabalho no cluster. O usuário pode personalizar o nome à vontade, sem requisitos forçados.
Job_Group: o nome do grupo ao qual o trabalho pertence ao cluster, que é personalizado pelo usuário à vontade, e não há requisitos forçados.
JOB_CLASS_NAME: O nome completo do pacote da classe de implementação do trabalho no cluster. O Quartz encontra a classe de trabalho com base nesse caminho para o ClassPath.
IS_DUCURUAL: Se deve persistir, defina esta propriedade como 1, o quartzo persistirá o trabalho no banco de dados
Job_Data: um campo de blob que armazena objetos de emprego persistentes.
2.2.5 Tabela de informações de permissão (qrtz_locks)
Nota: Existe uma inicialização correspondente de DML em tables_oracle.sql, como mostrado na Figura 2.4.
Figura 2.4 Informações de inicialização na tabela de informações de permissão de quartzo
2.3 Processo de inicialização do agendador de quartzo no cluster
O próprio Scheduler de quartzo não percebe que está agrupado e apenas o JOBSTORE JDBC configurado para o Scheduler saberá. Quando o agendador de quartzo é iniciado, ele chama o método ScheduleStarTed () da JobStore, que diz ao Scheduler ao JobStore que iniciou. O método ScheduleRerTarted () é implementado na classe JobStorsupport. A classe Jobstoresuport determina se a instância do agendador participa do cluster com base nas configurações no arquivo quartz.properties. Se o cluster estiver configurado, será criada uma instância de uma nova classe ClusterManager, inicializada e iniciada. O ClusterManager é uma classe embutida na classe JobStorsupport, herdando java.lang.thread, ele é executado regularmente e executa funções de check-in na instância do agendador. O Scheduler também precisa verificar se outros nós de cluster falharam. O ciclo de execução de operação de check-in está configurado em quartz.properties.
2.4 Detectando um nó do agendador com falha
Quando uma instância do agendador executa um check-in, ele verifica se outras instâncias do agendador não foram verificadas no momento em que esperavam. Isso é determinado verificando se o valor do agendador gravado na coluna Last_Chedk_time na tabela Scheduler_state é anteriormente que org.quartz.jobstore.clustercheckininterval. Se um ou mais nós não tiverem sido verificados em um horário predeterminado, o agendador em execução assume que eles falharam.
2.5 Recuperação de empregos de instâncias fracassadas
Quando uma instância do Sheduler falha ao executar um trabalho, é possível que outra instância do agendador de trabalho aceite o trabalho e a execute novamente. Para alcançar esse comportamento, a propriedade de recuperação de emprego configurada para o objeto JobDetail deve ser definida como true (Job.SetRequestSRecovery (true)). Se a propriedade recuperável estiver definida como false (o padrão for false), ela não executará novamente quando um agendador não conseguir executar o trabalho; Ele será acionado por outra instância do agendador no próximo horário de gatilho. A rapidez com que a instância do agendador pode ser detectada após uma falha depende do intervalo de check-in de cada agendador (ou seja, org.quartz.jobstore.clustercheckininterval mencionado em 2.3).
3. Instância de cluster de quartzo (quartzo+mola)
3.1 Spring incompatível com problemas de quartzo
A primavera não suporta mais o quartzo desde 2.0.2. Especificamente, quando o quartzo+mola instanta a tarefa de Quartz no banco de dados, ocorrerá um erro serializável:
<bean id = "jobTask"> <propriedade name = "TargetObject"> <ref bean = "quartzjob"/> </property> <propriedade name = "TargetMethod"> <Value> Execute </value> </Property> </bean>
O método MethodInving na classe MethodInvingJobDetailFactoryBean não suporta a serialização, por isso apresentará um erro ao serializar a tarefa de quartzo no banco de dados.
Primeiro, resolva o problema de MethodInvingJobDetailFactoryBean. Sem modificar o código -fonte da primavera, você pode evitar usar esta classe e chamar diretamente o JobDetail. No entanto, o uso da implementação do JobDetail exige que você implemente a lógica do Mothodinving por si mesmo. Você pode usar as propriedades JobClass e JobDataasMap do JobDetail para personalizar uma fábrica (gerente) para alcançar o mesmo objetivo. Por exemplo, neste exemplo, um novo myDetailquartzjobbean é criado para implementar essa função.
3.2 MyDetailquartzJobBean.java Arquivo
pacote org.lxh.mvc.jobbean; importar java.lang.reflect.method; importar org.apache.commons.logging.log; import org.apache.commons.logging.logFactory; importar org.quartz.jobexecutionContetConsept.Quartzy; org.springFramework.Context.ApplicationContext; importar org.springframework.scheduling.quartz.quartzjobbean; classe pública myDetailquartzjobbean estende o quartzjobbean {proteged loggger = logFactory.getLog (getclass); Private String TargetObject; Private String TargetMethod; ApplicationContext privado CTX; o void protegido ExecuteInternal (contexto JobExecutionContext) lança JobExecutionException {try {Logger.info ("Execute [" + TargoBject + "] de uma só vez >>>>"); Objeto oargetObject = ctx.getBean (TargetObject); Método m = nulo; tente {m = oargetObject.getClass (). getMethod (TargetMethod, nova classe [] {}); M.Invoke (OargetObject, novo objeto [] {}); } Catch (SegurançaException e) {Logger.error (e); } catch (noschmethodException e) {Logger.error (e); }} catch (Exceção e) {lança nova JobExecutionException (e); }} public void setApplicationContext (ApplicationContext ApplicationContext) {this.ctx = ApplicationContext; } public void STETTARGEGETOBJET (String TargetObject) {this.TargeGetObject = TargetObject; } public void SettargetMethod (String TargetMethod) {this.targetMethod = TargetMethod; }}3.3 Classe de implementação de emprego real
Na classe de teste, a função de imprimir o horário atual do sistema é simplesmente implementada.
pacote org.lxh.mvc.job; importar java.io.Serializable; importar java.util.date; importar org.apache.commons.logging.logs; importar org.apache.commons.logging.logFactory; public class Test Iplemments (SERIALIZABLABLE {private Logger = Logger = private estático final serialversionUid = -2073310586499744415L; public void Execute () {Data = new Date (); System.out.println (date.tolocalestring ()); }}3.4 Configure o arquivo quartz.xml
<bean id = "test" scope = "prototype"> </s bean> <bean id = "testJobTask"> <propriedade name = "jobclass"> <value> org.lxh.mvc.jobbean.myDetailQuartzjobBean </values> </propriedade <weeping) key = "TargetMethod" Value = "Execute"/> </pap> </Property> </bean> <bean name = "testTrigger"> <propriedade name = "jobDetail" ref = "testJobTask"/> <names name = "cronexpression" value = "0/1 * * * *?" /> </ Bean> <bean id = "quartzscheduler"> <propriedade name = "configLocation" value = "classPath: quartz.properties"/> <names name = "gatrigesy> <list> <ref bean =" testtrigger "/> </list> <xtkey> <erpation> name =" ApplicationContect = "
3.5 Teste
O código e a configuração do ServerA e do ServerB são exatamente iguais. Inicie o servidor primeiro e depois inicie o Serverb. Após o desligamento do servidor, o ServerB monitorará seu desligamento e assumirá o trabalho que está executando no ServerA e continuará a execução.
4. Instância de cluster de quartzo (quartzo único)
Embora tenhamos implementado a configuração do cluster do Spring+Quartz, ainda não é recomendável usar esse método devido a problemas de compatibilidade entre a primavera e o quartzo. Nesta seção, implementamos um cluster configurado separadamente com quartzo, que é simples e estável em comparação com o Spring+Quartz.
4.1 Estrutura de engenharia
Usamos o quartzo sozinho para implementar suas funções de cluster, estrutura de código e pacotes de jar de terceiros necessários, como mostra a Figura 3.1. Entre eles, a versão MySQL: 5.1.52, e a versão do driver MySQL: MySQL-Connector-Java-5.1.5-bin.jar (para 5.1.52, é recomendável usar esta versão do driver porque há um bug no quartzo que não pode ser executado normalmente quando combinado com alguns driver MySQL).
Figura 4.1 Estrutura de engenharia de cluster de quartzo e pacotes de jar de terceiros necessários
Entre eles, o quartz.properties é o arquivo de configuração de quartzo e colocado no diretório SRC. Se não houver tal arquivo, o quartzo carregará automaticamente o arquivo quartz.properties no pacote JAR; SimpleRecoveryjob.java e SimpleRecoverystratestadojob.java são dois empregos; ClusterExample.java escreve informações de agendamento, mecanismo de acionamento e funções principais de teste correspondentes.
4.2 Arquivo de configuração Quartz.properties
O nome do arquivo padrão quartz.properties é usado para ativar o recurso de cluster, configurando a propriedade "org.quartz.jobstore.isclustered" para "true". Cada instância no cluster deve ter um "ID da instância" exclusivo ("org.quartz.scheduler.instanceId" propriedade), mas deve ter o mesmo "nome da instância do agendador" ("org.quartz.scheduler.instancename"), o que significa que cada instância no cluster deve usar o mesmo arquivo de quartz.Properties "). Exceto pelas seguintes exceções, o conteúdo do arquivo de configuração deve ser o mesmo:
um. Tamanho do pool de threads.
b. Diferentes valores de atributo "org.quartz.scheduler.instanceId" (basta definir como "Auto").
#=============================================================== ================================================================== ================================================================== ================================================================== ================================================================== ================================================================== ================================================================== ================================================================== AUTO#============================================================================================== ==================================================================================================== org.quartz.jobStore.impl.jdbcjobstore.JobStoreTXorg.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegateorg.quartz.jobStore.tablePrefix = Qrtz_org.quartz.jobstore.isclustered = trueorg.quartz.jobstore.clustercheckininterval = 10000 org.quartz.jobstore.datasource = myds #================================================================================================= ================================================================================================== ================================================================================================== ================================================================================================== #=========================================================================== Org.Quartz.datasource.myds.niver com.mysql.jdbc.driverorg.quartz.dataSource.myds.url = jdbc: mysql: //192.168.31.18: 3306/teste? useunicode = true & caracteryncoding = utf-8org.quartz.datasousou.mysyds = truencoding = utf-8org.quartz.datasousou.myd.myd.myds.myds.myds.myds.myds.myds.Myds.Myds.Myds.Myds.Myds.Myds.Myds.Myds.Myds.Myds.Myds.Myds.Myds.Myds.Myds.Myds.Myds. rootorg.quartz.datasource.myds.password = 123456org.quartz.datasource.myds.maxconnections = 30#=========================================================== ============================================================== ============================================================== ============================================================== ============================================================== ============================================================== ============================================================== ============================================================== org.quartz.simpl.simpleThreadpoolorg.quartz.threadpool.threadCount = 5org.quartz.threadpool.threadpriority = 5org.quartz.threadpool.threadsIritContextClassLoaderofinitializing = true
4.3 Arquivo clusterExample.java
cluster de pacotes; importar java.util.date; importar org.quartz.jobdetail; importar org.quartz.scheduler; importar org.quartz.schedulerFactory; importSched.TamPerMPerMPertZ.spleTrigger; importação; Exceção lança {System.out.println ("**** excluindo trabalhos/gatilhos existentes *****"); // trabalhos não schedule string [] grupos = inscheduler.getTriggerGroupNames (); for (int i = 0; i <groups.length; i ++) {string [] names = inscheduler.getTriggerNames (grupos [i]); for (int j = 0; j <names.length; j ++) {inscheduler.unschedulejob (nomes [j], grupos [i]); }} // excluir grupos de empregos = inscheduler.getJobGroupNames (); for (int i = 0; i <groups.length; i ++) {string [] nomes = inscheduler.getJobNames (grupos [i]); for (int j = 0; j <names.length; j ++) {inscheduler.deleteJob (nomes [j], grupos [i]); }}} public void run (boolean inclarjobs, boolean inscheduleJobs) lança exceção {// Primeiro, devemos obter uma referência a um agendador de agendador SF = novo stdschedulerFactory (); Agendador cronograma = sf.getScheduler (); if (inclarjobs) {limpeza (agendamento); } System.out.println ("-------------------------------"); if (inscheduleJobs) { System.out.println("------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- reexecionar este trabalho se estava em andamento quando // o agendador caiu ... Job.SetRequestSRecovery (True); em: " + trigger.getNextfirETime () +" e repete: " + trigger.getRepeatCount () +" vezes, cada " + trigger.getRepeatInterval () / 1000 +" segundos "); sghting.ScheduleJob (Job, trigger); contagem ++; Jobs = JobDETAL (" ANGRAFT.Nellent. Agendador para reexecionar este trabalho se ele estava em andamento quando // o agendador caiu ... Job.SetRequestSRecovery (false); em: " + trigger.getNextfirETime () +" e repete: " + trigger.getRepeatCount () +" vezes, cada " + trigger.getRepeatInterval () / 1000 +" segundos "); sched.ScheduleJob (Job, Trigger);} // Jobs não iniciam o disparo até iniciar () System.out.println ("--------------------------------------"); System.out.println("-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Thread.sleep(3600L * 1000L); System.out.println ("------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- args.Length; }}4.4 SimpleRecoveryjob.java
cluster de pacotes; importar java.io.Serializable; importar java.util.date; importar org.apache.commons.logging.log; importar org.apache.commons.logging.OngFactory; import org.Quartz.job; import org.quartz.jobexcution; Que você deseja executar repetidamente, escreva o código relevante no método Execute, a premissa é: implemente a interface de emprego // sobre quando a classe Simplejob é instanciada e, quando o método de execução é chamado, não precisamos prestar atenção a ele e deixá -lo para a classe prateada ao prateado {private sticlt {private stick stick {private stick stick stick sloutygoveryjob _ Log Stailt {private sticklic {private stick {private stick {private stick stick stickl spleklOnGoveryJOB _ Log {private Stail LogFactory.getLog (SimpleRecoveryjob.class); public SimpleRecoveryjob () {} public void Execute (contexto JobExecutionContext) lança JobExecutionException {// Este trabalho está simplesmente imprimindo o nome do trabalho e o momento em que o trabalho está executando a String JobName = context.getJobDetail (). getfilname (); System.out.println ("Job 111111111111111111 SimpleRecoveryJob diz:" + JobName + "executando em" + new Date ()); }}4.5 Resultados da operação
A configuração e o código no servidor A e no servidor B são exatamente iguais. Método de execução: Execute o clusterExample.java em qualquer host, adicione a tarefa à programação e observe os resultados da execução:
Executar servidor, os resultados são mostrados na Figura 4.2.
Figura 4.2 Resultado da operação do servidor 1
Depois de ativar o Serverb, a saída do ServerA e do Serverb é mostrada nas Figuras 4.3 e 4.4.
Figura 4.3 Resultado em execução do servidor 2
Figura 4.4 Resultado da operação do servidorB 1
Pode ser visto nas Figuras 4.3 e 4.4 que, depois que o Serverb está ativado, o sistema percebe automaticamente a responsabilidade pelo equilíbrio e o Serverb assume o JOB1. Depois de desligar o ServerA, os resultados em execução do Serverb são mostrados na Figura 4.5.
Figura 4.5 Resultado da operação do Serverb 2
Como pode ser visto na Figura 4.5, o ServerB pode detectar que o servidor está perdido, assumir o controle da tarefa 2 e perder o servidor para o trabalho2 que precisa ser executado durante esse tempo de exceção.
5. Coisas para observar
5.1 Problema de sincronização de tempo
O quartzo não se importa se você executa nós nas mesmas máquinas ou diferentes. Quando um cluster é colocado em diferentes máquinas, ele é chamado de cluster horizontal. Quando um nó é executado na mesma máquina, ele é chamado de cluster vertical. Para clusters verticais, há um problema de ponto único de falha. Isso é inaceitável para aplicações de alta disponibilidade, porque uma vez que a máquina trava, todos os nós são encerrados. Para clusters horizontais, há um problema de sincronização do tempo.
O nó usa um registro de data e hora para notificar outras instâncias de seu último horário de check-in. Se o relógio do nó estiver definido para o horário futuro, o agendador em execução não perceberá mais que o nó caiu. Por outro lado, se o relógio de um nó estiver definido para o tempo passado, talvez o outro nó determine que o nó caiu e tente aceitar seu trabalho e executar novamente. A maneira mais fácil de sincronizar um relógio de computador é usar um servidor de tempo da Internet.
5.2 O problema dos nós competindo por empregos
Como o Quartz usa um algoritmo de balanceamento de carga aleatório, o trabalho é executado por diferentes instâncias de maneira aleatória. O site oficial da Quartz mencionou que, atualmente, não há método para atribuir (pin) um trabalho a um nó específico no cluster.
5.3 Edição para obter a lista de empregos do cluster
Atualmente, se você não inserir diretamente a consulta do banco de dados, não há uma maneira simples de obter uma lista de todos os trabalhos em execução no cluster. Solicitar uma instância do agendador receberá apenas uma lista de trabalhos em execução nessa instância. O site oficial do Quartz recomenda que você possa obter todas as informações do trabalho da tabela correspondente escrevendo algum código JDBC para acessar o banco de dados.
Resumir
O acima é o conteúdo inteiro deste artigo. Espero que o conteúdo deste artigo tenha certo valor de referência para o estudo ou trabalho de todos. Se você tiver alguma dúvida, pode deixar uma mensagem para se comunicar. Obrigado pelo seu apoio ao wulin.com.