Uncode-Schedule框架源码分析

博客原文

之前发布过一篇文章《分布式定时任务框架---Uncode Schedule》,在这篇文章中已经介绍uncode-schedule分布式定时任务框架的有关功能,以及实现机制。本文主要结合源码来介绍一下分布式定时任务uncode-schedule框架的实现原理。

1. Uncode-Schedule功能概述

Uncode-Schedule是基于zookeeper的分布式任务调度组件,非常小巧,使用简单。
1.1. 它能够确保所有任务在集群中不重复,不遗漏的执行。
1.2. 单节点故障时,任务能够自动转移到其他节点继续执行。
1.3. 支持动态添加和删除任务。
1.4. 支持添加机器ip黑名单。
1.5. 支持手动执行任务。

2. 使用方法

2.1. 配置maven依赖,pom.xml配置如下:

<dependency>
        <groupId>cn.uncode</groupId>
        <artifactId>uncode-schedule</artifactId>
        <version>0.8.0</version>
</dependency>

2.2. schedule.properties配置
这里主要配置固定值,而不是系统自动生成的,目前可配置机器编码,配置如下:

#uncode.schedule.server.code=0000000001

2.3. 定时任务的spring配置,applicationContext.xml配置如下:

  • ScheduleManager配置
<bean id="zkScheduleManager" class="cn.uncode.schedule.ZKScheduleManager" init-method="init">
        <property name="zkConfig">
            <map>
                <entry key="zkConnectString" value="192.168.7.149:2181" />
                <entry key="rootPath" value="/uncode/schedule" />
                <entry key="zkSessionTimeout" value="60000" />
                <entry key="userName" value="ScheduleAdmin" />
                <entry key="password" value="password" />
                <entry key="autoRegisterTask" value="true" />
                <entry key="isCheckParentPath" value="true" />
                <entry key="ipBlacklist" value="192.168.7.231" />
            </map>
        </property>
</bean>
  • spring task配置
<task:scheduled-tasks scheduler="zkScheduleManager">
        <task:scheduled ref="simpleTask" method="print" cron="0/30 * * * * ?" />
</task:scheduled-tasks>
  • 待执行任务类
@Component
public class SimpleTask {
        private static int i = 0;
        private Logger log = LoggerFactory.getLogger(SimpleTask.class);
        public void print() {
             log.info("===========print start!=========");
             log.info("print:"+i);i++;
             log.info("===========print end !=========");
        }
}

从上面的配置信息中可以看出,使用框架Uncode-Schedule可以很简单的实现定时任务的分布式。从代码上看,和原来的spring task或quartz任务写法完全一样。
关键点是,每个定时任务配置的调度器是uncode-schedule框架自定义的调度器 cn.uncode.schedule.ZKScheduleManager。上面是基于xml的配置,同样的,基于注解的配置是<task:annotation-driven scheduler="zkScheduleManager" />,详细的配置方式可以参考uncode-schedule-learn,或者uncode-schedule

3. 源码分析

从上面的Uncode-Schedule框架的使用和功能来看,源码分析应该有5个入口:

  • cn.uncode.schedule.ZKScheduleManagerinit 方法;
  • cn.uncode.schedule.ZKScheduleManager 的定时任务初始化;
  • cn.uncode.schedule.ZKScheduleManager 的心跳检测 hearBeatTimer
  • 控制管理类 cn.uncode.schedule.ConsoleManager
  • 对外暴露的连个servlet接口 ManagerServletManualServlet

下面按照谁许依次进行源码分析:

3.1. 类 cn.uncode.schedule.ZKScheduleManagerinit 方法

该方法的主要作用是,将配置文件中的数据加载进内存,连接zookeeper,校验zookeeper的连接状态,注册任务服务器,计算统一时间,启动心跳检测任务。

init方法的代码如下:

public void init() throws Exception {
    Properties properties = new Properties();
    for (Map.Entry<String, String> e : this.zkConfig.entrySet()) {
        properties.put(e.getKey(), e.getValue());
    }
    this.init(properties);
}

