Springboot Quartz 任務(wù)調(diào)度框架

1.maven依賴

maven依賴

2. Quartz 核心概念

1.Job 表示一個(gè)工作搪锣,要執(zhí)行的具體內(nèi)容秋忙。

2.JobDetail 表示一個(gè)具體的可執(zhí)行的調(diào)度程序,Job 是這個(gè)可執(zhí)行程調(diào)度程序所要執(zhí)行的內(nèi)容构舟,另外 JobDetail 還包含了這個(gè)任務(wù)調(diào)度的方案和策略灰追。

3.Trigger 代表一個(gè)調(diào)度參數(shù)的配置,什么時(shí)候去調(diào)狗超。

4.Scheduler 代表一個(gè)調(diào)度容器弹澎,一個(gè)調(diào)度容器中可以注冊(cè)多個(gè) JobDetail 和 Trigger。當(dāng) Trigger 與 JobDetail 組合努咐,就可以被 Scheduler 容器調(diào)度了苦蒿。


jobDetail

Scheduler 通過jobDetail識(shí)別將要調(diào)用的job的類型,來調(diào)用job工作

我們傳給scheduler一個(gè)JobDetail實(shí)例渗稍,因?yàn)槲覀冊(cè)趧?chuàng)建JobDetail時(shí)佩迟,將要執(zhí)行的job的類名傳給了JobDetail,所以scheduler就知道了要執(zhí)行何種類型的job竿屹;每次當(dāng)scheduler執(zhí)行job時(shí)报强,在調(diào)用其execute(…)方法之前會(huì)創(chuàng)建該類的一個(gè)新的實(shí)例;執(zhí)行完畢拱燃,對(duì)該實(shí)例的引用就被丟棄了秉溉,實(shí)例會(huì)被垃圾回收;這種執(zhí)行策略帶來的一個(gè)后果是扼雏,job必須有一個(gè)無參的構(gòu)造函數(shù)(當(dāng)使用默認(rèn)的JobFactory時(shí))坚嗜;另一個(gè)后果是夯膀,在job類中诗充,不應(yīng)該定義有狀態(tài)的數(shù)據(jù)屬性,因?yàn)樵趈ob的多次執(zhí)行中诱建,這些屬性的值不會(huì)保留蝴蜓。

如果需要有狀態(tài)的數(shù)據(jù)屬性時(shí),需要用JobDataMap,這個(gè)就相當(dāng)于Map茎匠,可以將數(shù)據(jù)傳入job中

JobDataMap

JobDataMap中可以包含不限量的(序列化的)數(shù)據(jù)對(duì)象格仲,在job實(shí)例執(zhí)行的時(shí)候,可以使用其中的數(shù)據(jù)诵冒;JobDataMap是Java Map接口的一個(gè)實(shí)現(xiàn)凯肋,額外增加了一些便于存取基本類型的數(shù)據(jù)的方法。

將job加入到scheduler之前汽馋,在構(gòu)建JobDetail時(shí)侮东,可以將數(shù)據(jù)放入JobDataMap

Trigger 觸發(fā)器

Job中包含了任務(wù)執(zhí)行的邏輯,Scheduler負(fù)責(zé)掃描需要執(zhí)行的Job任務(wù)豹芯,trigger則定義job何時(shí)被執(zhí)行悄雅。

下面對(duì)觸發(fā)器的子類分別進(jìn)行講述,我們主要看Trigger的4個(gè)可用的派生類铁蹈,分別是:

org.quartz.SimpleTrigger

org.quartz.CronTrigger (常用)

org.quartz.DateIntervalTrigger

org.quartz.NthIncludedDayTrigger


SimpleTrigger

SimpleTrigger是一種設(shè)置和使用簡(jiǎn)單的觸發(fā)器宽闲,它是在指定日期/時(shí)間且可能需要重復(fù)執(zhí)行n次的時(shí)機(jī)下使用的。這種觸發(fā)器不適合每天定時(shí)執(zhí)行任務(wù)這種場(chǎng)景

