Quartz快速入门

1.什么是Quartz

Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,作用是用来执行任务调度。然而任务调度本身涉及多线程并发、运行舒缓规则制定及解析、运行现场保持与恢复、以及线程池维护等很多工作。如果我们自己去开发,难度很大,而quartz工具为我们提供了便利。可以让我们创建多个Job任务。

2.开发环境搭建

引入maven的jar包:

<dependency>
	<groupId>org.quartz-scheduler</groupId>
	<artifactId>quartz</artifactId>
	<version>1.8.6</version>
</dependency>

3.quartz的基础结构

quartz提供了强大任务调度机制。允许我们灵活的定义触发器的调度时间表。然后也可以灵活得将触发器和任务进行关联映射。最主要的就是调度器、任务和触发器三大块。

Job接口:定义的方法execute,通过该方法完成需要执行的任务。JobExecutionContext类提供了调度上下文的各种信息。Job运行时的信息都保存在JobDataMap实例中。子接口StatefulJob代表有状态的任务。目得是让Quartz知道任务的类型。状态好比一把锁,无状态的Job拥有自己的JobDataMap的复制,对JobDataMap的更改不会影响下次的执行。然而有状态的Job会共享同一个JobDataMap实例,所以因此总结出无状态的Job是可以并发的,而有状态的Job是不可以并发的。

JobDetail类:Quartz在每次执行Job时,都重新创建一个Job实例,所以它不是直接接收一个Job实例,而是接收了一个Job实现类,方便运行时通过newInstance的反射调用机制实例化Job。所以通过这个JobDetail类就可以描述Job的实现类及相关的静态信息。它的构造器需要指定Job的实现类以及任务在Scheduler调度者中的组名和Job名称。

Trigger类:描述触发Job执行的时间触发规则。子类SimpleTrigger:当仅需要触发一次或者以固定间隔周期性执行。

子类CronTrigger:通过Cron表达式定义出复杂的调度方案。

Calendar类:它是一些日历特定的时间点集合。并且一个Trigger触发器可以和多个Calendar相关联。

Scheduler类:代表一个Quartz独立运行容器,可以让Trigger和JobDetail注册到Scheduler中。它们分别在Scheduler中对应了各自的组和名称。并且这些组和名称必须唯一。Scheduler将Trigger触发器绑定到JobDetail上,这样Trigger就会触发Job的执行。相互的关系是一个Job可以被多个Trigger触发器触发,但是一个触发器只能触发一个Job。至于Scheduler实例是通过SchedulerFactory工厂以工厂模式创建的。

ThreadPool:Scheduler类使用一个线程池作为任务运行的基础设施。任务通过共享线程池中的线程来提高运行效率。

4.SimpleTrigger触发器解析

实例:

任务类:

public class MyTask implements Job{

	@Override
	public void execute(JobExecutionContext context) throws JobExecutionException {
		// TODO Auto-generated method stub
		System.out.println(context.getTrigger().getName()+" trigger time :"+new Date());
	}

}
调度执行类:

public class SimpleTriggerTest {

