1. Principio de implementación básica de la programación de tareas de cuarzo
Quartz es un proyecto de código abierto en el campo de la programación de tareas de OpenSymphony, que se basa completamente en la implementación de Java. Como un excelente marco de programación de código abierto, Quartz tiene las siguientes características:
(1) Las poderosas funciones de programación, como apoyar una variedad de métodos de programación, pueden satisfacer varias necesidades convencionales y especiales;
(2) métodos de aplicación flexibles, como admitir múltiples combinaciones de tareas y programación, y admitir múltiples métodos de almacenamiento de datos de programación;
(3) Capacidades distribuidas y de agrupación, Terracotta ha mejorado aún más sus funciones originales después de la adquisición. Este artículo se sumará a esta parte.
1.1 elementos centrales de cuarzo
Los elementos centrales de la programación de tareas de cuarzo son: planificador de tareas de programador, gatillo gatillo y tarta de trabajo. donde el activador y el trabajo son metadatos para la programación de tareas, y el programador es el controlador que realmente realiza la programación.
Trigger es un elemento utilizado para definir el tiempo de programación, es decir, según qué reglas de tiempo se ejecuta la tarea. Hay cuatro tipos de desencadenantes en cuarzo: SimpleTrigger, Crontirgger, DateIntervalTRIGG y NTHINCLUDEDDAYTRIGER. Estos cuatro desencadenantes pueden satisfacer la mayoría de las necesidades de las aplicaciones empresariales.
El trabajo se usa para representar tareas programadas. Hay principalmente dos tipos de trabajos: sin estado y con estado. Para el mismo desencadenante, los trabajos con estado no se pueden ejecutar en paralelo. Solo después de que se ejecuta la tarea activada por última vez se puede activar la próxima ejecución. El trabajo tiene dos atributos principales: volátiles y durabilidad, donde volátil significa si la tarea persiste al almacenamiento de la base de datos, mientras que la durabilidad significa si la tarea se conserva cuando no hay asociación de activación. Ambos se persisten o conservan cuando el valor es verdadero. Un trabajo puede asociarse con múltiples desencadenantes, pero un desencadenante solo puede asociar un trabajo.
Scheduler es creado por Scheduler Factory: DirectSchedulerFactory o StdsChedulerFactory. La segunda fábrica, StdsChedulerFactory, se usa con más frecuencia porque DirectSchedulerFactory no es lo suficientemente conveniente como para usar, y se requieren muchas configuraciones de codificación manuales detalladas. Hay tres tipos principales de programador: remotembanscheduler, remotescheduler y stdscheduler.
La relación entre los elementos centrales del cuarzo se muestra en la Figura 1.1:
Figura 1.1 Diagrama de relación de elemento central
1.2 Vista de hilo de cuarzo
En cuarzo, hay dos tipos de hilos, hilos de programador y hilos de ejecución de tareas, donde los hilos de ejecución de tareas generalmente usan un grupo de subprocesos para mantener un grupo de subprocesos.
Figura 1.2 Vista de hilo de cuarzo
Hay dos hilos principales para Scheduler: hilos que realizan una programación regular y hilos que ejecutan falsiñadas. Las encuestas de hilo de despacho regular se almacenan todos los desencadenantes. Si hay un desencadenante que debe activarse, es decir, ha alcanzado el momento del siguiente disparador, obtiene un hilo inactivo del grupo de hilo de ejecución de la tarea para ejecutar la tarea asociada con el disparador. El hilo de fallo escanea todos los desencadenantes para ver si hay un corrigador falso. Si es así, se maneja por separado de acuerdo con la política de fallo (fuego ahora o espere el próximo incendio).
1.3 Almacenamiento de datos de trabajo de cuarzo
Los desencadenantes y los trabajos en cuarzo deben almacenarse antes de que puedan usarse. Hay dos métodos de almacenamiento en cuarzo: Ramjobstore y JobStoreSupport, donde Ramjobstore almacena desencadenantes y trabajos en la memoria, mientras que las tiendas de almacenes de trabajo se desencadenan y trabajos en la base de datos basadas en JDBC. Ramjobstore es un acceso muy rápido, pero dado que todos los datos se perderán después de que se detenga el sistema, JobSupport debe usarse en aplicaciones de clúster.
2. Arquitectura de clúster de cuarzo del clúster de cuarzo 2.1
Cada nodo en un clúster de cuarzo es una aplicación de cuarzo independiente, que a su vez administra otros nodos. Esto significa que debe iniciar o detener cada nodo por separado. En el clúster de cuarzo, los nodos de cuarzo independientes no se comunican con otro nodo o nodo de administración, sino que perciben otra aplicación de cuarzo a través de la misma tabla de base de datos, como se muestra en la Figura 2.1.
Figura 2.1 Arquitectura de clúster de cuarzo
2.2 tablas de bases de datos relacionadas con el clúster de cuarzo
Debido a que el clúster de cuarzo depende de la base de datos, es necesario crear primero la tabla de base de datos de cuarzo. El paquete de lanzamiento de cuarzo incluye scripts SQL para todas las plataformas de base de datos compatibles. Estos scripts SQL se almacenan en el directorio <Quartz_home>/Docs/DBtables. La versión 1.8.4 de cuarzo utilizada aquí tiene un total de 12 tablas. El número de tablas puede ser diferente en diferentes versiones. La base de datos es MySQL y usa Tables_Mysql.sql para crear una tabla de base de datos. Todas las tablas se muestran en la Figura 2.2, y se muestra una breve introducción a estas tablas en la Figura 2.3.
Figura 2.2 Tablas generadas en Quartz 1.8.4 en la base de datos MySQL
Figura 2.3 Introducción a la tabla de datos de cuarzo
2.2.1 Tabla de estado del planificador (qrtz_scheduler_state)
Descripción: La información de instancia de nodo en el clúster, Quartz lee la información de esta tabla regularmente para determinar el estado actual de cada instancia en el clúster.
instance_name: el nombre configurado por org.cartz.scheduler.instanceid en el archivo de configuración. Si se establece en Auto, Quartz generará un nombre basado en el nombre físico de la máquina y la hora actual.
last_checkin_time: última hora de check-in
checkin_interval: tiempo de intervalo de check-in
2.2.2 Tabla de activación y asociación de tareas (qrtz_fired_triggers)
Toma de información de estado relacionada con la información activada de activación y ejecución del trabajo asociado.
2.2.3 Tabla de información de activación (qrtz_triggers)
Trigger_name: nombre de activación, el usuario puede personalizar el nombre a voluntad, sin requisitos forzados
Trigger_group: el nombre del grupo de activación, que el usuario puede personalizar a voluntad, y no existe un requisito forzado.
JOB_NAME: Clave exterior de QRTZ_JOB_DETAILS TABLA JOB_NAME
JOB_GROUP: QRTZ_JOB_DETAILS TABLA JOB_GROUP EXTRANSE CLAVE
Trigger_state: el estado de activación actual se establece en adquirido. Si está configurado para esperar, el trabajo no se activará.
trigger_cron: tipo de activación, usando la expresión cron
2.2.4 Tabla de detalles de la tarea (qrtz_job_details)
Nota: Guarde los detalles del trabajo, la tabla debe ser inicializada por el usuario de acuerdo con la situación real
Job_name: el nombre del trabajo en el clúster. El usuario puede personalizar el nombre a voluntad, sin ningún requisito forzado.
Job_group: el nombre del grupo al que pertenece el trabajo en el clúster, que es personalizado por el usuario a voluntad, y no hay requisitos forzados.
Job_class_name: el nombre completo del paquete de la clase de implementación del trabajo en el clúster. Quartz encuentra la clase de trabajo basada en este camino hacia ClassPath.
IS_DRIBUARIO: si persistir, establecer esta propiedad en 1, el cuarzo persistirá el trabajo en la base de datos
Job_data: un campo BLOB que almacena objetos de trabajo persistentes.
2.2.5 Tabla de información de permiso (qrtz_locks)
Nota: Hay una inicialización de DML correspondiente en Tables_oracle.sql, como se muestra en la Figura 2.4.
Figura 2.4 Información de inicialización en la tabla de información de permiso de cuarzo
2.3 Proceso de inicio del planificador de cuarzo en el clúster
El Scheduler de cuarzo en sí no se da cuenta de que está agrupado, y solo lo sabrá la tienda de trabajo JDBC configurada para el programador. Cuando comienza el planificador de cuarzo, llama al método ScheduleStarted () de la tienda de trabajo, que le dice al planificador de JobStore que ha comenzado. El método ScheduleStarted () se implementa en la clase JobStoreSupport. La clase JobStorport determina si la instancia del planificador participa en el clúster en función de la configuración en el archivo Quartz.Properties. Si el clúster está configurado, se creará, inicializará e iniciará una instancia de una nueva clase ClusterManager. ClusterManager es una clase en línea en la clase de almacenes de trabajo, heredando java.lang.thread, se ejecuta regularmente y realiza funciones de check-in en la instancia de programador. Scheduler también debe verificar si cualquier otro nodo de clúster ha fallado. El ciclo de ejecución de la operación de check-in está configurado en cuarzo.properties.
2.4 Detección de un nodo de programador fallido
Cuando una instancia de programador realiza un check-in, verifica si otras instancias de programador no han sido registradas en el momento en que esperaban. Esto se determina verificar si el valor del planificador registrado en la columna Last_Chedk_Time en la tabla Scheduler_State es anterior a Org.quartz.Jobstore.ClusterCheckInInterval. Si uno o más nodos no se han registrado en un momento predeterminado, entonces el programador en ejecución supone que han fallado.
2.5 Trabajos de recuperación de instancias fallidas
Cuando una instancia de Sheduler falla al ejecutar un trabajo, es posible que otra instancia de programador de trabajo tome el trabajo y lo ejecute nuevamente. Para lograr este comportamiento, la propiedad de recuperación de trabajo configurada en el objeto JobDetail debe establecerse en True (Job.SetRequestScovery (verdadero)). Si la propiedad recuperable se establece en falso (el valor predeterminado es falso), no volverá a emitir cuando un programador no ejecute el trabajo; Será activado por otra instancia de programador en la siguiente hora de activación. La rapidez con la que se puede detectar la instancia del planificador después de una falla depende del intervalo de registro de cada programador (es decir, org.quartz.jobstore.ClusterCheckInInterval mencionado en 2.3).
3. Instancia de clúster de cuarzo (cuarzo+resorte)
3.1 primavera incompatible con problemas de cuarzo
La primavera ya no admite cuarzo desde 2.0.2. Específicamente, cuando Quartz+Spring instancia la tarea de Quartz en la base de datos, se producirá un error en serie:
<bean id = "JobTask"> <Property Name = "TargetObject"> <ref Bean = "QuartzJob"/> </property </Property Name = "TargetMethod"> <ale Value> Ejecutar </value> </property> </bean>
El método de invocación de métodos en la clase MethodinvokingJobDetailFactoryBean no admite la serialización, por lo que lanzará un error al serializar la tarea de cuarzo en la base de datos.
Primero, resuelva el problema del método que no se acumula. Sin modificar el código fuente de Spring, puede evitar usar esta clase y llamar directamente a JobDetail. Sin embargo, el uso de la implementación de JobDetail requiere que implementa la lógica de la polotodinco por sí mismo. Puede usar las propiedades JobClass y JobDataMap de JobDetail para personalizar una fábrica (gerente) para lograr el mismo propósito. Por ejemplo, en este ejemplo, se crea un nuevo myDetailquartzBobbean para implementar esta función.
3.2 MyDetailquartzJobbean.java
paquete org.lxh.mvc.jobbean; import java.lang.reflect.method; import org.apache.commons.logging.log; import org.apache.commons.logging.logFactory; import org.quartz.jobexecutionContex org.springframework.context.applicationContext; import org.springframework.scheduling.quartz.quartzjobbean; public class myDetailquartzjobbean extiende cuarzjobbean {lo protegido logger = logFactory.getLog (getClassSss ()); cadena privada TargetObject; Cadena privada TargetMethod; Aplicación privadaContext CTX; Protected void ExecuteInternal (JobExecutionContext context) lanza JobExecutionException {try {logger.info ("Ejecutar [" + TargetObject + "] a la vez >>>>>"); Objeto oargetObject = ctx.getBean (TargetObject); Método m = nulo; intente {m = oargetObject.getClass (). getMethod (targetMethod, new class [] {}); m.invoke (oargetObject, nuevo objeto [] {}); } catch (SecurityException e) {logger.error (e); } catch (nosuchmethodexception e) {logger.error (e); }} catch (Exception e) {tire New JobExecutionException (e); }} public void setApplicationContext (ApplicationContext ApplicationContext) {this.ctx = ApplicationContext; } public void settarGetObject (String TargetObject) {this.targetObject = TargetObject; } public void settargetMethod (String TargetMethod) {this.TargetMethod = TargetMethod; }}3.3 Clase de implementación de trabajo real
En la clase de prueba, la función de imprimir la hora actual del sistema simplemente se implementa.
paquete org.lxh.mvc.job; import java.io.serializable; import java.util.date; import org.apache.commons.logging.log; import org.apache.commons.logging.logFactory; testic de clase pública implementa serializable {log logger privado = logfactory.getLog (test.classss); Private static final Long SerialVersionUid = -2073310586499744415l; public void ejecute () {date date = new Date (); System.out.println (date.tolocalEstring ()); }}3.4 Configurar el archivo cuarzo.xml
<bean id = "test" scope = "prototype"> </bean> <bean id = "testJobTask"> <Property name = "JobClass"> <alone> org.lxh.mvc.jobbean.mydetailquartzJobBean </valor> </propiedad> <property name = "JobDataMap"> <pin Key = "TargetMethod" Value = "Execute"/> </map> </property> </bean> <bean name = "testtrigger"> <propiedad name = "jobDetail" ref = "testJobtask"/> <Property name = "cronexpression" value = "0/1 * * * *?" /> </bean> <bean id = "QuartzScheduler"> <Property name = "configLocation" value = "classpath: cuarzo.properties"/> <propiedad name = "desgarradoros"> <List> <refane = "testtrigger"/> </list> </Property> <propers name = "ApplicationContextSchedulerContextey" Value = "ApplicationContex.
3.5 Prueba
El código y la configuración de ServerA y ServerB son exactamente los mismos. Inicie primero ServerA y luego inicie Serverb. Después de que el servidor se apaga, ServerB supervisará su apagado y se hará cargo del trabajo que se está ejecutando en ServerA y continuará la ejecución.
4. Instancia de clúster de cuarzo (cuarzo único)
Aunque hemos implementado la configuración del clúster de Spring+Quartz, todavía no se recomienda utilizar este método debido a problemas de compatibilidad entre Spring y Quartz. En esta sección, implementamos un clúster configurado por separado con cuarzo, que es simple y estable en comparación con Spring+Quartz.
4.1 Estructura de ingeniería
Utilizamos solo cuarzo para implementar sus funciones de clúster, estructura de código y paquetes JAR de terceros requeridos como se muestra en la Figura 3.1. Entre ellos, la versión de MySQL: 5.1.52, y la versión del controlador MySQL: MySQL-Connector-Java-5.1.5-Bin.jar (para 5.1.52, se recomienda usar esta versión del controlador porque hay un error en cuarzo que no puede ejecutarse normalmente cuando se combina con algunos controladores MySQL).
Figura 4.1 Estructura de ingeniería de clúster de cuarzo y paquetes de jarra de terceros requeridos
Entre ellos, Quartz.Properties es el archivo de configuración de cuarzo y se coloca en el directorio SRC. Si no hay dicho archivo, Quartz cargará automáticamente el archivo Quartz.Properties en el paquete JAR; SimpleRecoveryJob.java y SimpleRecoverystatefuljob.java son dos trabajos; ClusterExample.Java escribe información de programación, mecanismo de activación y funciones principales de prueba correspondientes.
4.2 FILE DE CONFIGURACIÓN Quartz.Properties
El nombre de archivo predeterminado Quartz.Properties se usa para activar la función de clúster configurando la propiedad "org.quartz.jobstore.isclustered" en "verdadero". Cada instancia en el clúster debe tener una propiedad única "ID de instancia" ("org.quartz.scheduler.instanceid"), pero debe tener el mismo "nombre de instancia de planificador" ("org.quartz.scheduler.instancename"), lo que significa que cada instancia en el clúster debe usar el mismo archivo de configuración de curarz.properties. Excepto por las siguientes excepciones, el contenido del archivo de configuración debe ser el mismo:
a. Tamaño de la piscina de hilos.
b. Diferentes valores de atributo "org.quartz.scheduler.instanceid" (solo configurado en "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.driver = com.mysql.jdbc.driverorg.quartz.datasource.myds.url = jdbc: mysql: //192.168.31.18: 3306/test? useUnicode = true & caractericoding = utf-8org.quartz.datasource.myds.user = roadorg.quartz.datasource.myds.password = 123456org.quartz.datasource.myds.maxconnections = 30#============================================================= ============================================================== ============================================================== ============================================================== ============================================================== ============================================================== ============================================================== ============================================================== org.quartz.simpl.simplethreadpoolorg.quartz.threadpool.threadCount = 5org.quartz.threadpool.threadpriority = 5org.quartz.threadpool.threadsinheritContextClassLoaderofinitialization
4.3 archivo clusterexample.java
paquete clúster; import java.util.date; import org.quartz.jobdetail; import org.quartz.scheduler; import org.quartz.schedulerfactory; importar org.quartz.simpletrger; import. Excepción {System.out.println ("**** Eliminar trabajos/disparadores existentes *****"); // sin selección de trabajos cadena [] 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 (nombres [j], grupos [i]); }} // eliminar grupos de trabajos = inscheduler.getJobGroupNames (); for (int i = 0; i <groups.length; i ++) {string [] names = inscheduler.getJobnames (grupos [i]); for (int j = 0; j <names.length; j ++) {inscheduler.deleteJob (nombres [j], grupos [i]); }}} public void run (boolean inclearJobs, boolean inscheduleJobs) lanza la excepción {// Primero debemos obtener una referencia a un planificador SchedulerFactory SF = New StdsChedulerFactory (); Scheduler Sched = sf.getScheduler (); if (inclearJobs) {Cleanup (Sched); } System.out.println ("-------------------------------"); if (inscheduleJobs) { System.out.println("------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Vuelva a ejecutar este trabajo si estaba en progreso cuando el planificador se fue ... " + trigger.getNextFireTime () +" y repita: " + trigger.getRepeatCount () +" Times, cada " + trigger.getRepeatInterval () / 1000 +" segundos "); sched.scheduleJob (trabajo, thrigger); count ++; trabajo = nuevo JobDetail (" JOB_ " + Count, SCHELD, SimplerEChroverystatefulfulyfulfuly); Scheduler para volver a ejecutar este trabajo si estaba en progreso cuando el programador se fue ... AT: " + TRIGG.GETNEXTFIRETIME () +" y repita: " + Trigger.getRepeatCount () +" Times, cada " + Trigger.getRepeatInterval () / 1000 +" Seconds "); Sched.scheduleJob (Job, Trigger);} // Los trabajos no comienzan a disparar hasta que se ha llamado ... System.out.println ("------------------------------------"); System.out.println("-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Thread.sleep(3600L * 1000l); System.out.println ("---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ----------------------------------------------------------------------------------------------- args.length; }}4.4 SimpleRecoveryJob.java
paquete clúster; import java.io.Serializable; import java.util.date; importar org.apache.commons.logging.log; importar org.apache.commons.logging.logFactory; importar org.quartz.job; import.quartz.jobExecutionConTcont; que desea ejecutar repetidamente, escriba el código relevante en el método Ejecutar, la premisa es: implementar la interfaz de trabajo // En cuanto a cuando se instancia la clase SimpleJob y cuando se llama el método de ejecución, no necesitamos prestarle atención y dejarla a QuartzPublicCoveryJob Implement Job, serializable {log _Log = logFactory.getLog (getLoG. public SimpleRecoveryJob () {} public void Ejecute (JobExecutionContext context) lanza JobExecutionException {// Este trabajo simplemente imprime el nombre del trabajo y el momento en que el trabajo ejecuta String JobName = context.getJobDetail (). GetFullName (); System.out.println ("Job 111111111111111111 SimpleCoveryJob dice:" + JobName + "Ejecutando en" + nueva fecha ()); }}4.5 Resultados de la operación
La configuración y el código en el servidor A y el servidor B son exactamente los mismos. Método de ejecución: ejecute clusterExample.java en cualquier host, agregue la tarea al horario y observe los resultados de ejecución:
Ejecutar ServerA, los resultados se muestran en la Figura 4.2.
Figura 4.2 Resultado de operación de ServerA 1
Después de encender ServerB, la salida de ServerA y Serverb se muestra en las Figuras 4.3 y 4.4.
Figura 4.3 ServerA Ejecutando el resultado 2
Figura 4.4 Resultado de operación del servidorb 1
Se puede ver en las Figuras 4.3 y 4.4 que después de que ServerB se enciende, el sistema se da cuenta automáticamente de la responsabilidad del equilibrio, y ServerB se hace cargo de Job1. Después de cerrar ServerA, los resultados en ejecución de ServerB se muestran en la Figura 4.5.
Figura 4.5 Resultado de operación de serverb 2
Como se puede ver en la Figura 4.5, ServerB puede detectar que ServerA se pierde, hacerse cargo de la tarea Job2 y perder ServerA al Job22 que debe ejecutarse durante este tiempo de excepción.
5. cosas a tener en cuenta
5.1 Problema de sincronización de tiempo
A Quartz realmente no le importa si ejecuta nodos en las mismas o diferentes máquinas. Cuando se coloca un clúster en diferentes máquinas, se llama clúster horizontal. Cuando un nodo se ejecuta en la misma máquina, se llama clúster vertical. Para los grupos verticales, hay un problema del punto de falla único. Esto es inaceptable para aplicaciones de alta disponibilidad, porque una vez que la máquina se bloquea, todos los nodos terminan. Para los grupos horizontales, hay un problema de sincronización del tiempo.
El nodo usa una marca de tiempo para notificar a otras instancias de su último tiempo de registro. Si el reloj del nodo está configurado en el tiempo futuro, el programador en ejecución ya no se dará cuenta de que el nodo ha caído. Por otro lado, si el reloj de un nodo se establece en la hora pasada, tal vez el otro nodo determinará que el nodo se ha caído e intenta tomar su trabajo y volver a ejecutar. La forma más fácil de sincronizar un reloj de computadora es usar un servidor de tiempo de Internet.
5.2 El problema de los nodos que compiten por trabajos
Debido a que Quartz utiliza un algoritmo de equilibrio de carga aleatorio, el trabajo se ejecuta por diferentes instancias de manera aleatoria. El sitio web oficial de Quartz mencionó que en la actualidad, no hay un método para asignar (PIN) un trabajo a un nodo específico en el clúster.
5.3 Problema para obtener la lista de trabajo del clúster
Actualmente, si no ingresa directamente la consulta de la base de datos, no hay una manera simple de obtener una lista de todos los trabajos de ejecución en el clúster. Solicitar una instancia de planificador solo obtendrá una lista de trabajos que se ejecutan en esa instancia. El sitio web oficial de Quartz recomienda que pueda obtener toda la información de trabajo de la tabla correspondiente escribiendo algún código JDBC para acceder a la base de datos.
Resumir
Lo anterior es todo el contenido de este artículo. Espero que el contenido de este artículo tenga cierto valor de referencia para el estudio o el trabajo de todos. Si tiene alguna pregunta, puede dejar un mensaje para comunicarse. Gracias por su apoyo a Wulin.com.