quartz2.3.0(十五)执行、暂停、继续执行、清除,花式操作数据库中持久化的job任务
#############################################################################################################################################
1、 建立数据库11张表
#############################################################################################################################################
先在数据库中建立quartz需要的11张表(我这里用的是Oracle数据库),根据不同的数据库quartz分别提供了不同的初始化sql文件,sql文件路径在 quartz-2.3.0-SNAPSHOT-0724\src\org\quartz\impl\jdbcjobstore下:
tables_cloudscape.sql
tables_cubrid.sql
tables_db2.sql
tables_db2_v8.sql
tables_db2_v72.sql
tables_db2_v95.sql
tables_derby.sql
tables_derby_previous.sql
tables_firebird.sql
tables_h2.sql
tables_hsqldb.sql
tables_hsqldb_old.sql
tables_informix.sql
tables_mysql.sql
tables_mysql_innodb.sql
tables_oracle.sql
tables_pointbase.sql
tables_postgres.sql
tables_sapdb.sql
tables_solid.sql
tables_sqlServer.sql
tables_sybase.sql
#############################################################################################################################################
2、 配置定时器数据库等相关配置:quartz.properties
#############################################################################################################################################
#============================================================================
# Configure Main Scheduler Properties
#============================================================================
#调度器实例名称
org.quartz.scheduler.instanceName: SchedulerJoyce0725
org.quartz.scheduler.instanceId: InstanceJoyce0725 #============================================================================
# Configure ThreadPool
#============================================================================
org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount: 5
org.quartz.threadPool.threadPriority: 1 #============================================================================
# 配置Oracle数据库,命名dataSource为myDS
#============================================================================
### 支持PostgreSQL数据库
###org.quartz.dataSource.myDS.driver=org.postgresql.Driver
###org.quartz.dataSource.myDS.URL=jdbc:postgresql://localhost:5432/quartz
org.quartz.dataSource.myDS.driver=oracle.jdbc.driver.OracleDriver
org.quartz.dataSource.myDS.URL=jdbc:oracle:thin:@localhost:1521:orcl
org.quartz.dataSource.myDS.user=zhuwen
org.quartz.dataSource.myDS.password=ZHUwen12
org.quartz.dataSource.myDS.maxConnections=5
org.quartz.dataSource.myDS.validationQuery=select 0 FROM DUAL #============================================================================
# 配置job任务存储策略,指定一个叫myDS的dataSource
#============================================================================
org.quartz.jobStore.misfireThreshold: 60000
org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
### 支持PostgreSQL数据库
###org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.PostgreSQLDelegate
### 支持Oracle数据库
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.oracle.OracleDelegate
org.quartz.jobStore.useProperties=false
org.quartz.jobStore.dataSource=myDS
org.quartz.jobStore.tablePrefix=QRTZ_
org.quartz.jobStore.isClustered=true
################################################################################################################
3、 11张表不同定时方式分别存储了不同数据到不同的表
################################################################################################################
ScheduleBuilder是trigger触发器的触发规则定制类,旗下有4种触发器实现类: CalendarIntervalScheduleBuilder、CronScheduleBuilder、DailyTimeIntervalScheduleBuilder、SimpleScheduleBuilder。
这里演示了CronScheduleBuilder和SimpleScheduleBuilder两种定时方式,分别执行后面的StoreSimpleTrigger2OracleExample.java 和 StoreCronTrigger2OracleExample.java 就能看到数据库如下的差别:
这4中实现类在数据库的11张表中存储一个任务时,分别会产生不一样的数据,用颜色标注insert语句如下:
select * from qrtz_blob_triggers; --没有insert语句就表示此表一直没有数据
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
select * from qrtz_calendars;
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
select * from qrtz_cron_triggers;
Insert into QRTZ_CRON_TRIGGERS (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP,CRON_EXPRESSION,TIME_ZONE_ID)
values ('SchedulerJoyce0725','cronTrigger','cronGroup1','0/5 * * * * ?','Asia/Shanghai');
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
select * from qrtz_fired_triggers;
Insert into QRTZ_FIRED_TRIGGERS (SCHED_NAME,ENTRY_ID,TRIGGER_NAME,TRIGGER_GROUP,INSTANCE_NAME,FIRED_TIME,SCHED_TIME,PRIORITY,STATE,JOB_NAME,JOB_GROUP,IS_NONCONCURRENT,REQUESTS_RECOVERY)
values ('SchedulerJoyce0725','InstanceJoyce07251564061848994','cronTrigger','cronGroup1','InstanceJoyce0725',1564061855002,1564061855000,5,'EXECUTING','simpleRecoveryJob','cronGroup1','0','0'); --每一次远程启动时ENTRY_ID总是会变一变
Insert into QRTZ_FIRED_TRIGGERS (SCHED_NAME,ENTRY_ID,TRIGGER_NAME,TRIGGER_GROUP,INSTANCE_NAME,FIRED_TIME,SCHED_TIME,PRIORITY,STATE,JOB_NAME,JOB_GROUP,IS_NONCONCURRENT,REQUESTS_RECOVERY) values ('SchedulerJoyce0725','InstanceJoyce07251564066439267','simpleTriger1','simpleGroup','InstanceJoyce0725',1564066446293,1564066451270,5,'ACQUIRED',null,null,'0','0');
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
select * from qrtz_job_details;
Insert into QRTZ_JOB_DETAILS (SCHED_NAME,JOB_NAME,JOB_GROUP,DESCRIPTION,JOB_CLASS_NAME,IS_DURABLE,IS_NONCONCURRENT,IS_UPDATE_DATA,REQUESTS_RECOVERY,JOB_DATA)
values ('SchedulerJoyce0725','simpleRecoveryJob','cronGroup1',null,'org.quartz.examples.example15.SimpleRecoveryJob','0','0','0','0',
TO_BLOB(HEXTORAW('...'))|| TO_BLOB(HEXTORAW('...')));
Insert into QRTZ_JOB_DETAILS (SCHED_NAME,JOB_NAME,JOB_GROUP,DESCRIPTION,JOB_CLASS_NAME,IS_DURABLE,IS_NONCONCURRENT,IS_UPDATE_DATA,REQUESTS_RECOVERY,JOB_DATA)
values ('SchedulerJoyce0725','simpleJob1','simpleGroup',null,'org.quartz.examples.example15.SimpleRecoveryJob','0','0','0','1',
TO_BLOB(HEXTORAW('...'))|| TO_BLOB(HEXTORAW('...')));
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
select * from qrtz_locks;
Insert into qrtz_locks (SCHED_NAME,LOCK_NAME) values ('SchedulerJoyce0725','STATE_ACCESS');
Insert into qrtz_locks (SCHED_NAME,LOCK_NAME) values ('SchedulerJoyce0725','TRIGGER_ACCESS');
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
select * from qrtz_paused_trigger_grps;
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
select * from qrtz_scheduler_state;
Insert into qrtz_scheduler_state (SCHED_NAME,INSTANCE_NAME,LAST_CHECKIN_TIME,CHECKIN_INTERVAL)
values ('SchedulerJoyce0725','InstanceJoyce0725',1564061857522,7500);
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
select * from qrtz_simple_triggers;
Insert into QRTZ_SIMPLE_TRIGGERS (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP,REPEAT_COUNT,REPEAT_INTERVAL,TIMES_TRIGGERED)
values ('SchedulerJoyce0725','simpleTriger1','simpleGroup',20,5000,2);
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
select * from qrtz_simprop_triggers;
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
select * from qrtz_triggers;
Insert into qrtz_triggers (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP,JOB_NAME,JOB_GROUP,DESCRIPTION,NEXT_FIRE_TIME,PREV_FIRE_TIME,PRIORITY,TRIGGER_STATE,TRIGGER_TYPE,START_TIME,END_TIME,CALENDAR_NAME,MISFIRE_INSTR,JOB_DATA)
values ('SchedulerJoyce0725','cronTrigger','cronGroup1','simpleRecoveryJob','cronGroup1',null,1564061865000,1564061860000,5,'ACQUIRED','CRON',1564061849000,0,null,0, EMPTY_BLOB());
Insert into QRTZ_TRIGGERS (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP,JOB_NAME,JOB_GROUP,DESCRIPTION,NEXT_FIRE_TIME,PREV_FIRE_TIME,PRIORITY,TRIGGER_STATE,TRIGGER_TYPE,START_TIME,END_TIME,CALENDAR_NAME,MISFIRE_INSTR,JOB_DATA) values ('SchedulerJoyce0725','simpleTriger1','simpleGroup','simpleJob1','simpleGroup',null,1564066451270,1564066446270,5,'ACQUIRED','SIMPLE',1564066441270,0,null,0, EMPTY_BLOB());
#############################################################################################################################################
4、 job任务类,SimpleRecoveryJob.java
#############################################################################################################################################
package org.quartz.examples.example15; import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import java.util.Date; /**
* 一个job作业。
*/
public class SimpleRecoveryJob implements Job { private static Logger LOG = LoggerFactory.getLogger(SimpleRecoveryJob.class); private static final String COUNT = "count"; //必须要有public修饰的无参构造函数
public SimpleRecoveryJob() {
} //任务执行方法
public void execute(JobExecutionContext context) throws JobExecutionException { JobKey jobKey = context.getJobDetail().getKey(); // 如果由于“恢复”情况而重新执行作业,此方法将返回true。
if (context.isRecovering()) {
LOG.info("恢复作业:SimpleRecoveryJob: " + jobKey + " RECOVERING at " + new Date());
} else {
LOG.info("不恢复作业:SimpleRecoveryJob: " + jobKey + " starting at " + new Date());
} JobDataMap data = context.getJobDetail().getJobDataMap();
int count;
if (data.containsKey(COUNT)) {
count = data.getInt(COUNT);
} else {
count = 0;
}
count++;
data.put(COUNT, count); LOG.info("SimpleRecoveryJob: " + jobKey + " done at " + new Date() + "\n Execution #" + count); } }
#############################################################################################################################################
5、 简单定时任务存储到数据库表,StoreSimpleTrigger2OracleExample.java
#############################################################################################################################################
package org.quartz.examples.example15; import static org.quartz.DateBuilder.futureDate;
import static org.quartz.JobBuilder.newJob;
import static org.quartz.SimpleScheduleBuilder.simpleSchedule;
import static org.quartz.TriggerBuilder.newTrigger; import org.quartz.DateBuilder.IntervalUnit;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerFactory;
import org.quartz.SimpleTrigger;
import org.quartz.impl.StdSchedulerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; /**
* 简单定时任务存储到数据库表。
* 存储job任务到数据库,这里打印的job任务都是: 不恢复作业……
*/
public class StoreSimpleTrigger2OracleExample { private static Logger LOG = LoggerFactory.getLogger(StoreSimpleTrigger2OracleExample.class); public void run(boolean inClearJobs, boolean inScheduleJobs) throws Exception { // 初始化调度器
SchedulerFactory sf = new StdSchedulerFactory();
Scheduler sched = sf.getScheduler(); if (inClearJobs) {
sched.clear();
LOG.warn("***** Deleted existing jobs/triggers *****");
} LOG.info("------- Initialization Complete -----------"); if (inScheduleJobs) { LOG.info("------- Scheduling Jobs ------------------"); String schedId = sched.getSchedulerInstanceId(); // ========================================================
// ============ job1
// ========================================================
int count = 1;
JobDetail job = newJob(SimpleRecoveryJob.class).withIdentity("simpleJob" + count, "simpleGroup").requestRecovery() // 如果job执行过程中宕机,则job重新执行
.build();
SimpleTrigger trigger = newTrigger().withIdentity("simpleTriger" + count, "simpleGroup")
.startAt(futureDate(1, IntervalUnit.SECOND))
.withSchedule(simpleSchedule().withRepeatCount(20).withIntervalInSeconds(5)).build();
LOG.info(job.getKey() + " will run at: " + trigger.getNextFireTime() + " and repeat: "
+ trigger.getRepeatCount() + " times, every " + trigger.getRepeatInterval() / 1000 + " seconds");
sched.scheduleJob(job, trigger);
//
// // ========================================================
// // ============ job2
// // ========================================================
// count++;
// job = newJob(SimpleRecoveryJob.class).withIdentity("job0724_" + count, schedId).requestRecovery() // 如果job执行过程中宕机,则job重新执行
// .build();
// trigger = newTrigger().withIdentity("triger0724_" + count, schedId).startAt(futureDate(2, IntervalUnit.SECOND))
// .withSchedule(simpleSchedule().withRepeatCount(20).withIntervalInSeconds(5)).build();
// LOG.info(job.getKey() + " will run at: " + trigger.getNextFireTime() + " and repeat: "
// + trigger.getRepeatCount() + " times, every " + trigger.getRepeatInterval() / 1000 + " seconds");
// sched.scheduleJob(job, trigger); } LOG.info("------- Starting Scheduler ---------------");
sched.start();
try {
Thread.sleep(3600L * 1000L);
} catch (Exception e) {
//
}
sched.shutdown();
LOG.info("------- Shutdown Complete ----------------");
} public static void main(String[] args) throws Exception {
boolean clearJobs = true; // 是否清空job任务
boolean scheduleJobs = true; // 是否调度任务 for (String arg : args) {
if (arg.equalsIgnoreCase("clearJobs")) {
clearJobs = true;
} else if (arg.equalsIgnoreCase("dontScheduleJobs")) {
scheduleJobs = false;
}
} StoreSimpleTrigger2OracleExample example = new StoreSimpleTrigger2OracleExample();
example.run(clearJobs, scheduleJobs);
}
}
#############################################################################################################################################
6、 cron定义定时任务存储到数据库表,StoreCronTrigger2OracleExample.java
#############################################################################################################################################
package org.quartz.examples.example15; import static org.quartz.CronScheduleBuilder.cronSchedule;
import static org.quartz.JobBuilder.newJob;
import static org.quartz.TriggerBuilder.newTrigger; import java.util.Date; import org.quartz.CronTrigger;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerFactory;
import org.quartz.impl.StdSchedulerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* cron定义定时任务存储到数据库表。
* 存储job任务到数据库,这里打印的job任务都是: 不恢复作业……
*/
public class StoreCronTrigger2OracleExample { private static Logger LOG = LoggerFactory.getLogger(StoreCronTrigger2OracleExample.class); public void run(boolean inClearJobs) throws Exception { // 初始化调度器
SchedulerFactory sf = new StdSchedulerFactory();
Scheduler sched = sf.getScheduler(); if (inClearJobs) {
sched.clear();
LOG.warn("***** Deleted existing jobs/triggers *****");
} // ========================================================
// ============ job1 每20秒执行一次,无限期重复
// ========================================================
JobDetail job = newJob(SimpleRecoveryJob.class).withIdentity("simpleRecoveryJob_Recovery", "cronGroup2")
.requestRecovery() //如果job任务所在服务宕机了或由于其它原因job任务被中断,请标记JobExecutionContext.isRecovering()=true。
//让我可以拿这个标记知道怎么去适配业务场景。
//但是只有恢复任务后首次执行任务时,拿到Recovering标记值为true,此后该任务Recovering值又标记为了false。
.build();
//每5秒执行一次
CronTrigger trigger = newTrigger().withIdentity("cronTrigger_Recovery", "cronGroup2").withSchedule(cronSchedule("0/5 * * * * ?")).build();
Date ft = sched.scheduleJob(job, trigger);
LOG.info(job.getKey() + " has been scheduled to run at: " + ft + " and repeat based on expression: "
+ trigger.getCronExpression()); LOG.info("------- Starting Scheduler ---------------");
sched.start();
try {
Thread.sleep(3600L * 1000L);
} catch (Exception e) {
//
} // 暂停执行任务
sched.pauseJob(job.getKey());
LOG.info("调度器暂停执行定时器,主线程睡眠11秒!!!!会错过执行job1的N次定时任务。模拟当定时器的执行线程由于抢不到CPU时间或其他事件错过执行的情况。");
Thread.sleep(11L * 1000L);
// 继续执行任务
sched.resumeJob(job.getKey()); //当定时器得到继续执行的命令时,被错过执行的任务次数,就会按照misfire的定义去执行 sched.shutdown();
LOG.info("------- Shutdown Complete ----------------");
} public static void main(String[] args) throws Exception {
boolean clearJobs = false; // 是否清空job任务 StoreCronTrigger2OracleExample example = new StoreCronTrigger2OracleExample();
example.run(clearJobs);
}
}
#############################################################################################################################################
7、 执行数据库中的定时任务,RunOracleExistingJobExample.java
#############################################################################################################################################
package org.quartz.examples.example15; import org.quartz.Scheduler;
import org.quartz.SchedulerFactory;
import org.quartz.impl.StdSchedulerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; /**
* 先运行ClusterExample.java,确定Oracle数据库中已存在job任务.
* 这里打印的job任务都是从数据库表里恢复回来的,所以这些任务的第一次执行会打印: 恢复作业……
* 此后job任务就只打印:不恢复作业……
*/
public class RunOracleExistingJobExample { private static Logger LOG = LoggerFactory.getLogger(RunOracleExistingJobExample.class); public void run(boolean inClearJobs) throws Exception { // 初始化调度器
SchedulerFactory sf = new StdSchedulerFactory();
Scheduler sched = sf.getScheduler(); if (inClearJobs) {
sched.clear();
LOG.warn("***** Deleted existing jobs/triggers *****");
} LOG.info("------- Starting Scheduler ---------------");
sched.start();
try {
Thread.sleep(3600L * 1000L);
} catch (Exception e) {
//
} sched.shutdown();
LOG.info("------- Shutdown Complete ----------------");
} public static void main(String[] args) throws Exception {
boolean clearJobs = false; // 是否清空job任务,这里为不清空 RunOracleExistingJobExample example = new RunOracleExistingJobExample();
example.run(clearJobs);
}
}
quartz2.3.0(十五)执行、暂停、继续执行、清除,花式操作数据库中持久化的job任务的更多相关文章
- 孤荷凌寒自学python第五十二天初次尝试使用python读取Firebase数据库中记录
孤荷凌寒自学python第五十二天初次尝试使用python读取Firebase数据库中记录 (完整学习过程屏幕记录视频地址在文末) 今天继续研究Firebase数据库,利用google免费提供的这个数 ...
- 【hibernate spring data jpa】执行了save()方法 sql语句也执行了,但是数据并未插入数据库中
执行了save()方法 sql语句也执行了,但是数据并未插入数据库中 解决方法: 是因为执行了save()方法,也执行了sql语句,但是因为使用的是 @Transactional 注解,不是手动去提 ...
- Java-JUC(十五):synchronized执行流程分析
一.锁对象及 synchronized 的使用 synchronized 通过互斥锁(Mutex Lock)来实现,同一时刻,只有获得锁的线程才可以执行锁内的代码. 锁对象分为两种: 实例对象(一个类 ...
- 从零开始学ios开发(十五):Navigation Controllers and Table Views(中)
这篇内容我们继续上一篇的例子接着做下去,为其再添加3个table view的例子,有了之前的基础,学习下面的例子会变得很简单,很多东西都是举一反三,稍稍有些不同的内容,好了,闲话少说,开始这次的学习. ...
- ExpandoObject与DynamicObject的使用 RabbitMQ与.net core(一)安装 RabbitMQ与.net core(二)Producer与Exchange ASP.NET Core 2.1 : 十五.图解路由(2.1 or earler) .NET Core中的一个接口多种实现的依赖注入与动态选择看这篇就够了
ExpandoObject与DynamicObject的使用 using ImpromptuInterface; using System; using System.Dynamic; names ...
- python语言(五)匿名函数、读写excel、操作数据库、加密、redis操作
一.匿名函数 递归:就是调用自己 def func(): num = int(input('num:')) if num % 2 ==0: print('是偶数') return else: func ...
- quartz2.3.0(五)制定错过执行任务的misfire策略,用pause,resume模拟job暂停执行和继续执行
感谢兄台: <quartz-misfire 错失.补偿执行> misfire定义 misfire:被错过的执行任务策略 misfire重现——CronTrigger job任务类: pac ...
- 第十五篇:使用 FP-growth 算法高效挖掘海量数据中的频繁项集
前言 对于如何发现一个数据集中的频繁项集,前文讲解的经典 Apriori 算法能够做到. 然而,对于每个潜在的频繁项,它都要检索一遍数据集,这是比较低效的.在实际的大数据应用中,这么做就更不好了. 本 ...
- 十五、CI框架之自动加载数据库
一.在config的autoload.php文件中,如果写入以下代码,那么在控制器中无需再次加载数据库了,相当于全局自动加载数据库了 不忘初心,如果您认为这篇文章有价值,认同作者的付出,可以微信二维码 ...
随机推荐
- Codevs 3122 奶牛代理商 VIII(状压DP)
3122 奶牛代理商 VIII 时间限制: 3 s 空间限制: 256000 KB 题目等级 : 大师 Master 题目描述 Description 小徐是USACO中国区的奶牛代理商,专门出售质优 ...
- CF888G 【Xor-MST】
妙妙题-- 看到\(MST\),想到\(Kruskal\),看到异或,想到\(Trie\) 首先我们模拟一下\(Kruskal\)的流程:找到最小边,如果联通就忽略,未联通就加边 我们把所有点权值加入 ...
- Linux修改服务器Oracle字符集
Linux安装Oracle时太仓促,没设置好,导入dmp字符集(ZHS16GBK)与服务器字符集(WE8MSWIN1252)对不上,导致导入数据失败: [oracle@ORACLE ~]$ sqlpl ...
- 网络营销CPA、CPS、CPM、CPT、CPC 是什么
网络营销之所以越来越受到重视一个主要的原因就是因为“精准”.相比较传统媒体的陈旧广告形式,网络营销能为广告主带来更为确切的效果与回报,更有传统媒体所没有的即时互动性.很多企业借助于精准的网络营销成为人 ...
- XmlIgnore的解释和使用
XmlIgnore是一个自定义属性,用来指明在序列化时是否序列化一个属性.如下面的例子: public class Group { public string GroupName; [XmlIgnor ...
- Java实现批量将word文档转换成PDF
先导入words的jar包 需要jar包的私聊我发你 代码如下:import com.aspose.words.Document;import java.io.File; public class W ...
- redis 锁的案例
1: redis 锁 作为一种术装饰器使用 基本逻辑: 1:声明一个redislock类 定义生成锁和释放锁两个方法 2:生成锁使用了一个默认值 setnx ; 如果当前时间大于 第一次锁的生成时间 ...
- App installation failed (A valid provisioning profile for this executable was not found)
真机调试build success ,App installation failed (A valid provisioning profile for this executable was not ...
- [LeetCode] 366. Find Leaves of Binary Tree 找二叉树的叶节点
Given a binary tree, find all leaves and then remove those leaves. Then repeat the previous steps un ...
- UE4 window打包ios备忘
1.生成SHH key 2.安装证书 *.cer,*.p12 以下转自:http://wangjie.rocks/2017/11/30/ue4-ios-build-on-windows/ 问题一 12 ...