它適合的任務(wù)類似于:9:00 開始握牧,每隔1小時(shí)容诬,每隔幾分鐘,每隔幾秒鐘執(zhí)行一次我碟。


CronTrigger

類似于SimpleTrigger放案,指定從某一個(gè)時(shí)間開始,以一定的時(shí)間間隔執(zhí)行的任務(wù)矫俺。 但是不同的是SimpleTrigger指定的時(shí)間間隔為毫秒吱殉,沒辦法指定每隔一個(gè)月執(zhí)行一次(每月的時(shí)間間隔不是固定值),而CalendarIntervalTrigger支持的間隔單位有秒厘托,分鐘友雳,小時(shí),天铅匹,月押赊,年,星期包斑。

它適合的任務(wù)類似于:9:00 開始執(zhí)行流礁,并且以后每周 9:00 執(zhí)行一次

Cron表達(dá)式的格式:秒 分 時(shí) 日 月 周 年(可選)



DailyTimeIntervalTrigger

指定每天的某個(gè)時(shí)間段內(nèi),以一定的時(shí)間間隔執(zhí)行任務(wù)罗丰。并且它可以支持指定星期神帅。

它適合的任務(wù)類似于:指定每天9:00 至 18:00 ,每隔70秒執(zhí)行一次萌抵,并且只要周一至周五執(zhí)行找御。


NthIncludedDayTrigger

NthIncludedDayTrigger適用于在每一個(gè)間隔類型(月或年等)的第N天觸發(fā)元镀。直接上代碼:

它適合的任務(wù)類似于:每個(gè)月的第10天中午12點(diǎn)整觸發(fā)



3.代碼實(shí)現(xiàn)

job 繼承QuartzJobBean實(shí)現(xiàn)executeInternal方法




4.misfired 策略

對(duì)于判定為misfired job,其實(shí)有很多條件霎桅,目前了解到的有:

1.到執(zhí)行時(shí)間時(shí)栖疑,上一個(gè)任務(wù)還未完成;

2.過期時(shí)間已超過設(shè)置的misfireThreshold滔驶;

3.線程池中已沒有空閑線程

4.線程池中雖有空閑線程遇革,但有優(yōu)先級(jí)更高的任務(wù)

5.服務(wù)器停止運(yùn)行

產(chǎn)生misfire后,會(huì)根據(jù)設(shè)置的misfire策略進(jìn)行任務(wù)的處理揭糕。

對(duì)CronTrigger來說澳淑,有三種misfire策略:

withMisfireHandlingInstructionDoNothing

不觸發(fā)立即執(zhí)行,等待下次Cron觸發(fā)頻率到達(dá)時(shí)刻開始按照Cron頻率依次執(zhí)行

withMisfireHandlingInstructionFireAndProceed

以錯(cuò)過的第一個(gè)頻率時(shí)間立刻開始執(zhí)行插佛,重做錯(cuò)過的所有頻率周期后杠巡,當(dāng)下一次觸發(fā)頻率發(fā)生時(shí)間大于當(dāng)前時(shí)間后,再按照正常的Cron頻率依次執(zhí)行

withMisfireHandlingInstructionIgnoreMisfires

以當(dāng)前時(shí)間為觸發(fā)頻率立刻觸發(fā)一次執(zhí)行雇寇,并發(fā)執(zhí)行一次做完氢拥,然后按照Cron頻率依次執(zhí)行

5.job、trigger锨侯、scheduler監(jiān)聽器

由于quartz沒有實(shí)現(xiàn)對(duì)任務(wù)執(zhí)行情況做記錄嫩海,所以主要用監(jiān)聽器來實(shí)現(xiàn)記錄任務(wù)的執(zhí)行情況,記錄任務(wù)是否被錯(cuò)過,任務(wù)具體什么時(shí)候被執(zhí)行,執(zhí)行過程中是否拋出異常等益眉。

