4-fescar(seata)源码分析-事务的提交&回滚

4-fescar源码分析-事务的提交&回滚

一、官方介绍

1.TM 向 TC 发起针对 XID 的全局提交或回滚决议。
2.TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。

上一篇分析了事务分支的注册逻辑。
那这一篇主要分析fescar如何提交全局事务?TM提交事务commit请求后,TC如何处理?RM如何管控分支事务的commit?

--

二、(原理)源码分析

紧接着上一篇的RM向TC注册了事务分支,依然借助官网的example例图进行出发。

2.1 demo
  • 继续看下官网的结构图:


    image.png
    image.png
项目中存在官方的example模块,里面就模拟了上图的相关流程:先回到本节主题:**事务的提交**

--

三、commit

3.1.TM触发commit
  • 触发逻辑

    #TransactionalTemplate
    public Object execute(TransactionalExecutor business) throws TransactionalExecutor.ExecutionException {
        ...
        tx.commit();
        ...
        return rs;
    }
    
    #DefaultGlobalTransaction
    @Override
    public void commit() throws TransactionException {
        check();
        RootContext.unbind();
        if (role == GlobalTransactionRole.Participant) {
            // Participant has no responsibility of committing
            return;
        }
        status = transactionManager.commit(xid);
    
    }
    
    #DefaultTransactionManager
    @Override
    public GlobalStatus commit(String xid) throws TransactionException {
        long txId = XID.getTransactionId(xid);
        GlobalCommitRequest globalCommit = new GlobalCommitRequest();
        globalCommit.setTransactionId(txId);
        GlobalCommitResponse response = (GlobalCommitResponse) syncCall(globalCommit);
        return response.getGlobalStatus();
    }
    

    TM 端很简单,

    • 1.从RootContext中移除XID
    • 2.构造GlobalCommitRequest参数
    • 3.向TC端发送commit消息

    主要逻辑继续跟踪TC对于commit类型消息的处理

    --