将xml配置文件中的配置信息加载进properties变量,然后去进一步初始化。

public void init(Properties p) throws Exception {
    if (this.initialThread != null) {
        this.initialThread.stopThread();
    }
    this.initLock.lock();
    try {
        this.scheduleDataManager = null;
        if (this.zkManager != null) {
            this.zkManager.close();
        }
        //连接zookeeper
        this.zkManager = new ZKManager(p);
        this.errorMessage = "Zookeeper connecting ......"
                + this.zkManager.getConnectStr();
        initialThread = new InitialThread(this);
        initialThread.setName("ScheduleManager-initialThread");
        initialThread.start();
    } finally {
        this.initLock.unlock();
    }
}

在代码中通过this.zkManager = new ZKManager(p); 和zookeeper建立连接,然后会启动一个初始化线程,这个线程的作业主要是等待连接zookeeper成功之后,进一步初始化之后的注册服务器等,初始化线程的代码如下:

class InitialThread extends Thread {
    private transient Logger log = LoggerFactory.getLogger(InitialThread.class);
    ZKScheduleManager sm;
    public InitialThread(ZKScheduleManager sm) {
        this.sm = sm;
    }
    boolean isStop = false;
    public void stopThread() {
        this.isStop = true;
    }
    @Override
    public void run() {
        sm.initLock.lock();
        try {
            int count = 0;
            while (!sm.zkManager.checkZookeeperState()) {
                count = count + 1;
                if (count % 50 == 0) {
                    sm.errorMessage = "Zookeeper connecting ......"
                            + sm.zkManager.getConnectStr() + " spendTime:"
                            + count * 20 + "(ms)";
                    log.error(sm.errorMessage);
                }
                Thread.sleep(20);
                if (this.isStop) {
                    return;
                }
            }
            sm.initialData();
        } catch (Throwable e) {
            log.error(e.getMessage(), e);
        } finally {
            sm.initLock.unlock();
        }
    }
}

看线程的 run 方法,while 循环中检测是否连接成功zookeeper,连接成功之后,调用 sm.initialData(); 真正的初始化 ZKScheduleManager,初始化的代码如下:

public void initialData() throws Exception {
    //首先进行了框架的版本兼容性校验
    this.zkManager.initial();
    this.scheduleDataManager = new ScheduleDataManager4ZK(this.zkManager);
    if (this.start) {
        // 注册调度管理器
        this.scheduleDataManager.registerScheduleServer(this.currenScheduleServer);
        if (hearBeatTimer == null) {
            hearBeatTimer = new Timer("ScheduleManager-"
                    + this.currenScheduleServer.getUuid() + "-HearBeat");
        }
        hearBeatTimer.schedule(new HeartBeatTimerTask(this), 2000, this.timerInterval);
    }
}

代码中首先进行了版本兼容性校验,然后将自身作为一个调度服务器注册到管理器中,最后启动检测调度器本身的心跳任务。心跳检测的任务在下一个小节重点分析,这里重点看一下注册调度管理器,代码如下:

@Override
public void registerScheduleServer(ScheduleServer server) throws Exception {
    if(server.isRegister()){
        throw new Exception(server.getUuid() + " 被重复注册");
    }
    //clearExpireScheduleServer();
    String realPath;
    //此处必须增加UUID作为唯一性保障
    StringBuffer id = new StringBuffer();
    id.append(server.getIp()).append("$")
        .append(UUID.randomUUID().toString().replaceAll("-", "").toUpperCase());
    
    String serverCode = ScheduleUtil.getServerCode();
    if(serverCode != null){ //如果配置文件schedule.properties中配置server code
        String zkServerPath = pathServer + "/" + id.toString() + "$" + serverCode;
        realPath = this.getZooKeeper().create(zkServerPath, null, this.zkManager.getAcl(),CreateMode.PERSISTENT);
    }else{
        String zkServerPath = pathServer + "/" + id.toString() +"$";
        realPath = this.getZooKeeper().create(zkServerPath, null, this.zkManager.getAcl(),CreateMode.PERSISTENT_SEQUENTIAL);
    }
    
    server.setUuid(realPath.substring(realPath.lastIndexOf("/") + 1));
    
    Timestamp heartBeatTime = new Timestamp(getSystemTime());
    server.setHeartBeatTime(heartBeatTime);
    
    String valueString = this.gson.toJson(server);
    this.getZooKeeper().setData(realPath,valueString.getBytes(),-1);
    server.setRegister(true);
}