監(jiān)聽器的執(zhí)行順序:

SimpleSchedulerListener.java

public class SimpleSchedulerListener extends SchedulerListenerSupport {

? ? private static Logger logger = LoggerFactory.getLogger(SimpleSchedulerListener.class);

? ? @Override

? ? public void jobScheduled(Trigger trigger) {

? ? ? ? String jobName = trigger.getJobKey().getName();

? ? ? ? logger.info(jobName + " has been scheduled");

? ? }

? ? @Override

? ? public void jobUnscheduled(TriggerKey triggerKey) {

? ? ? ? logger.info(triggerKey + " is being unscheduled");

? ? }

? ? @Override

? ? public void triggerFinalized(Trigger trigger) {

? ? ? ? logger.info("Trigger is finished for " + trigger.getJobKey().getName());

? ? }

? ? @Override

? ? public void triggerPaused(TriggerKey triggerKey) {

? ? ? ? logger.info(triggerKey + " is being paused");

? ? }

? ? @Override

? ? public void triggersPaused(String triggerGroup) {

? ? ? ? logger.info("trigger group "+triggerGroup + " is being paused");

? ? }

? ? @Override

? ? public void triggerResumed(TriggerKey triggerKey) {

? ? ? ? logger.info(triggerKey + " is being resumed");

? ? }

? ? @Override

? ? public void triggersResumed(String triggerGroup) {

? ? ? ? logger.info("trigger group "+triggerGroup + " is being resumed");

? ? }

? ? @Override

? ? public void jobAdded(JobDetail jobDetail) {

? ? ? ? logger.info(jobDetail.getKey()+" is added");

? ? }

? ? @Override

? ? public void jobDeleted(JobKey jobKey) {

? ? ? ? logger.info(jobKey+" is deleted");

? ? }

? ? @Override

? ? public void jobPaused(JobKey jobKey) {

? ? ? ? logger.info(jobKey+" is paused");

? ? }

? ? @Override

? ? public void jobsPaused(String jobGroup) {

? ? ? ? logger.info("job group "+jobGroup+" is paused");

? ? }

? ? @Override

? ? public void jobResumed(JobKey jobKey) {

? ? ? ? logger.info(jobKey+" is resumed");

? ? }

? ? @Override

? ? public void jobsResumed(String jobGroup) {

? ? ? ? logger.info("job group "+jobGroup+" is resumed");

? ? }

? ? @Override

? ? public void schedulerError(String msg, SchedulerException cause) {

? ? ? ? logger.error(msg, cause.getUnderlyingException());

? ? }

? ? @Override

? ? public void schedulerInStandbyMode() {

? ? ? ? logger.info("scheduler is in standby mode");

? ? }

? ? @Override

? ? public void schedulerStarted() {

? ? ? ? logger.info("scheduler has been started");

? ? }

? ? @Override

? ? public void schedulerStarting() {

? ? ? ? logger.info("scheduler is being started");

? ? }

? ? @Override

? ? public void schedulerShutdown() {

? ? ? ? logger.info("scheduler has been shutdown");

? ? }

? ? @Override

? ? public void schedulerShuttingdown() {

? ? ? ? logger.info("scheduler is being shutdown");

? ? }

? ? @Override

? ? public void schedulingDataCleared() {

? ? ? ? logger.info("scheduler has cleared all data");

? ? }

}

MonitorTriggerListener.java