3.2.TC处理commit
  • 继续忽略接收消息的细节,跟begin消息触发逻辑一致。直接进入核心处理:

    #DefaultCoordinator
    @Override
    protected void doGlobalCommit(GlobalCommitRequest request, GlobalCommitResponse response, RpcContext rpcContext)
        throws TransactionException {
        response.setGlobalStatus(core.commit(XID.generateXID(request.getTransactionId())));
    }
    
    #DefaultCore
    @Override
    public GlobalStatus commit(String xid) throws TransactionException {
        GlobalSession globalSession = SessionHolder.findGlobalSession(XID.getTransactionId(xid));
        if (globalSession == null) {
            return GlobalStatus.Finished;
        }
        GlobalStatus status = globalSession.getStatus();
    
        globalSession.closeAndClean(); // Highlight: Firstly, close the session, then no more branch can be registered.
    
        if (status == GlobalStatus.Begin) {
            if (globalSession.canBeCommittedAsync()) {
                asyncCommit(globalSession);
            } else {
                doGlobalCommit(globalSession, false);
            }
        }
        return globalSession.getStatus();
    }
    
    #DefaultCore
    private void asyncCommit(GlobalSession globalSession) throws TransactionException {
        globalSession.addSessionLifecycleListener(SessionHolder.getAsyncCommittingSessionManager());
        SessionHolder.getAsyncCommittingSessionManager().addGlobalSession(globalSession);
    }
    

    TC端逻辑就是如此:

    • 1.关闭session相关资源
    • 2.释放branchSeeion的相关的锁
    • 3.将ASYNC_COMMITTING_SESSION_MANAGER的DefaultSessionManager加入GlobalSession生命周期
    • 4.将当前的GlobalSession加入到将ASYNC_COMMITTING_SESSION_MANAGER的DefaultSessionManager中。

    看了上面的逻辑,好像跟事务的提交没啥关系,那么不要慌,还记的最开始介绍的初始化逻辑么:

    public void init() {
        retryRollbacking.scheduleAtFixedRate(new Runnable() {
    
           ...
    
        asyncCommitting.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                try {
                    handleAsyncCommitting();
                } catch (Exception e) {
                    LOGGER.info("Exception async committing ... ", e);
                }
    
            }
        }, 0, 10, TimeUnit.MILLISECONDS);
    
       ...
    }
    

    这里早就在启动的时候跑了线程去异步轮训commit任务了,我们究竟看看做了什么?

    #DefaultCoordinator
    private void handleAsyncCommitting() {
        Collection<GlobalSession> asyncCommittingSessions = SessionHolder.getAsyncCommittingSessionManager().allSessions();
        for (GlobalSession asyncCommittingSession : asyncCommittingSessions) {
            try {
                core.doGlobalCommit(asyncCommittingSession, true);
            } catch (TransactionException ex) {
                LOGGER.info("Failed to async committing [{}] {} {}",
                        asyncCommittingSession.getTransactionId(), ex.getCode(), ex.getMessage());
            }
        }
    }
    
    #DefaultCore
    @Override
    public void doGlobalCommit(GlobalSession globalSession, boolean retrying) throws TransactionException {
        for (BranchSession branchSession : globalSession.getSortedBranches()) {
            BranchStatus currentStatus = branchSession.getStatus();
            if (currentStatus == BranchStatus.PhaseOne_Failed) {
                continue;
            }
            try {
                BranchStatus branchStatus = resourceManagerInbound.branchCommit(XID.generateXID(branchSession.getTransactionId()), branchSession.getBranchId(), branchSession.getResourceId(), branchSession.getApplicationData());
    
                switch (branchStatus) {
                    case PhaseTwo_Committed:
                        globalSession.removeBranch(branchSession);
                        continue;
                    case PhaseTwo_CommitFailed_Unretryable:
                        if (globalSession.canBeCommittedAsync()) {
                            LOGGER.error("By [{}], failed to commit branch {}", branchStatus, branchSession);
                            continue;
                        } else {
                            globalSession.changeStatus(GlobalStatus.CommitFailed);
                            globalSession.end();
                            LOGGER.error("Finally, failed to commit global[{}] since branch[{}] commit failed",
                                    globalSession.getTransactionId(), branchSession.getBranchId());
                            return;
                        }
                    default:
                        if (!retrying) {
                            queueToRetryCommit(globalSession);
                            return;
                        }
                        if (globalSession.canBeCommittedAsync()) {
                            LOGGER.error("By [{}], failed to commit branch {}", branchStatus, branchSession);
                            continue;
                        } else {
                            LOGGER.error("Failed to commit global[{}] since branch[{}] commit failed, will retry later.",
                                    globalSession.getTransactionId(), branchSession.getBranchId());
                            return;
                        }
    
                }
    
            } catch (Exception ex) {
                ...
            }
        if (globalSession.hasBranch()) {
            LOGGER.info("Global[{}] committing is NOT done.", globalSession.getTransactionId());
            return;
        }
        globalSession.changeStatus(GlobalStatus.Committed);
        globalSession.end();
        LOGGER.info("Global[{}] committing is successfully done.", globalSession.getTransactionId());
    }
    

    这里就是轮训SessionHolder.getAsyncCommittingSessionManager().allSessions()出来的GlobalSession,也就是前面加入的逻辑了。
    轮训GlobalSession出来后,再次轮训其中的branchSession,最后调用resourceManagerInbound.branchCommit再次进行分支事务的commit。逻辑好像清楚了。
    最终移除分支事务且根据分支事务返回的状态进行全局事务状态的更新

    继续跟踪代码:

    @Override
    public BranchStatus branchCommit(String xid, long branchId, String resourceId, String applicationData)
        throws TransactionException {
        try {
            BranchCommitRequest request = new BranchCommitRequest();
            request.setXid(xid);
            request.setBranchId(branchId);
            request.setResourceId(resourceId);
            request.setApplicationData(applicationData);
    
            GlobalSession globalSession = SessionHolder.findGlobalSession(XID.getTransactionId(xid));
            BranchSession branchSession = globalSession.getBranch(branchId);
    
            BranchCommitResponse response = (BranchCommitResponse)messageSender.sendSyncRequest(resourceId, branchSession.getClientId(), request);
            return response.getBranchStatus();
        } catch (IOException e) {
            throw new TransactionException(FailedToSendBranchCommitRequest, branchId + "/" + xid, e);
        } catch (TimeoutException e) {
            throw new TransactionException(FailedToSendBranchCommitRequest, branchId + "/" + xid, e);
        }
    }
    

    这里构造了BranchCommitRequest参数,并进行了消息的发送,那么这里是TC端,此处可想而知,分支事务的派发就是通知RM进行相关的处理了。
    RM具体执行完成后,TC更新分支事务的状态,完成TC端逻辑的最终处理。

    那么具体的分支事务的执行,就紧接着回到了RM的逻辑处理了,继续转到RM处理commit逻辑。

--

3.3.RM处理TC传送的commit
  • RM接受TC端消息依然是AbstractRpcRemoting中channelRead,之后进行消息的分发,这里我们在启动的rpc时,初始化的是AbstractRpcRemotingClient,因此进入以下逻辑:

    @Override
    public void dispatch(long msgId, ChannelHandlerContext ctx, Object msg) {
        if (clientMessageListener != null) {
            String remoteAddress = NetUtil.toStringAddress(ctx.channel().remoteAddress());
            clientMessageListener.onMessage(msgId, remoteAddress, msg, this);
        }
    }
    

    继续看消息监听逻辑:

    @Override
    public void onMessage(long msgId, String serverAddress, Object msg, ClientMessageSender sender) {
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info("onMessage:" + msg);
        }
        if (msg instanceof BranchCommitRequest) {
            handleBranchCommit(msgId, serverAddress, (BranchCommitRequest)msg, sender);
        } else if (msg instanceof BranchRollbackRequest) {
            handleBranchRollback(msgId, serverAddress, (BranchRollbackRequest)msg, sender);
        }
    }
    

    最终找到处理器:RMHandlerAT

    @Override
    protected void doBranchCommit(BranchCommitRequest request, BranchCommitResponse response) throws TransactionException {
        String xid = request.getXid();
        long branchId = request.getBranchId();
        String resourceId = request.getResourceId();
        String applicationData = request.getApplicationData();
        LOGGER.info("AT Branch committing: " + xid + " " + branchId + " " + resourceId + " " + applicationData);
        BranchStatus status = dataSourceManager.branchCommit(xid, branchId, resourceId, applicationData);
        response.setBranchStatus(status);
        LOGGER.info("AT Branch commit result: " + status);
    }
    
    @Override
    public BranchStatus branchCommit(String xid, long branchId, String resourceId, String applicationData) throws TransactionException {
        return asyncWorker.branchCommit(xid, branchId, resourceId, applicationData);
    }
    

    看看AsyncWorker做了什么?

    /**
     * 用于在分支事务提交后异步删除undo sql记录
     */
    @Override
    public BranchStatus branchCommit(String xid, long branchId, String resourceId, String applicationData) throws TransactionException {
        if (ASYNC_COMMIT_BUFFER.size() < ASYNC_COMMIT_BUFFER_LIMIT) {
            ASYNC_COMMIT_BUFFER.add(new Phase2Context(xid, branchId, resourceId, applicationData));
        } else {
            LOGGER.warn("Async commit buffer is FULL. Rejected branch [" + branchId + "/" + xid + "] will be handled by housekeeping later.");
        }
        return BranchStatus.PhaseTwo_Committed;
    }
    

    根据提交的分支事务构造Phase2Context,并将其加入ASYNC_COMMIT_BUFFER中。
    还记得rpc服务启动时,会初始化AsyncWorker的轮训逻辑吗?

    public synchronized void init() {
        LOGGER.info("Async Commit Buffer Limit: " + ASYNC_COMMIT_BUFFER_LIMIT);
        timerExecutor = new ScheduledThreadPoolExecutor(1, new NamedThreadFactory("AsyncWorker", 1, true));
        timerExecutor.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                try {
    
                    doBranchCommits();
    
    
                } catch (Throwable e) {
                    LOGGER.info("Failed at async committing ... " + e.getMessage());
    
                }
            }
        }, 10, 1000 * 1, TimeUnit.MILLISECONDS);
    }
    
    
    
    private void doBranchCommits() {
        if (ASYNC_COMMIT_BUFFER.size() == 0) {
            return;
        }
        Map<String, List<Phase2Context>> mappedContexts = new HashMap<>();
        Iterator<Phase2Context> iterator = ASYNC_COMMIT_BUFFER.iterator();
        while (iterator.hasNext()) {
            ...
    
                List<Phase2Context> contextsGroupedByResourceId = mappedContexts.get(resourceId);
                for (Phase2Context commitContext : contextsGroupedByResourceId) {
                    try {
                        UndoLogManager.deleteUndoLog(commitContext.xid, commitContext.branchId, conn);
                    ...
                }
    
            } finally {
               ...
            }
        }
    }
    

    此时就是删除回滚脚本的具体逻辑了。至此,commit逻辑结束。即commit无非就是释放了session,且删除了回滚的脚本undo日志,结果的执行在execute就已经完结。当然,此处还有个锁的释放。锁在后面一节单独分析。

