浅析Quartz的集群配置

来源:互联网 发布:淘宝服装模特招聘要求 编辑:程序博客网 时间:2024/05/24 05:54
1、基本信息:
     Quartz是一个开源的作业调度框架,它完全由java写成,并设计用于J2Se和J2EE应用中。它提供了巨大的灵活性而不牺牲简单性。你能够用它来为执行一个作业而创建简单的或复杂的调度。它有很多特征,如:数据库支持,集群,插件,EJB作业预构建,JavaMail及其它,支持cron-like表达式等等。其中集群配置一般比较复杂,那么在Quartz中如何配置它的集群特性呢?

2 Quartz的集群配置:
     2.1 实现集群的基本原理
          Quartz是通过借助关系数据库和JDBC作业存储来实现集群管理的。
         
     1、原理:     
          集群通过故障切换和负载平衡的功能,能给调度器带来高可用性和伸缩性。目前集群只能工作在JDBC-JobStore(JobStore TX或者JobStoreCMT)方式下,从本质上来说,是使集群上的每一个节点通过共享同一个数据库来工作的(Quartz通过启动两个维护线程来维护数据库状态实现集群管理,一个是检测节点状态线程,一个是恢复任务线程)。
        负载平衡是自动完成的,集群的每个节点会尽快触发任务。当一个触发器的触发时间到达时,第一个节点将会获得任务(通过锁定),成为执行任务的节点。
                故障切换的发生是在当一个节点正在执行一个或者多个任务失败的时候。当一个节点失败了,其他的节点会检测到并且标 识在失败节点上正在进行的数据库中的任务。任何被标记为可恢复(任务详细信息的"requests recovery"属性)的任务都会被其他的节点重新执行。没有标记可恢复的任务只会被释放出来,将会在下次相关触发器触发时执行。
     2、集群管理用到的表
          --任务详细信息表
                         
    CREATE TABLE qrtz_job_details
    (
    JOB_NAME VARCHAR2(80) NOT NULL,
    JOB_GROUP VARCHAR2(80) NOT NULL,
    DESCRIPTION VARCHAR2(120) NULL,
    JOB_CLASS_NAME VARCHAR2(128) NOT NULL,
    IS_DURABLE VARCHAR2(1) NOT NULL,
    IS_VOLATILE VARCHAR2(1) NOT NULL,
    IS_STATEFUL VARCHAR2(1) NOT NULL,
    REQUESTS_RECOVERY VARCHAR2(1) NOT NULL, --可恢复标记
    JOB_DATA BLOB NULL,
    PRIMARY KEY (JOB_NAME,JOB_GROUP)
    );


          
--触发器与任务关联表

                         
     CREATE TABLE qrtz_fired_triggers
    (
    ENTRY_ID VARCHAR2(95) NOT NULL,
    TRIGGER_NAME VARCHAR2(80) NOT NULL,
    TRIGGER_GROUP VARCHAR2(80) NOT NULL,
    IS_VOLATILE VARCHAR2(1) NOT NULL,
    INSTANCE_NAME VARCHAR2(80) NOT NULL,
    FIRED_TIME NUMBER(13) NOT NULL,
    STATE VARCHAR2(16) NOT NULL,
    JOB_NAME VARCHAR2(80) NULL,
    JOB_GROUP VARCHAR2(80) NULL,
    IS_STATEFUL VARCHAR2(1) NULL,
    REQUESTS_RECOVERY VARCHAR2(1) NULL, --可恢复标记
    PRIMARY KEY (ENTRY_ID)
    );

                  --调度器状态表

                         
     CREATE TABLE qrtz_scheduler_state
    (
    INSTANCE_NAME VARCHAR2(80) NOT NULL, --调度器实例ID
    LAST_CHECKIN_TIME NUMBER(13) NOT NULL, --上次检查时间
    CHECKIN_INTERVAL NUMBER(13) NOT NULL, --检查时间间隔
    RECOVERER VARCHAR2(80) NULL, --恢复调度器
    PRIMARY KEY (INSTANCE_NAME)
    );

          

     2.2 集群配置
        通过设置"org.quartz.jobStore.isClustered"属性为"true"来激活集群特性。在集群中的每一个实例都必须有一 个唯一的"instance id" ("org.quartz.scheduler.instanceId" 属性), 但是应该有相同的"scheduler instance name" ("org.quartz.scheduler.instanceName"),也就是说集群中的每一个实例都必须使用相同的 quartz.properties 配置文件。除了以下几种例外,配置文件的内容其他都必须相同:

 不同的线程池大小,
 不同的"org.quartz.scheduler.instanceId"属性值(这个可以很容易做到,设定为"AUTO"即可)。
 注意: 永远不要在不同的机器上运行集群,除非他们的时钟是使用某种形式的同步服务(守护)非常有规律的运行(时钟必须在一分一秒内)来达到同步。还有: 永远不要触发一个非集群的实例,如果其他的实例正在同一个数据库表上运行。你将使你的数据严重腐蚀,出现非预期行为。
 示例及详细配置说明,请参照附录Quartz配置文件说明。