将调度服务器信息注册到zookeeper中,服务器信息在zk上的节点是由 ip$UUID$serverCode 组成,存储在目录{rootPath}/server 下,例如, 192.168.7.231$B6A47BA82F4C44389D8D066F571D51D8$1000000001。其中serverCode有两个来源,一是配置文件schedule.properties中的 uncode.schedule.server.code,另一个是由zk的持久化顺序节点生产,这个数值关系到分布式系统中leader节点的选取,因此做成可配置的,从而控制leader节点的选取,选leader节点的算法将会在心跳检测中详细介绍。
并且zk中server路径下的每一个服务器节点中都存储有相关数据,主要数据包括注册时间、最后一次心跳时间、ip、UUID等。

3.2. 类cn.uncode.schedule.ZKScheduleManager的定时任务初始化

这里主要介绍分布式任务调度器初始化完毕之后,定时任务启动时的任务注册和任务启动的代码。

cn.uncode.schedule.ZKScheduleManager继承了类 org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler,它又实现了接口org.springframework.scheduling.TaskScheduler,重写以下接口来实现在任务调度的同时将定时任务的信息注册到zookeeper中。

ScheduledFuture<?> schedule(Runnable task, Trigger trigger);
ScheduledFuture<?> schedule(Runnable task, Date startTime);
ScheduledFuture<?> scheduleAtFixedRate(Runnable task, Date startTime, long period);
ScheduledFuture<?> scheduleAtFixedRate(Runnable task, long period);
ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, Date startTime, long delay);
ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, long delay);

重写之后的源代码如下:

@Override
public ScheduledFuture<?> scheduleAtFixedRate(Runnable task, long period) {
    TaskDefine taskDefine = getTaskDefine(task);
    LOGGER.info("spring task init------taskName:{}, period:{}", taskDefine.stringKey(), period);
    taskDefine.setPeriod(period);
    addTask(task, taskDefine);
    return super.scheduleAtFixedRate(taskWrapper(task), period);
}

public ScheduledFuture<?> schedule(Runnable task, Trigger trigger) {
    TaskDefine taskDefine = getTaskDefine(task);
    if(trigger instanceof CronTrigger){
        CronTrigger cronTrigger = (CronTrigger)trigger;
        taskDefine.setCronExpression(cronTrigger.getExpression());
        LOGGER.info("spring task init------trigger:" + cronTrigger.getExpression());
    }
    addTask(task, taskDefine);
    return super.schedule(taskWrapper(task), trigger);
}

public ScheduledFuture<?> schedule(Runnable task, Date startTime) {
    TaskDefine taskDefine = getTaskDefine(task);
    LOGGER.info("spring task init------taskName:{}, period:{}", taskDefine.stringKey(), startTime);
    taskDefine.setStartTime(startTime);
    addTask(task, taskDefine);
    return super.schedule(taskWrapper(task), startTime);
}

public ScheduledFuture<?> scheduleAtFixedRate(Runnable task, Date startTime, long period) {
    TaskDefine taskDefine = getTaskDefine(task);
    LOGGER.info("spring task init------taskName:{}, period:{}", taskDefine.stringKey(), period);
    taskDefine.setStartTime(startTime);
    taskDefine.setPeriod(period);
    addTask(task, taskDefine);
    return super.scheduleAtFixedRate(taskWrapper(task), startTime, period);
}

public ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, Date startTime, long delay) {
    TaskDefine taskDefine = getTaskDefine(task);
    LOGGER.info("spring task init------taskName:{}, delay:{}", taskDefine.stringKey(), delay);
    taskDefine.setStartTime(startTime);
    taskDefine.setPeriod(delay);
    taskDefine.setType(TaskDefine.TASK_TYPE_QSD);
    addTask(task, taskDefine);
    return super.scheduleWithFixedDelay(taskWrapper(task), startTime, delay);
}

public ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, long delay) {
    TaskDefine taskDefine = getTaskDefine(task);
    LOGGER.info("spring task init------taskName:{}, delay:{}", taskDefine.stringKey(), delay);
    taskDefine.setPeriod(delay);
    taskDefine.setType(TaskDefine.TASK_TYPE_QSD);
    addTask(task, taskDefine);
    return super.scheduleWithFixedDelay(taskWrapper(task), delay);
}

主要是在任务调度之前,通过private TaskDefine getTaskDefine(Runnable task);获取任务的详细信息,然后通过private void addTask(Runnable task, TaskDefine taskDefine)将其存储到zookeeper中。
另外一个关键点是,所有的task都经过了 taskWrapper 的包装,先看代码:

/**
 * 将Spring的定时任务进行包装,决定任务是否在本机执行。
 * @param task
 * @return
 */
private Runnable taskWrapper(final Runnable task){
    return new Runnable(){
        public void run(){
            Method targetMethod = null;
            if(task instanceof ScheduledMethodRunnable){
                ScheduledMethodRunnable uncodeScheduledMethodRunnable = (ScheduledMethodRunnable)task;
                targetMethod = uncodeScheduledMethodRunnable.getMethod();
            }else{
                org.springframework.scheduling.support.ScheduledMethodRunnable springScheduledMethodRunnable = (org.springframework.scheduling.support.ScheduledMethodRunnable)task;
                targetMethod = springScheduledMethodRunnable.getMethod();
            }
        String[] beanNames = applicationcontext.getBeanNamesForType(targetMethod.getDeclaringClass());
        if(null != beanNames && StringUtils.isNotEmpty(beanNames[0])){
            String name = ScheduleUtil.getTaskNameFormBean(beanNames[0], targetMethod.getName());
            boolean isOwner = false;
                try {
                    if(!isScheduleServerRegister){
                        Thread.sleep(1000);
                    }
                    if(zkManager.checkZookeeperState()){
                        isOwner = scheduleDataManager.isOwner(name, currenScheduleServer.getUuid());
                        isOwnerMap.put(name, isOwner);
                    }else{
                        // 如果zk不可用,使用历史数据
                        if(null != isOwnerMap){
                            isOwner = isOwnerMap.get(name);
                        }
                    }
                    if(isOwner){
                    task.run();
                    scheduleDataManager.saveRunningInfo(name, currenScheduleServer.getUuid());
                    LOGGER.info("Cron job has been executed.");
                }
                } catch (Exception e) {
                    LOGGER.error("Check task owner error.", e);
                }
        }
        }
    };
}

这里主要控制定时任务的执行,在执行时,需要检测该任务是否属于该服务器。并且考虑到zookeeper不可用的情况,如果不可用查看缓存的任务归属关系。

3.3. 类cn.uncode.schedule.ZKScheduleManager的心跳检测hearBeatTimer

在分布式系统中心跳检测任务是很重要的,负责整个分布式系统的稳定性和健壮性。在3.1.节中的代码中我们看到,心跳检测的定时任务调度代码 hearBeatTimer.schedule(new HeartBeatTimerTask(this), 2000, this.timerInterval); 启动延迟2秒执行,心跳间隔2秒。心跳检测任务 HeartBeatTimerTask 的代码如下:

class HeartBeatTimerTask extends java.util.TimerTask {
    private transient final Logger log = LoggerFactory.getLogger(HeartBeatTimerTask.class);
    ZKScheduleManager manager;