--

四、rollback

4.1.TM触发rollback
  • 触发逻辑

    #TransactionalTemplate
    public Object execute(TransactionalExecutor business) throws TransactionalExecutor.ExecutionException {
        ...
        tx.rollback();
        ...
        return rs;
    }
    
    #DefaultGlobalTransaction
    @Override
    public void rollback() throws TransactionException {
        check();
        RootContext.unbind();
        if (role == GlobalTransactionRole.Participant) {
            // Participant has no responsibility of committing
            return;
        }
    
        /**
         * 通过将事务ID传递给TM来进行指定事务的回滚,Fescar同样提供了一个默认的DefaultTransactionManager实现
         */
        status = transactionManager.rollback(xid);
    
    }
    
    
    #DefaultTransactionManager
    /**
     * 这里发起了一个同步调用,使用事务的XID组装了一个GlobalRollbackRequest,同时向Fescar-Server发起远程调用表示需要对XID这个事务进行全局回滚,
     * 在这边阻塞直到收到Fescar-Server执行完毕的回复,至此调用者的逻辑结束。
     */
    @Override
    public GlobalStatus rollback(String xid) throws TransactionException {
        long txId = XID.getTransactionId(xid);
        GlobalRollbackRequest globalRollback = new GlobalRollbackRequest();
        globalRollback.setTransactionId(txId);
        GlobalRollbackResponse response = (GlobalRollbackResponse) syncCall(globalRollback);
        return response.getGlobalStatus();
    }
    

    TM 端很简单,

    • 1.从RootContext中移除XID
    • 2.构造GlobalRollbackRequest参数
    • 3.向TC端发送rollback消息

    主要逻辑继续跟踪TC对于rollback类型消息的处理

    --