public class MonitorTriggerListener extends TriggerListenerSupport {

? ? private static Logger log = LoggerFactory.getLogger(MonitorTriggerListener.class);

? ? @Override

? ? public String getName() {

? ? ? ? // TODO Auto-generated method stub

? ? ? ? return getClass().getSimpleName();

? ? }

? ? @Override

? ? public void triggerFired(Trigger trigger, JobExecutionContext jobExecutionContext) {

? ? ? ? //Trigger 被觸發(fā)了旭从,此時(shí)job上的execute()方法將要被執(zhí)行

? ? }

? ? @Override

? ? public boolean vetoJobExecution(Trigger trigger, JobExecutionContext jobExecutionContext) {

? ? ? ? //trigger被觸發(fā)后爪瓜,job將要被執(zhí)行時(shí)Scheduler調(diào)用該方法,如返回true則job此次將不被執(zhí)行

? ? ? ? return false;

? ? }

? ? @Override

? ? public void triggerMisfired(Trigger trigger) {

? ? ? ? log.info("當(dāng)前任務(wù)為錯(cuò)過任務(wù)" );

? ? ? ? trigger.getJobDataMap().put("isMisfire",true);

? ? }

? ? @Override

? ? public void triggerComplete(Trigger trigger, JobExecutionContext jobExecutionContext, Trigger.CompletedExecutionInstruction completedExecutionInstruction) {

? ? ? ? log.info("Trigger被觸發(fā)并且完成了job的執(zhí)行,此方法被調(diào)用"+trigger.getJobKey().getName());

? ? }

}

MonitorJobListener.java

public class MonitorJobListener extends JobListenerSupport {

? ? private final static Logger log = LoggerFactory.getLogger(MonitorJobListener.class);

? ? @Override

? ? public String getName() {

? ? ? ? return getClass().getSimpleName();

? ? }

? ? /**

? ? * (1)

? ? * 任務(wù)執(zhí)行之前執(zhí)行

? ? * Called by the Scheduler when a JobDetail is about to be executed (an associated Trigger has occurred).

? ? */

? ? @Override

? ? public void jobToBeExecuted(JobExecutionContext jobExecutionContext) {

? ? ? ? log.info(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())+"->開始任務(wù):" + jobExecutionContext.getJobDetail().getJobClass().getName());

? ? ? ? log.info(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(System.currentTimeMillis()));

? ? ? ? Long now = System.currentTimeMillis();

? ? ? ? Long jobfire = jobExecutionContext.getScheduledFireTime().getTime();

? ? ? ? log.info(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(jobExecutionContext.getScheduledFireTime().getTime()));

? ? ? ? if (now - 10000L> jobfire){

? ? ? ? ? ? log.info("當(dāng)前任務(wù)為錯(cuò)過任務(wù)" );

? ? ? ? ? ? jobExecutionContext.getTrigger().getJobDataMap().put("isMisfire",true);

? ? ? ? }else {

? ? ? ? ? ? jobExecutionContext.getTrigger().getJobDataMap().put("isMisfire",false);

? ? ? ? }

? ? ? ? jobExecutionContext.getTrigger().getJobDataMap().put("now",now);

? ? }

? ? /**

? ? * (2)

? ? * 這個(gè)方法正常情況下不執(zhí)行,但是如果當(dāng)TriggerListener中的vetoJobExecution方法返回true時(shí),那么執(zhí)行這個(gè)方法.

? ? * 需要注意的是 如果方法(2)執(zhí)行 那么(1),(3)這個(gè)倆個(gè)方法不會(huì)執(zhí)行,因?yàn)槿蝿?wù)被終止了嘛.

? ? * Called by the Scheduler when a JobDetail was about to be executed (an associated Trigger has occurred),

? ? * but a TriggerListener vetoed it's execution.

? ? */

? ? @Override

? ? public void jobExecutionVetoed(JobExecutionContext jobExecutionContext) {

? ? ? ? log.info("========這個(gè)方法正常情況下不執(zhí)行,但是如果當(dāng)TriggerListener中的vetoJobExecution方法返回true時(shí),那么執(zhí)行這個(gè)方法.==========" + jobExecutionContext.getJobDetail().getJobClass().getName());

? ? }