	public static void main(String[] args) {
		try {
			JobDetail jobDetail =new JobDetail("job1","jgroup1",MyTask.class);
			SimpleTrigger simpleTrigger =new SimpleTrigger("trigger1","tgroup1");
			simpleTrigger.setStartTime(new Date());
			//每两秒执行一次
			simpleTrigger.setRepeatInterval(2000);
			//重复执行10次,那么就是一共执行11次了
			simpleTrigger.setRepeatCount(10);
			
			SchedulerFactory factory =new StdSchedulerFactory();
			Scheduler scheduler =factory.getScheduler();
			scheduler.scheduleJob(jobDetail, simpleTrigger);
			scheduler.start();
		} catch (SchedulerException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}
结果:


5.CronTrigger触发器解析

Cron表达式中的特殊字符:

(1) *  :可用在所有字段中,表示对应时间域的每一个时刻。

(2) ? :该字符只在日期和星期字段中使用。通常都当作占位符。

(3) -  :表示一个范围,如时间1-3代表值为1,2,3.

(4) ,  :表示一个列表值。

(5) /  :x/y表示一个等步长的序列,x为起始值,y为增量步长值。

(6) L :该字符只在日期和星期字段中使用。该字段用在月份中代表月份中的最后一天。该字段用在星期中代表星期六。6L代表该月的最后一个星期五,因为星期六代表最后一天,星期日代表一个月中的第一天。

(7) W:该字符只能出现在日期字段里,是对前导日期的修饰。表示离该日期最近的工作日。

(8)LW:在日期字段中可以组合使用LW,意思是当月的最后一个工作日。

(9) #  :该字符只能在星期字段中使用,表示当月的某个工作日。

(10) C :该字符只在日期和星期字段中使用。

实例:

任务类:

public class MyTask implements Job{

	@Override
	public void execute(JobExecutionContext context) throws JobExecutionException {
		// TODO Auto-generated method stub
		System.out.println(context.getTrigger().getName()+" trigger time :"+new Date());
	}

}
任务调度类:

public class CronTriggerTest {

	public static void main(String[] args) {
		try {
			JobDetail jobDetail =new JobDetail("job2","jgroup2",MyTask.class);
			CronTrigger cronTrigger =new CronTrigger("trigger2","tgroup2");
			CronExpression exp =new CronExpression("0/2 * * * * ?");
			cronTrigger.setCronExpression(exp);
			SchedulerFactory factroy =new StdSchedulerFactory();
			Scheduler scheduler =factroy.getScheduler();
			scheduler.scheduleJob(jobDetail, cronTrigger);
			scheduler.start();
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}
结果:


说明:由于调度器寄生于主线程,而主线程如果退出的话,那么调度器中的任务就会被销毁。

6.任务调度信息的存储之默认内存数据库

默认情况下,Quartz将任务调度的运行信息保存在内存中。都知道保存在内存中数据的访问速度加快,但是却缺乏了持久性。为了保持数据的持久性,就要使用数据库进行存储相关的数据。

将quartz.properties文件放到类路径下,这样就不会加载默认的quartz.properties文件,而是加载我们定义的quartz.properties文件。

内存数据库:

quartz.propeties文件

# ===========================================================================
# Configure Main Scheduler Properties 调度器属性
# ===========================================================================
#集群的配置,这里不使用集群
org.quartz.scheduler.instanceName=DefaultQuartzScheduler
# ===========================================================================  
# Configure ThreadPool 线程池属性  
# ===========================================================================
#线程池的实现类(一般使用SimpleThreadPool即可满足几乎所有用户的需求)
org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
#指定线程数,至少为1(无默认值)(一般设置为1-100直接的整数合适)
org.quartz.threadPool.threadCount=10
#设置线程的优先级(最大为java.lang.Thread.MAX_PRIORITY 10,最小为Thread.MIN_PRIORITY 1,默认为5)
org.quartz.threadPool.threadPriority=5
#设置SimpleThreadPool的一些属性
#设置是否为守护线程
#org.quartz.threadpool.makethreadsdaemons = false
#org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true
#org.quartz.threadpool.threadsinheritgroupofinitializingthread=false
#线程前缀默认值是:[Scheduler Name]_Worker
#org.quartz.threadpool.threadnameprefix=swhJobThead;
# 配置全局监听(TriggerListener,JobListener) 则应用程序可以接收和执行 预定的事件通知
# ===========================================================================
# Configuring a Global TriggerListener 配置全局的Trigger监听器
# MyTriggerListenerClass 类必须有一个无参数的构造函数,和 属性的set方法,目前2.2.x只支持原始数据类型的值(包括字符串)
# ===========================================================================
#org.quartz.triggerListener.NAME.class = com.swh.MyTriggerListenerClass
#org.quartz.triggerListener.NAME.propName = propValue
#org.quartz.triggerListener.NAME.prop2Name = prop2Value
# ===========================================================================  
# Configure JobStore 存储调度信息(工作,触发器和日历等)
# ===========================================================================
# 信息保存时间 默认值60秒
org.quartz.jobStore.misfireThreshold=60000
#保存job和Trigger的状态信息到内存中的类
org.quartz.jobStore.class=org.quartz.simpl.RAMJobStore
# ===========================================================================  
# Configure SchedulerPlugins 插件属性 配置
# ===========================================================================
# 自定义插件  
#org.quartz.plugin.NAME.class = com.swh.MyPluginClass
#org.quartz.plugin.NAME.propName = propValue
#org.quartz.plugin.NAME.prop2Name = prop2Value
#配置trigger执行历史日志(可以看到类的文档和参数列表)
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}  
#配置job调度插件  quartz_jobs(jobs and triggers内容)的XML文档  
#加载 Job 和 Trigger 信息的类   (1.8之前用:org.quartz.plugins.xml.JobInitializationPlugin)
org.quartz.plugin.jobInitializer.class = org.quartz.plugins.xml.XMLSchedulingDataProcessorPlugin
#指定存放调度器(Job 和 Trigger)信息的xml文件,默认是classpath下quartz_jobs.xml
#org.quartz.plugin.jobInitializer.fileNames = my_quartz_job2.xml  
#org.quartz.plugin.jobInitializer.overWriteExistingJobs = false  
#org.quartz.plugin.jobInitializer.failOnFileNotFound = true  
#自动扫描任务单并发现改动的时间间隔,单位为秒
#org.quartz.plugin.jobInitializer.scanInterval = 10
#覆盖任务调度器中同名的jobDetail,避免只修改了CronExpression所造成的不能重新生效情况
#org.quartz.plugin.jobInitializer.wrapInUserTransaction = false
# ===========================================================================  
# Sample configuration of ShutdownHookPlugin  ShutdownHookPlugin插件的配置样例
# ===========================================================================
#org.quartz.plugin.shutdownhook.class = \org.quartz.plugins.management.ShutdownHookPlugin
#org.quartz.plugin.shutdownhook.cleanShutdown = true
#
# Configure RMI Settings 远程服务调用配置
#
#如果你想quartz-scheduler出口本身通过RMI作为服务器,然后设置“出口”标志true(默认值为false)。
#org.quartz.scheduler.rmi.export = false
#主机上rmi注册表(默认值localhost)
#org.quartz.scheduler.rmi.registryhost = localhost
#注册监听端口号(默认值1099)
#org.quartz.scheduler.rmi.registryport = 1099
#创建rmi注册,false/never:如果你已经有一个在运行或不想进行创建注册
# true/as_needed:第一次尝试使用现有的注册,然后再回来进行创建
# always:先进行创建一个注册,然后再使用回来使用注册
#org.quartz.scheduler.rmi.createregistry = never
#Quartz Scheduler服务端端口,默认是随机分配RMI注册表
#org.quartz.scheduler.rmi.serverport = 1098
#true:链接远程服务调度(客户端),这个也要指定registryhost和registryport,默认为false
# 如果export和proxy同时指定为true,则export的设置将被忽略
#org.quartz.scheduler.rmi.proxy = false

7.任务调度信息的存储之Mysql数据库

环境搭建:

由于我们使用了dbcp连接池和mysql数据库,所以要引入必需的连接池包和mysql驱动包:

<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
	<version>5.1.35</version>
</dependency>
<dependency>
	<groupId>commons-dbcp</groupId>
	<artifactId>commons-dbcp</artifactId>
	<version>1.3</version>
</dependency>
<dependency>
	<groupId>commons-lang</groupId>
	<artifactId>commons-lang</artifactId>
	<version>2.6</version>
</dependency>

生成mysql数据库表的脚本,我使用的是quartz-1.8的版本对应的脚本

DROP TABLE IF EXISTS QRTZ_JOB_LISTENERS;  
DROP TABLE IF EXISTS QRTZ_TRIGGER_LISTENERS;  
DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS;  
DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS;  
DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE;  
DROP TABLE IF EXISTS QRTZ_LOCKS;  
DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS;  
DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS;  
DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS;  
DROP TABLE IF EXISTS QRTZ_TRIGGERS;  
DROP TABLE IF EXISTS QRTZ_JOB_DETAILS;  
DROP TABLE IF EXISTS QRTZ_CALENDARS;  
CREATE TABLE QRTZ_JOB_DETAILS(  
JOB_NAME VARCHAR(200) NOT NULL,  
JOB_GROUP VARCHAR(200) NOT NULL,  
DESCRIPTION VARCHAR(250) NULL,  
JOB_CLASS_NAME VARCHAR(250) NOT NULL,  
IS_DURABLE VARCHAR(1) NOT NULL,  
IS_VOLATILE VARCHAR(1) NOT NULL,  
IS_STATEFUL VARCHAR(1) NOT NULL,  
REQUESTS_RECOVERY VARCHAR(1) NOT NULL,  
JOB_DATA BLOB NULL,  
PRIMARY KEY (JOB_NAME,JOB_GROUP))  
ENGINE=InnoDB;  
  
CREATE TABLE QRTZ_JOB_LISTENERS (  
JOB_NAME VARCHAR(200) NOT NULL,  
JOB_GROUP VARCHAR(200) NOT NULL,  
JOB_LISTENER VARCHAR(200) NOT NULL,  
PRIMARY KEY (JOB_NAME,JOB_GROUP,JOB_LISTENER),  
INDEX (JOB_NAME, JOB_GROUP),  
FOREIGN KEY (JOB_NAME,JOB_GROUP)  
REFERENCES QRTZ_JOB_DETAILS(JOB_NAME,JOB_GROUP))  
ENGINE=InnoDB;  
  
CREATE TABLE QRTZ_TRIGGERS (  
TRIGGER_NAME VARCHAR(200) NOT NULL,  
TRIGGER_GROUP VARCHAR(200) NOT NULL,  
JOB_NAME VARCHAR(200) NOT NULL,  
JOB_GROUP VARCHAR(200) NOT NULL,  
IS_VOLATILE VARCHAR(1) NOT NULL,  
DESCRIPTION VARCHAR(250) NULL,  
NEXT_FIRE_TIME BIGINT(13) NULL,  
PREV_FIRE_TIME BIGINT(13) NULL,  
PRIORITY INTEGER NULL,  
TRIGGER_STATE VARCHAR(16) NOT NULL,  
TRIGGER_TYPE VARCHAR(8) NOT NULL,  
START_TIME BIGINT(13) NOT NULL,  
END_TIME BIGINT(13) NULL,  
CALENDAR_NAME VARCHAR(200) NULL,  
MISFIRE_INSTR SMALLINT(2) NULL,  
JOB_DATA BLOB NULL,  
PRIMARY KEY (TRIGGER_NAME,TRIGGER_GROUP),  
INDEX (JOB_NAME, JOB_GROUP),  
FOREIGN KEY (JOB_NAME,JOB_GROUP)  
REFERENCES QRTZ_JOB_DETAILS(JOB_NAME,JOB_GROUP))  
ENGINE=InnoDB;  
  
CREATE TABLE QRTZ_SIMPLE_TRIGGERS (  
TRIGGER_NAME VARCHAR(200) NOT NULL,  
TRIGGER_GROUP VARCHAR(200) NOT NULL,  
REPEAT_COUNT BIGINT(7) NOT NULL,  
REPEAT_INTERVAL BIGINT(12) NOT NULL,  
TIMES_TRIGGERED BIGINT(10) NOT NULL,  
PRIMARY KEY (TRIGGER_NAME,TRIGGER_GROUP),  
INDEX (TRIGGER_NAME, TRIGGER_GROUP),  
FOREIGN KEY (TRIGGER_NAME,TRIGGER_GROUP)  
REFERENCES QRTZ_TRIGGERS(TRIGGER_NAME,TRIGGER_GROUP))  
ENGINE=InnoDB;  
  
CREATE TABLE QRTZ_CRON_TRIGGERS (  
TRIGGER_NAME VARCHAR(200) NOT NULL,  
TRIGGER_GROUP VARCHAR(200) NOT NULL,  
CRON_EXPRESSION VARCHAR(120) NOT NULL,  
TIME_ZONE_ID VARCHAR(80),  
PRIMARY KEY (TRIGGER_NAME,TRIGGER_GROUP),  
INDEX (TRIGGER_NAME, TRIGGER_GROUP),  
FOREIGN KEY (TRIGGER_NAME,TRIGGER_GROUP)  
REFERENCES QRTZ_TRIGGERS(TRIGGER_NAME,TRIGGER_GROUP))  
ENGINE=InnoDB;  
  
CREATE TABLE QRTZ_BLOB_TRIGGERS (  
TRIGGER_NAME VARCHAR(200) NOT NULL,  
TRIGGER_GROUP VARCHAR(200) NOT NULL,  
BLOB_DATA BLOB NULL,  
PRIMARY KEY (TRIGGER_NAME,TRIGGER_GROUP),  
INDEX (TRIGGER_NAME, TRIGGER_GROUP),  
FOREIGN KEY (TRIGGER_NAME,TRIGGER_GROUP)  
REFERENCES QRTZ_TRIGGERS(TRIGGER_NAME,TRIGGER_GROUP))  
ENGINE=InnoDB;  
  
CREATE TABLE QRTZ_TRIGGER_LISTENERS (  
TRIGGER_NAME VARCHAR(200) NOT NULL,  
TRIGGER_GROUP VARCHAR(200) NOT NULL,  
TRIGGER_LISTENER VARCHAR(200) NOT NULL,  
PRIMARY KEY (TRIGGER_NAME,TRIGGER_GROUP,TRIGGER_LISTENER),  
INDEX (TRIGGER_NAME, TRIGGER_GROUP),  
FOREIGN KEY (TRIGGER_NAME,TRIGGER_GROUP)  
REFERENCES QRTZ_TRIGGERS(TRIGGER_NAME,TRIGGER_GROUP))  
ENGINE=InnoDB;  
  
CREATE TABLE QRTZ_CALENDARS (  
CALENDAR_NAME VARCHAR(200) NOT NULL,  
CALENDAR BLOB NOT NULL,  
PRIMARY KEY (CALENDAR_NAME))  
ENGINE=InnoDB;  
  
CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS (  
TRIGGER_GROUP VARCHAR(200) NOT NULL,  
PRIMARY KEY (TRIGGER_GROUP))  
ENGINE=InnoDB;  
  
CREATE TABLE QRTZ_FIRED_TRIGGERS (  
ENTRY_ID VARCHAR(95) NOT NULL,  
TRIGGER_NAME VARCHAR(200) NOT NULL,  
TRIGGER_GROUP VARCHAR(200) NOT NULL,  
IS_VOLATILE VARCHAR(1) NOT NULL,  
INSTANCE_NAME VARCHAR(200) NOT NULL,  
FIRED_TIME BIGINT(13) NOT NULL,  
PRIORITY INTEGER NOT NULL,  
STATE VARCHAR(16) NOT NULL,  
JOB_NAME VARCHAR(200) NULL,  
JOB_GROUP VARCHAR(200) NULL,  
IS_STATEFUL VARCHAR(1) NULL,  
REQUESTS_RECOVERY VARCHAR(1) NULL,  
PRIMARY KEY (ENTRY_ID))  
ENGINE=InnoDB;  
  
CREATE TABLE QRTZ_SCHEDULER_STATE (  
INSTANCE_NAME VARCHAR(200) NOT NULL,  
LAST_CHECKIN_TIME BIGINT(13) NOT NULL,  
CHECKIN_INTERVAL BIGINT(13) NOT NULL,  
PRIMARY KEY (INSTANCE_NAME))  
ENGINE=InnoDB;  
  
CREATE TABLE QRTZ_LOCKS (  
LOCK_NAME VARCHAR(40) NOT NULL,  
PRIMARY KEY (LOCK_NAME))  
ENGINE=InnoDB;  
  
INSERT INTO QRTZ_LOCKS values('TRIGGER_ACCESS');  
INSERT INTO QRTZ_LOCKS values('JOB_ACCESS');  
INSERT INTO QRTZ_LOCKS values('CALENDAR_ACCESS');  
INSERT INTO QRTZ_LOCKS values('STATE_ACCESS');  
INSERT INTO QRTZ_LOCKS values('MISFIRE_ACCESS');  
commit;   
结果:



quartz.properties配置文件:

org.quartz.scheduler.instanceName:MyFirstScheduler
#线程池的实现类(一般使用SimpleThreadPool即可满足几乎所有用户的需求)
org.quartz.threadPool.class:org.quartz.simpl.SimpleThreadPool
#指定线程数,至少为1(无默认值)(一般设置为1-100直接的整数合适)
org.quartz.threadPool.threadCount:10
#设置线程的优先级(最大为java.lang.Thread.MAX_PRIORITY 10,最小为Thread.MIN_PRIORITY 1,默认为5)
org.quartz.threadPool.threadPriority:5

org.quartz.jobStore.class:org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass:org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.tablePrefix:QRTZ_
org.quartz.jobStore.dataSource:qzDS

org.quartz.dataSource.qzDS.driver:com.mysql.jdbc.Driver
org.quartz.dataSource.qzDS.URL:jdbc:mysql://localhost:3306/quartz
org.quartz.dataSource.qzDS.user:root
org.quartz.dataSource.qzDS.password:
org.quartz.dataSource.qzDS.maxConnections:10

#配置trigger执行历史日志(可以看到类的文档和参数列表)
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}  

让SimpleTrigger触发器的程序SimpleTriggerTest跑起来,如果我们中途突然停止了程序的运行,那么数据库就会存储当前组和触发器的程序状态。


SimpleTriggerTest程序:

public class SimpleTriggerTest {

	public static void main(String[] args) {
		try {
			JobDetail jobDetail =new JobDetail("job1","jgroup1",MyTask.class);
			SimpleTrigger simpleTrigger =new SimpleTrigger("trigger1","tgroup1");
			simpleTrigger.setStartTime(new Date());
			//每两秒执行一次
			simpleTrigger.setRepeatInterval(2000);
			//重复执行10次,那么就是一共执行101次了
			simpleTrigger.setRepeatCount(100);
			
			SchedulerFactory factory =new StdSchedulerFactory();
			Scheduler scheduler =factory.getScheduler();
			scheduler.scheduleJob(jobDetail, simpleTrigger);
			scheduler.start();
		} catch (SchedulerException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}
结果:


运行到出现第6条记录时,故意将程序停止了。那么对应的数据库记录的现场持久化的结果:


8.问题

如果我们停止程序后,想再次运行该程序就会发现,程序运行报异常:


显示的异常信息说:该组和任务不能进行存储了,因为它们本身已经存在了唯一的标识。

也证明了之前的结论:一个触发器只能触发一个Job任务。



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值