4.2.TC处理rollback
  • 继续忽略接收消息的细节,跟commit消息触发逻辑一致。直接进入核心处理:

    #DefaultCoordinator
    @Override
    protected void doGlobalRollback(GlobalRollbackRequest request, GlobalRollbackResponse response, RpcContext rpcContext) throws TransactionException {
        response.setGlobalStatus(core.rollback(XID.generateXID(request.getTransactionId())));
    }
    
    #DefaultCore
    @Override
    public GlobalStatus rollback(String xid) throws TransactionException {
        GlobalSession globalSession = SessionHolder.findGlobalSession(XID.getTransactionId(xid));
        if (globalSession == null) {
            return GlobalStatus.Finished;
        }
        GlobalStatus status = globalSession.getStatus();
    
        globalSession.close(); // Highlight: Firstly, close the session, then no more branch can be registered.
    
        if (status == GlobalStatus.Begin) {
            globalSession.changeStatus(GlobalStatus.Rollbacking);
            doGlobalRollback(globalSession, false);
    
        }
        return globalSession.getStatus();
    }
    
    #DefaultCore
    @Override
    public void doGlobalRollback(GlobalSession globalSession, boolean retrying) throws TransactionException {
        for (BranchSession branchSession : globalSession.getReverseSortedBranches()) {
            BranchStatus currentBranchStatus = branchSession.getStatus();
            if (currentBranchStatus == BranchStatus.PhaseOne_Failed) {
                continue;
            }
            try {
                BranchStatus branchStatus = resourceManagerInbound.branchRollback(XID.generateXID(branchSession.getTransactionId()), branchSession.getBranchId(),
                        branchSession.getResourceId(), branchSession.getApplicationData());
    
                switch (branchStatus) {
                    case PhaseTwo_Rollbacked:
                        globalSession.removeBranch(branchSession);
                        continue;
                    case PhaseTwo_RollbackFailed_Unretryable:
                        GlobalStatus currentStatus = globalSession.getStatus();
                        if (currentStatus.name().startsWith("Timeout")) {
                            globalSession.changeStatus(GlobalStatus.TimeoutRollbackFailed);
                        } else {
                            globalSession.changeStatus(GlobalStatus.RollbackFailed);
                        }
                        globalSession.end();
                        return;
                    default:
                        if (!retrying) {
                            queueToRetryRollback(globalSession);
                        }
                        return;
                }
            } catch (Exception ex) {
                ...
            }
        }
        GlobalStatus currentStatus = globalSession.getStatus();
        if (currentStatus.name().startsWith("Timeout")) {
            globalSession.changeStatus(GlobalStatus.TimeoutRollbacked);
        } else {
            globalSession.changeStatus(GlobalStatus.Rollbacked);
        }
        globalSession.end();
    }
    
    #DefaultCoordinator
    @Override
    public BranchStatus branchRollback(String xid, long branchId, String resourceId, String applicationData)
        throws TransactionException {
        try {
            BranchRollbackRequest request = new BranchRollbackRequest();
            request.setXid(xid);
            request.setBranchId(branchId);
            request.setResourceId(resourceId);
            request.setApplicationData(applicationData);
    
            GlobalSession globalSession = SessionHolder.findGlobalSession(XID.getTransactionId(xid));
            BranchSession branchSession = globalSession.getBranch(branchId);
    
            BranchRollbackResponse response = (BranchRollbackResponse)messageSender.sendSyncRequest(resourceId,branchSession.getClientId(), request);
            return response.getBranchStatus();
        } catch (IOException e) {
           ...
        }
    }
    

    TC端逻辑就是如此:

    • 1.关闭session相关资源
    • 2.更新globalSession对应的状态为GlobalStatus.Rollbacking
    • 3.遍历globalSession中的branchSession,构造BranchRollbackRequest并进行向RM发送分支事务回滚的请求。
    • 4.TC获取分支的回滚状态进行分支移除
    • 5.TC最终更新全局事务状态

    看了上面的逻辑,跟提交commit逻辑类似,但简单于从媒体,因为没有异步任务进行,而是直接遍历branchSession并发送到RM处理

    那么具体的分支事务的执行,就紧接着回到了RM的逻辑处理了,继续转到RM处理commit逻辑。

    --