? ? /**

? ? * (3)

? ? * 任務(wù)執(zhí)行完成后執(zhí)行,jobException如果它不為空則說明任務(wù)在執(zhí)行過程中出現(xiàn)了異常

? ? * Called by the Scheduler after a JobDetail has been executed, and be for the associated Trigger's triggered(xx) method has been called.

? ? */

? ? @Override

? ? public void jobWasExecuted(JobExecutionContext jobExecutionContext, JobExecutionException e) {

? ? ? ? //log.info("========任務(wù)執(zhí)行完成后執(zhí)行,jobException如果它不為空則說明任務(wù)在執(zhí)行過程中出現(xiàn)了異常==========" + getName());

? ? ? ? log.info(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())+"->結(jié)束任務(wù)任務(wù):" + getName());

? ? ? ? Long now = (Long) jobExecutionContext.getTrigger().getJobDataMap().get("now");

? ? ? ? System.currentTimeMillis();

? ? ? ? JobBo jobService = SpringContextHelp.getBean(JobBo.class);

? ? ? ? Long jobfire = jobExecutionContext.getScheduledFireTime().getTime();

? ? ? ? String ip_address =? Constants.getApplicationValue("applicationContext","servlet.ip.address");

? ? ? ? Calendar cr = Calendar.getInstance();

? ? ? ? cr.setTimeInMillis(now);

? ? ? ? Date d1= cr.getTime();

? ? ? ? cr.setTimeInMillis(jobfire);

? ? ? ? Date d2 = cr.getTime();

? ? ? ? Boolean isMisfire = (Boolean) jobExecutionContext.getTrigger().getJobDataMap().get("isMisfire") == null? false:(Boolean) jobExecutionContext.getTrigger().getJobDataMap().get("isMisfire");

? ? ? ? try {

? ? ? ? ? ? QrtzTaskDetailVo vo = new? QrtzTaskDetailVo(jobExecutionContext.getScheduler().getSchedulerName(),

? ? ? ? ? ? ? ? ? ? jobExecutionContext.getJobDetail().getKey().getName(),

? ? ? ? ? ? ? ? ? ? jobExecutionContext.getJobDetail().getKey().getGroup(),

? ? ? ? ? ? ? ? ? ? jobExecutionContext.getTrigger().getKey().getName(),

? ? ? ? ? ? ? ? ? ? jobExecutionContext.getTrigger().getKey().getGroup(),

? ? ? ? ? ? ? ? ? ? d2,

? ? ? ? ? ? ? ? ? ? d1,isMisfire,ip_address);

? ? ? ? ? ? if (e != null){

? ? ? ? ? ? ? ? log.info(e.getMessage());

? ? ? ? ? ? ? vo.setError_detail(e.getMessage());

? ? ? ? ? ? ? vo.setFinish_status(2);

? ? ? ? ? ? }else {

? ? ? ? ? ? ? ? vo.setFinish_status(1);

? ? ? ? ? ? }

? ? ? ? ? ? jobService.saveQrtzTaskDetail(vo);

? ? ? ? } catch (SchedulerException s) {

? ? ? ? ? ? s.printStackTrace();

? ? ? ? }

? ? }

}

6.Quartz 集群


注意事項(xiàng)