3 附录
          3.1 Quartz配置文件说明
               3.1.1 Quartz配置文件基本说明
               3.1.2 Quartz配置文件详细说明
                    3.1.2.1  Scheduler主要属性的配置
                        
    # Scheduler主要属性的一般定义模式如下:
    #
    # org.quartz.scheduler.instanceName = SCHED_NAME
    # org.quartz.scheduler.instanceId = INSTANCE_ID
    # org.quartz.scheduler.threadName = THREAD_NAME
    # org.quartz.scheduler.rmi.export = false
    # org.quartz.scheduler.rmi.proxy = false
    # org.quartz.scheduler.rmi.registryHost = localhost
    # org.quartz.scheduler.rmi.registryPort = 1099
    # org.quartz.scheduler.rmi.createRegistry = never
    # org.quartz.scheduler.userTransactionURL = USER_TX_LOCATION
    # org.quartz.scheduler.wrapJobExecutionInUserTransaction = JOBS_IN_USER_TX
    # org.quartz.scheduler.idleWaitTime = IDLE_WAIT_TIME
    # org.quartz.scheduler.dbFailureRetryInterval = DB_FAILURE_RETRY_INTERVAL
    # org.quartz.scheduler.classLoadHelper.class = CLASS_LOAD_HELPER_CLASS
    # org.quartz.context.key.SOME_KEY = SOME_VALUE



                3.1.2.2   线程池(ThreadPool)的配置

                        
    # 定制一个线程池的一般模式如下:
    #
    # org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
    # org.quartz.threadPool.threadCount = THREAD_COUNT
    # org.quartz.threadPool.threadPriority = THREAD_PRIO
    #
    # 简单线程池(SimpleThreadPool)的选项参数:
    #
    # org.quartz.threadPool.makeThreadsDaemons = DAEMON_THREADS
    # org.quartz.threadPool.threadsInheritGroupOfInitializingThread = INHERIT_GRP
    # org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = INHERIT_LDR
    #
    # or
    #
    # org.quartz.threadPool.class = com.mycompany.goo.FooThreadPool
    # org.quartz.threadPool.somePropOfFooThreadPool = someValue
    #

          

                     3.1.2.3 任务存储(JobStore)的配置
                        
    # 定义一个任务存储的一般模式如下:
    #
    # org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
    # org.quartz.jobStore.misfireThreshold = MISFIRE_THRESHOLD
    #
    # or
    #
    # org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.<JobStoreClass>
    # JobStoreClass 是下面其中的一个:
    # - JobStoreTX 用于单机(standalone-Quartz)实现
    # - JobStoreCMT 用于基于应用服务器容器管理事务(appserver-based container-managed transaction )的Quartz 实现
    #
    # org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.<DriverDelegateClass>
    # DriverDelegateClass 是下面其中的一个:
    # - StdJDBCDelegate (用于许多 JDBC-compliant drivers)
    # - MSSQLDelegate (用于 Microsoft SQL Server drivers)
    # - PostgreSQLDelegate (用于 PostgreSQL drivers)
    # - WebLogicDelegate (用于 WebLogic drivers)
    # - oracle.OracleDelegate (用于 Oracle drivers)
    #
    # org.quartz.jobStore.useProperties = USE_PROPERTIES
    # org.quartz.jobStore.dataSource = DS_NAME
    # org.quartz.jobStore.tablePrefix = TABLE_PREFIX
    # org.quartz.jobStore.isClustered = IS_CLUSTERED
    # org.quartz.jobStore.selectWithLockSQL = LOCKING_SELECT_STATEMENT
    # org.quartz.jobStore.dontSetAutoCommitFalse = DONT_TURN_OFF_AUTO_COMMIT
    # org.quartz.jobStore.maxMisfiresToHandleAtATime = MAX_MISFIRE_HANDLE
    # org.quartz.jobStore.txIsolationLevelSerializable = SERIALIZABLE_ISOLATION
    #
    # 如果你使用JobStoreCMT,你还需要下面的参数:
    #
    # org.quartz.jobStore.nonManagedTXDataSource = NON_MANAGED_TX_DS_NAME
    #
    # 并且如果你使用JobStoreCMT,下面的参数是可选的:
    #
    # org.quartz.jobStore.dontSetNonManagedTXConnectionAutoCommitFalse = DONT_TURN_OFF_AUTO_COMMIT
    # org.quartz.jobStore.txIsolationLevelReadCommitted = READ_COMMITTED_ISOLATION
    #
    #
    # 或者,使用一个用户自定义JobStore实现:
    #
    # org.quartz.jobStore.class = com.mycompany.goo.FooJobStore
    # org.quartz.jobStore.somePropOfFooJobStore = someValue
    #
    #

    


                    3.1.2.4 数据源的配置  
                        
    # (只有当使用JDBCJobStore时需要, 或者一个插件需要JDBC)
    # -- 如果你的Scheduler非常忙碌,比如在一定的线程池内执行相同数目的任务,那么你应让数据源的连接数等于线程数 + 1
    #
    # 数据源定义的一般模式如下:
    #
    # org.quartz.dataSource.NAME.driver = DRIVER_CLASS_NAME
    # org.quartz.dataSource.NAME.URL = DB_URL
    # org.quartz.dataSource.NAME.user = DB_USER
    # org.quartz.dataSource.NAME.password = DB_PASSWORD
    # org.quartz.dataSource.NAME.maxConnections = DB_POOL_SIZE
    # org.quartz.dataSource.NAME.validationQuery= VALIDATION_QUERY
    #
    # or
    #
    # org.quartz.dataSource.NAME.jndiURL = DB_JNDI_URL
    #
    # or
    # org.quartz.dataSource.NAME.jndiURL = DB_JNDI_URL
    # org.quartz.dataSource.NAME.jndiAlwaysLookup = DB_JNDI_ALWAYS_LOOKUP
    # org.quartz.dataSource.NAME.java.naming.factory.initial = JNDI_CTXT_FACTORY
    # org.quartz.dataSource.NAME.java.naming.provider.url = JNDI_PROVIDER_URL
    # org.quartz.dataSource.NAME.java.naming.security.principal = JNDI_PRINCIPAL
    # org.quartz.dataSource.NAME.java.naming.security.credentials = JNDI_CREDENTIALS
    #
    #

      上面显示了两种数据源定义方式:一个数据源可以用给定的数据库连接信息创建,也可以是利用应用服务器管理生成的JNDI数据源的逻辑映射。 
 


            

                    3.1.2.5 Scheduler插件的配置
                        
    # SchedulerPlugin定义的一般模式如下:
    #
    # org.quartz.plugin.NAME.class = PLUGIN_CLASS_NAME
    #
    # 如果这个插件类有一些属性值需要通过"setter"方法设定, 名称和值的属性定义如下:
    #
    # org.quartz.plugin.NAME.propName = propValue
    #
    # ..."propName" 在插件类中会有一个"setPropName"方法.但是只支持原始数据类型(包括 Strings)。
    #

      配置插件的简单示例:    

                        
    org.quartz.plugin.triggHistory.class = org.quartz.plugins.history.LoggingTriggerHistoryPlugin
    org.quartz.plugin.triggHistory.triggerFiredMessage = Trigger {1}.{0} fired job {6}.{5} at: {4, date, HH:mm:ss MM/dd/yyyy}
    org.quartz.plugin.triggHistory.triggerCompleteMessage = Trigger {1}.{0} completed firing job {6}.{5} at {4, date, HH:mm:ss MM/dd/yyyy} with resulting trigger instruction code: {9}
    
    org.quartz.plugin.jobInitializer.class = org.quartz.plugins.xml.JobInitializationPlugin
    org.quartz.plugin.jobInitializer.fileName = data/my_job_data.xml
    org.quartz.plugin.jobInitializer.overWriteExistingJobs = false
    org.quartz.plugin.jobInitializer.failOnFileNotFound = true
    
    org.quartz.plugin.shutdownhook.class = org.quartz.plugins.management.ShutdownHookPlugin
    org.quartz.plugin.shutdownhook.cleanShutdown = true

      3.1.3 示例

                        