4.3.RM处理TC传送的rollback
  • RM接受TC端消息依然是AbstractRpcRemoting中channelRead,之后进行消息的分发,这里我们在启动的rpc时,初始化的是AbstractRpcRemotingClient,因此进入以下逻辑:

    #AbstractRpcRemotingClient
    @Override
    public void dispatch(long msgId, ChannelHandlerContext ctx, Object msg) {
        if (clientMessageListener != null) {
            String remoteAddress = NetUtil.toStringAddress(ctx.channel().remoteAddress());
            clientMessageListener.onMessage(msgId, remoteAddress, msg, this);
        }
    }
    

    继续看消息监听逻辑:

    #RmMessageListener
    @Override
    public void onMessage(long msgId, String serverAddress, Object msg, ClientMessageSender sender) {
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info("onMessage:" + msg);
        }
        if (msg instanceof BranchCommitRequest) {
            handleBranchCommit(msgId, serverAddress, (BranchCommitRequest)msg, sender);
        } else if (msg instanceof BranchRollbackRequest) {
            handleBranchRollback(msgId, serverAddress, (BranchRollbackRequest)msg, sender);
        }
    }
    

    最终找到处理器:RMHandlerAT

    #RMHandlerAT
    @Override
    protected void doBranchRollback(BranchRollbackRequest request, BranchRollbackResponse response) throws TransactionException {
        String xid = request.getXid();
        long branchId = request.getBranchId();
        String resourceId = request.getResourceId();
        String applicationData = request.getApplicationData();
        LOGGER.info("AT Branch rolling back: " + xid + " " + branchId + " " + resourceId);
        BranchStatus status = dataSourceManager.branchRollback(xid, branchId, resourceId, applicationData);
        response.setBranchStatus(status);
        LOGGER.info("AT Branch rollback result: " + status);
    
    }
    
    #DataSourceManager
    @Override
    public BranchStatus branchRollback(String xid, long branchId, String resourceId, String applicationData) throws TransactionException {
        DataSourceProxy dataSourceProxy = get(resourceId);
        if (dataSourceProxy == null) {
            throw new ShouldNeverHappenException();
        }
        try {
            /**
             * 回滚的关键所在了
             *
             * 整个回滚操作中最重要的就是UndoLogManager,在这里通过undolog记录的用于回滚的信息进行数据库回滚,Fescar-Server的回滚实现思路是根据INSERT,UPDATE和DELETE三种语句进行解析,反向生成用于回滚的SQL,
             * 具体实现可以参见fescar-rm-distribution项目中undo包中的MySQLUndoDeleteExecutor,MySQLUndoInsertExecutor和MySQLUndoUpdateExecutor,
             * 最终Fescar-Server会将回滚操作的结果组装成GlobalRollbackResponse返回给TM调用方,至此Fescar-Server的回滚逻辑完成。
             */
            UndoLogManager.undo(dataSourceProxy, xid, branchId);
        } catch (TransactionException te) {
            if (te.getCode() == TransactionExceptionCode.BranchRollbackFailed_Unretriable) {
                return BranchStatus.PhaseTwo_RollbackFailed_Unretryable;
            } else {
                return BranchStatus.PhaseTwo_RollbackFailed_Retryable;
            }
        }
        return BranchStatus.PhaseTwo_Rollbacked;
    }
    

    回滚的关键所在了
    UndoLogManagers是整个回滚操作中核心逻辑,通过之前execute执行时生成的undolog记录,Fescar-Server根据INSERT,UPDATE和DELETE三种语句进行解析,反向生成用于回滚的SQL,进而还原数据。
    具体实现可以参见fescar-rm-distribution项目中undo包中的MySQLUndoDeleteExecutor,MySQLUndoInsertExecutor和MySQLUndoUpdateExecutor,
    最终Fescar-Server会将回滚操作的结果组装成GlobalRollbackResponse返回给TM调用方,至此Fescar-Server的回滚逻辑完成。

    此时就是回滚的具体逻辑了。至此,rollback逻辑结束。

    因此,fescar如果决议是全局提交,此时分支事务此时已经完成提交,不需要同步协调处理(只需要异步清理回滚日志),Phase2 可以非常快速地完成。借助官网的图示;如果是回滚,就直接根据undo日志生成回滚sql直接将执行后的数据还原。就是如下了:

    image.png
至此,事务提交&回滚分析完成。

--

四.未完待续...

后续分析主要针对fescar 锁

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

推荐阅读更多精彩内容

  • 怎么如此平静, 感觉像是走错了片场.为什么呢, 因为上下游工作在同一个线程呀骚年们! 这个时候上游每次调用emit...
    Young1657阅读 1,440评论 2 1
  • 先看官网两张图【引用来自官网】:image.png 官网说明: 1.首先 ReferenceConfig 类的 i...
    致虑阅读 1,021评论 0 2
  • 黑小飞 开最帅的车,泡最辣的妞,杀最狠的人,那是007干的事,但我们那几年不也曾向往过这种生活吗? 还记不记得第一...
    宋明qn99521阅读 548评论 0 0
  • 眼神的力量 文/笑看人生 眼睛是心灵的窗口,是人类的指明灯,照亮了你前方的道路和全世界。生活中会有多少种你熟悉而又...
    笑看人生_b68a阅读 485评论 0 1
  • 墨记·日歌(贰佰壹拾玖·2019.1.20) 天地玄黄 1 为什么,要劈开清浊不定的混沌之网 让沉重的坠落,让轻盈...
    湖北叶潇阅读 513评论 0 6