時(shí)間同步問題

  Quartz實(shí)際并不關(guān)心你是在相同還是不同的機(jī)器上運(yùn)行節(jié)點(diǎn)。當(dāng)集群放置在不同的機(jī)器上時(shí)血柳,稱之為水平集群。節(jié)點(diǎn)跑在同一臺(tái)機(jī)器上時(shí)生兆,稱之為垂直集群难捌。對(duì)于垂直集群,存在著單點(diǎn)故障的問題鸦难。這對(duì)高可用性的應(yīng)用來說是無法接受的根吁,因?yàn)橐坏C(jī)器崩潰了,所有的節(jié)點(diǎn)也就被終止了合蔽。對(duì)于水平集群击敌,存在著時(shí)間同步問題。

  節(jié)點(diǎn)用時(shí)間戳來通知其他實(shí)例它自己的最后檢入時(shí)間辈末。假如節(jié)點(diǎn)的時(shí)鐘被設(shè)置為將來的時(shí)間愚争,那么運(yùn)行中的Scheduler將再也意識(shí)不到那個(gè)結(jié)點(diǎn)已經(jīng)宕掉了。另一方面挤聘,如果某個(gè)節(jié)點(diǎn)的時(shí)鐘被設(shè)置為過去的時(shí)間轰枝,也許另一節(jié)點(diǎn)就會(huì)認(rèn)定那個(gè)節(jié)點(diǎn)已宕掉并試圖接過它的Job重運(yùn)行。最簡(jiǎn)單的同步計(jì)算機(jī)時(shí)鐘的方式是使用某一個(gè)Internet時(shí)間服務(wù)器(Internet Time Server ITS)组去。

節(jié)點(diǎn)爭(zhēng)搶Job問題

  因?yàn)镼uartz使用了一個(gè)隨機(jī)的負(fù)載均衡算法鞍陨, Job以隨機(jī)的方式由不同的實(shí)例執(zhí)行。Quartz官網(wǎng)上提到當(dāng)前从隆,還不存在一個(gè)方法來指派(釘住) 一個(gè) Job 到集群中特定的節(jié)點(diǎn)诚撵。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市键闺,隨后出現(xiàn)的幾起案子寿烟,更是在濱河造成了極大的恐慌,老刑警劉巖辛燥,帶你破解...
    沈念sama閱讀 218,284評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件筛武,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡挎塌,警方通過查閱死者的電腦和手機(jī)徘六,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來榴都,“玉大人待锈,你說我怎么就攤上這事∽旄撸” “怎么了竿音?”我有些...
    開封第一講書人閱讀 164,614評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)拴驮。 經(jīng)常有香客問我谍失,道長(zhǎng),這世上最難降的妖魔是什么莹汤? 我笑而不...
    開封第一講書人閱讀 58,671評(píng)論 1 293
  • 正文 為了忘掉前任快鱼,我火速辦了婚禮,結(jié)果婚禮上纲岭,老公的妹妹穿的比我還像新娘抹竹。我一直安慰自己,他們只是感情好止潮,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,699評(píng)論 6 392
  • 文/花漫 我一把揭開白布窃判。 她就那樣靜靜地躺著,像睡著了一般喇闸。 火紅的嫁衣襯著肌膚如雪袄琳。 梳的紋絲不亂的頭發(fā)上询件,一...
    開封第一講書人閱讀 51,562評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音唆樊,去河邊找鬼宛琅。 笑死,一個(gè)胖子當(dāng)著我的面吹牛逗旁,可吹牛的內(nèi)容都是我干的嘿辟。 我是一名探鬼主播,決...
    沈念sama閱讀 40,309評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼片效,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼红伦!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起淀衣,我...
    開封第一講書人閱讀 39,223評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤昙读,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后膨桥,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體箕戳,經(jīng)...
    沈念sama閱讀 45,668評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,859評(píng)論 3 336
  • 正文 我和宋清朗相戀三年国撵,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了陵吸。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,981評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡介牙,死狀恐怖壮虫,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情环础,我是刑警寧澤囚似,帶...
    沈念sama閱讀 35,705評(píng)論 5 347
  • 正文 年R本政府宣布,位于F島的核電站线得,受9級(jí)特大地震影響饶唤,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜贯钩,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,310評(píng)論 3 330
  • 文/蒙蒙 一募狂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧角雷,春花似錦祸穷、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至吗坚,卻和暖如春祈远,著一層夾襖步出監(jiān)牢的瞬間呆万,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工车份, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留谋减,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,146評(píng)論 3 370
  • 正文 我出身青樓躬充,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親讨便。 傳聞我的和親對(duì)象是個(gè)殘疾皇子充甚,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,933評(píng)論 2 355