[html] view plaincopy在CODE上查看代码片派生到我的代码片
  1. #============================================================  
  2.    # Configure Main Scheduler Properties  
  3.    #===========================================================  
  4.      
  5.    org.quartz.scheduler.instanceName = MyClusteredScheduler  
  6.    org.quartz.scheduler.instanceId = AUTO  
  7.      
  8.    #===========================================================  
  9.    # Configure ThreadPool  
  10.    #===========================================================  
  11.      
  12.    org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool  
  13.    org.quartz.threadPool.threadCount = 25  
  14.    org.quartz.threadPool.threadPriority = 5  
  15.      
  16.    #===========================================================  
  17.    # Configure JobStore  
  18.    #===========================================================  
  19.      
  20.    org.quartz.jobStore.misfireThreshold = 60000  
  21.      
  22.    org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX  
  23.    org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.oracle.OracleDelegate  
  24.    org.quartz.jobStore.useProperties = false  
  25.    org.quartz.jobStore.dataSource = myDS  
  26.    org.quartz.jobStore.tablePrefix = QRTZ_  
  27.      
  28.    org.quartz.jobStore.isClustered = true  
  29.    org.quartz.jobStore.clusterCheckinInterval = 20000  
  30.      
  31.    #===========================================================  
  32.    # Configure Datasources  
  33.    #===========================================================  
  34.      
  35.    org.quartz.dataSource.myDS.driver = oracle.jdbc.driver.OracleDriver  
  36.    org.quartz.dataSource.myDS.URL = jdbc:oracle:thin:@cluster:1521:dev  
  37.    org.quartz.dataSource.myDS.user = quartz  
  38.    org.quartz.dataSource.myDS.password = quartz  
  39.    org.quartz.dataSource.myDS.maxConnections = 5  
  40.    org.quartz.dataSource.myDS.validationQuery=select 0 from dual  



   