    public HeartBeatTimerTask(ZKScheduleManager aManager) {
        manager = aManager;
    }

    public void run() {
        try {
            Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
            manager.refreshScheduleServer();
        } catch (Exception ex) {
            log.error(ex.getMessage(), ex);
        }
    }
}

从以上代码中可以看到,心跳检测通过 manager.refreshScheduleServer(); 不停在刷新调度服务器信息,代码是:

/**
 * 1. 定时向数据配置中心更新当前服务器的心跳信息。 如果发现本次更新的时间如果已经超过了,服务器死亡的心跳周期,则不能在向服务器更新信息。
 * 而应该当作新的服务器,进行重新注册。
 * 2. 任务分配
 * 3. 检查任务是否属于本机,是否添加到调度器
 * 
 * @throws Exception
 */
public void refreshScheduleServer() throws Exception {
    try {
        // 更新或者注册服务器信息
        rewriteScheduleInfo();
        // 如果任务信息没有初始化成功,不做任务相关的处理
        if (!this.isScheduleServerRegister) {
            return;
        }

        // 重新分配任务
        this.assignScheduleTask();
        // 检查本地任务
        this.checkLocalTask();
    } catch (Throwable e) {
        // 清除内存中所有的已经取得的数据和任务队列,避免心跳线程失败时候导致的数据重复
        this.clearMemoInfo();
        if (e instanceof Exception) {
            throw (Exception) e;
        } else {
            throw new Exception(e.getMessage(), e);
        }
    }
}

进入到方法之后看到,心跳检测任务主要负责:

  1. 方法rewriteScheduleInfo();的功能是,定时向数据配置中心zk更新当前服务器的心跳信息,如果更新失败,重新注册调度服务器信息(在3.1节中已经介绍过了,就是方法scheduleDataManager.registerScheduleServer);
  2. 方法assignScheduleTask();的功能是,定时任务的分配,分配任务的时候会校验该节点是否是leader节点,因为只有leader节点才能分配任务;在分配任务的时候启用了服务器ip黑名单,在黑名单列表中的机器不参与任务分配;
  3. 检查本地的定时任务,添加调度器;该功能是检查是否有通过控制台添加uncode task 类型的定时任务,如果有的话启动该定时任务;这是一种自定义的定时任务类型,任务的启动方式也是自定义的,主要方法在类 DynamicTaskManager 中;

下面看几个关键步骤的代码:首先是leader节点的选择算法代码,

private String getLeader(List<String> serverList){
    if(serverList == null || serverList.size() ==0){
        return "";
    }
    long no = Long.MAX_VALUE;
    long tmpNo = -1;
    String leader = null;
    for(String server:serverList){
        tmpNo =Long.parseLong( server.substring(server.lastIndexOf("$")+1));
        if(no > tmpNo){
            no = tmpNo;
            leader = server;
        }
    }
    return leader;
}

从代码可以看出,选择leader节点的算法是,取serverCode最小的服务器为leader。这种方法的好处是,由于serverCode是递增的,再新增服务器的时候,leader节点不会变化,比较稳定,算法又简单。

3.4. 控制管理类cn.uncode.schedule.ConsoleManager

在该类的功能主要是对外提供的是一些操作任务和数据的方法,包括注册在zk上的定时任务数据的增、删、查;以及定时任务的执行入口。主要代码如下:

public static void addScheduleTask(TaskDefine taskDefine) throws Exception{
    ConsoleManager.getScheduleManager().getScheduleDataManager().addTask(taskDefine);
}

public static void delScheduleTask(TaskDefine taskDefine) {
    try {
        ConsoleManager.scheduleManager.getScheduleDataManager().delTask(taskDefine);
    } catch (Exception e) {
        log.error(e.getMessage(), e);
    }
}