附件:创建数据库:
[sql] view plaincopy在CODE上查看代码片派生到我的代码片
  1. DROP TABLE IF EXISTS QRTZ_JOB_LISTENERS;  
  2. DROP TABLE IF EXISTS QRTZ_TRIGGER_LISTENERS;  
  3. DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS;  
  4. DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS;  
  5. DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE;  
  6. DROP TABLE IF EXISTS QRTZ_LOCKS;  
  7. DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS;  
  8. DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS;  
  9. DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS;  
  10. DROP TABLE IF EXISTS QRTZ_TRIGGERS;  
  11. DROP TABLE IF EXISTS QRTZ_JOB_DETAILS;  
  12. DROP TABLE IF EXISTS QRTZ_CALENDARS;  
  13.   
  14.   
  15. CREATE TABLE QRTZ_JOB_DETAILS  
  16. (  
  17. JOB_NAME VARCHAR(200) NOT NULL,  
  18. JOB_GROUP VARCHAR(200) NOT NULL,  
  19. DESCRIPTION VARCHAR(250) NULL,  
  20. JOB_CLASS_NAME VARCHAR(250) NOT NULL,  
  21. IS_DURABLE VARCHAR(1) NOT NULL,  
  22. IS_VOLATILE VARCHAR(1) NOT NULL,  
  23. IS_STATEFUL VARCHAR(1) NOT NULL,  
  24. REQUESTS_RECOVERY VARCHAR(1) NOT NULL,  
  25. JOB_DATA BLOB NULL,  
  26. PRIMARY KEY (JOB_NAME,JOB_GROUP)  
  27. );  
  28.   
  29. CREATE TABLE QRTZ_JOB_LISTENERS  
  30. (  
  31. JOB_NAME VARCHAR(200) NOT NULL,  
  32. JOB_GROUP VARCHAR(200) NOT NULL,  
  33. JOB_LISTENER VARCHAR(200) NOT NULL,  
  34. PRIMARY KEY (JOB_NAME,JOB_GROUP,JOB_LISTENER),  
  35. FOREIGN KEY (JOB_NAME,JOB_GROUP)  
  36. REFERENCES QRTZ_JOB_DETAILS(JOB_NAME,JOB_GROUP)  
  37. );  
  38.   
  39. CREATE TABLE QRTZ_TRIGGERS  
  40. (  
  41. TRIGGER_NAME VARCHAR(200) NOT NULL,  
  42. TRIGGER_GROUP VARCHAR(200) NOT NULL,  
  43. JOB_NAME VARCHAR(200) NOT NULL,  
  44. JOB_GROUP VARCHAR(200) NOT NULL,  
  45. IS_VOLATILE VARCHAR(1) NOT NULL,  
  46. DESCRIPTION VARCHAR(250) NULL,  
  47. NEXT_FIRE_TIME BIGINT(13) NULL,  
  48. PREV_FIRE_TIME BIGINT(13) NULL,  
  49. PRIORITY INTEGER NULL,  
  50. TRIGGER_STATE VARCHAR(16) NOT NULL,  
  51. TRIGGER_TYPE VARCHAR(8) NOT NULL,  
  52. START_TIME BIGINT(13) NOT NULL,  
  53. END_TIME BIGINT(13) NULL,  
  54. CALENDAR_NAME VARCHAR(200) NULL,  
  55. MISFIRE_INSTR SMALLINT(2) NULL,  
  56. JOB_DATA BLOB NULL,  
  57. PRIMARY KEY (TRIGGER_NAME,TRIGGER_GROUP),  
  58. FOREIGN KEY (JOB_NAME,JOB_GROUP)  
  59. REFERENCES QRTZ_JOB_DETAILS(JOB_NAME,JOB_GROUP)  
  60. );  
  61.   
  62. CREATE TABLE QRTZ_SIMPLE_TRIGGERS  
  63. (  
  64. TRIGGER_NAME VARCHAR(200) NOT NULL,  
  65. TRIGGER_GROUP VARCHAR(200) NOT NULL,  
  66. REPEAT_COUNT BIGINT(7) NOT NULL,  
  67. REPEAT_INTERVAL BIGINT(12) NOT NULL,  
  68. TIMES_TRIGGERED BIGINT(7) NOT NULL,  
  69. PRIMARY KEY (TRIGGER_NAME,TRIGGER_GROUP),  
  70. FOREIGN KEY (TRIGGER_NAME,TRIGGER_GROUP)  
  71. REFERENCES QRTZ_TRIGGERS(TRIGGER_NAME,TRIGGER_GROUP)  
  72. );  
  73.   
  74. CREATE TABLE QRTZ_CRON_TRIGGERS  
  75. (  
  76. TRIGGER_NAME VARCHAR(200) NOT NULL,  
  77. TRIGGER_GROUP VARCHAR(200) NOT NULL,  
  78. CRON_EXPRESSION VARCHAR(200) NOT NULL,  
  79. TIME_ZONE_ID VARCHAR(80),  
  80. PRIMARY KEY (TRIGGER_NAME,TRIGGER_GROUP),  
  81. FOREIGN KEY (TRIGGER_NAME,TRIGGER_GROUP)  
  82. REFERENCES QRTZ_TRIGGERS(TRIGGER_NAME,TRIGGER_GROUP)  
  83. );  
  84.   
  85. CREATE TABLE QRTZ_BLOB_TRIGGERS  
  86. (  
  87. TRIGGER_NAME VARCHAR(200) NOT NULL,  
  88. TRIGGER_GROUP VARCHAR(200) NOT NULL,  
  89. BLOB_DATA BLOB NULL,  
  90. PRIMARY KEY (TRIGGER_NAME,TRIGGER_GROUP),  
  91. FOREIGN KEY (TRIGGER_NAME,TRIGGER_GROUP)  
  92. REFERENCES QRTZ_TRIGGERS(TRIGGER_NAME,TRIGGER_GROUP)  
  93. );  
  94.   
  95. CREATE TABLE QRTZ_TRIGGER_LISTENERS  
  96. (  
  97. TRIGGER_NAME VARCHAR(200) NOT NULL,  
  98. TRIGGER_GROUP VARCHAR(200) NOT NULL,  
  99. TRIGGER_LISTENER VARCHAR(200) NOT NULL,  
  100. PRIMARY KEY (TRIGGER_NAME,TRIGGER_GROUP,TRIGGER_LISTENER),  
  101. FOREIGN KEY (TRIGGER_NAME,TRIGGER_GROUP)  
  102. REFERENCES QRTZ_TRIGGERS(TRIGGER_NAME,TRIGGER_GROUP)  
  103. );  
  104.   
  105.   
  106. CREATE TABLE QRTZ_CALENDARS  
  107. (  
  108. CALENDAR_NAME VARCHAR(200) NOT NULL,  
  109. CALENDAR BLOB NOT NULL,  
  110. PRIMARY KEY (CALENDAR_NAME)  
  111. );  
  112.   
  113.   
  114.   
  115. CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS  
  116. (  
  117. TRIGGER_GROUP VARCHAR(200) NOT NULL,  
  118. PRIMARY KEY (TRIGGER_GROUP)  
  119. );  
  120.   
  121. CREATE TABLE QRTZ_FIRED_TRIGGERS  
  122. (  
  123. ENTRY_ID VARCHAR(95) NOT NULL,  
  124. TRIGGER_NAME VARCHAR(200) NOT NULL,  
  125. TRIGGER_GROUP VARCHAR(200) NOT NULL,  
  126. IS_VOLATILE VARCHAR(1) NOT NULL,  
  127. INSTANCE_NAME VARCHAR(200) NOT NULL,  
  128. FIRED_TIME BIGINT(13) NOT NULL,  
  129. PRIORITY INTEGER NOT NULL,  
  130. STATE VARCHAR(16) NOT NULL,  
  131. JOB_NAME VARCHAR(200) NULL,  
  132. JOB_GROUP VARCHAR(200) NULL,  
  133. IS_STATEFUL VARCHAR(1) NULL,  
  134. REQUESTS_RECOVERY VARCHAR(1) NULL,  
  135. PRIMARY KEY (ENTRY_ID)  
  136. );  
  137.   
  138. CREATE TABLE QRTZ_SCHEDULER_STATE  
  139. (  
  140. INSTANCE_NAME VARCHAR(200) NOT NULL,  
  141. LAST_CHECKIN_TIME BIGINT(13) NOT NULL,  
  142. CHECKIN_INTERVAL BIGINT(13) NOT NULL,  
  143. PRIMARY KEY (INSTANCE_NAME)  
  144. );  
  145.   
  146. CREATE TABLE QRTZ_LOCKS  
  147. (  
  148. LOCK_NAME VARCHAR(40) NOT NULL,  
  149. PRIMARY KEY (LOCK_NAME)  
  150. );  
  151.   
  152.   
  153. INSERT INTO QRTZ_LOCKS values('TRIGGER_ACCESS');  
  154. INSERT INTO QRTZ_LOCKS values('JOB_ACCESS');  
  155. INSERT INTO QRTZ_LOCKS values('CALENDAR_ACCESS');  
  156. INSERT INTO QRTZ_LOCKS values('STATE_ACCESS');  
  157. INSERT INTO QRTZ_LOCKS values('MISFIRE_ACCESS');  
  158.   
  159.   
  160. commit;   
0 0
原创粉丝点击