public static List<TaskDefine> queryScheduleTask() {
    List<TaskDefine> taskDefines = new ArrayList<TaskDefine>();
    try {
        List<TaskDefine> tasks = ConsoleManager.getScheduleManager().getScheduleDataManager().selectTask();
        taskDefines.addAll(tasks);
    } catch (Exception e) {
        log.error(e.getMessage(), e);
    }
    return taskDefines;
}

public static boolean isExistsTask(TaskDefine taskDefine) throws Exception{
    return ConsoleManager.scheduleManager.getScheduleDataManager().isExistsTask(taskDefine);
}

/**
* 手动执行定时任务
* @param task
*/
public static void runTask(TaskDefine task) throws Exception{
    Object object = null;
    if (StringUtils.isNotEmpty(task.getTargetBean())) {
        object = ZKScheduleManager.getApplicationcontext().getBean(task.getTargetBean());
    }
    if (object == null) {
        log.error("任务名称 = [{}]---------------未启动成功,targetBean不存在,请检查是否配置正确!!!", task.stringKey());
        throw new Exception("targetBean:"+task.getTargetBean()+"不存在");
    }
    Method method = null;
    try {
        if(StringUtils.isNotEmpty(task.getParams())){
            method = object.getClass().getDeclaredMethod(task.getTargetMethod(), String.class);
        }else{
            method = object.getClass().getDeclaredMethod(task.getTargetMethod());
        }
    } catch (Exception e) {
        log.error(String.format("定时任务bean[%s],method[%s]初始化失败.", task.getTargetBean(), task.getTargetMethod()), e);
        throw new Exception("定时任务:"+task.stringKey()+"初始化失败");
    }
    if (method != null) {
        try {
            if(StringUtils.isNotEmpty(task.getParams())){
                method.invoke(object, task.getParams());
            }else{
                method.invoke(object);
            }
        } catch (Exception e) {
            log.error(String.format("定时任务bean[%s],method[%s]调用失败.", task.getTargetBean(), task.getTargetMethod()), e);
            throw new Exception("定时任务:"+task.stringKey()+"调用失败");
        }
    }
    log.info("任务名称 = [{}]----------启动成功", task.stringKey());
}
3.5. 对外暴露的连个servlet接口ManagerServletManualServlet

servlet ManagerServlet是一个简单管理界面,ManualServlet是一个手动执行定时任务的接口;使用方法是要在项目中的web.xml中配置响应的servlet,配置文件代码如下:

<!-- 配置 uncode schedule 管理后台 -->
<servlet>
    <servlet-name>UncodeSchedule</servlet-name>
    <servlet-class>cn.uncode.schedule.web.ManagerServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>UncodeSchedule</servlet-name>
    <url-pattern>/uncode/schedule</url-pattern>
</servlet-mapping>

<!-- 配置 uncode schedule 手动执行器 -->
<servlet>
    <servlet-name>ScheduleManual</servlet-name>
    <servlet-class>cn.uncode.schedule.web.ManualServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>ScheduleManual</servlet-name>
    <url-pattern>/schedule/manual</url-pattern>
</servlet-mapping>

结束语,源代码分析结束,uncode-schedule分布式定时任务框架实现的主要功能都已覆盖到,有问题的请留言!最后再一次附上源代码 github链接!下一篇预告:《uncode-schedule-manage定制化管理系统

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,594评论 18 139
  • 《分布式任务调度平台XXL-JOB》 一、简介 1.1 概述 XXL-JOB是一个轻量级分布式任务调度框架,其核心...
    许雪里阅读 16,766评论 3 29
  • from http://www.infoq.com/cn/articles/etcd-interpretation...
    小树苗苗阅读 13,939评论 3 38
  • Timer 定时器相信都不会陌生,之所以拿它来做源码分析,是发现整个控制流程可以体现很多有意思的东西。 在业务开发...
    石先阅读 6,328评论 2 13
  • 柳哲 我研究家谱二十余年,收藏家谱不计其数。和圣柳下惠后裔世代传承2700多年的《展氏族谱》,尤为罕见,极其珍贵。...
    柳志儒阅读 242评论 0 0