RocketMQ刷盤機制

概覽

RocketMQ的存儲讀寫是基于JDK NIO的內存映射機制的,消息存儲時首先將消息追加到內存中。在根據不同的刷盤策略在不同的時間進行刷盤。如果是同步刷盤,消息追加到內存后,將同步調用MappedByteBuffer的force()方法,同步等待刷盤結果,進行刷盤結果返回。如果是異步刷盤,在消息追加到內存后立刻,不等待刷盤結果立刻返回存儲成功結果給消息發送端。RocketMQ使用一個單獨的線程按照一個設定的頻率執行刷盤操作。通過在broker配置文件中配置flushDiskType來設定刷盤方式,ASYNC_FLUSH(異步刷盤)、SYNC_FLUSH(同步刷盤)。默認為異步刷盤。本次以Commitlog文件刷盤機制為例來講解刷盤機制。Consumequeue、IndexFile刷盤原理和Commitlog一直。索引文件的刷盤機制并不是采取定時刷盤機制,而是每更新一次索引文件就會將上一次的改動刷寫到磁盤。

刷盤服務是將commitlog、consumequeue兩者中的MappedFile文件中的MappedByteBuffer或者FileChannel中的內存中的數據,刷寫到磁盤。還有將IndexFile中的MappedByteBuffer(this.mappedByteBuffer = this.mappedFile.getMappedByteBuffer())中內存的數據刷寫到磁盤。

刷盤服務的入口

刷盤服務的入口是CommitLog類對象,FlushCommitLogService是刷盤服務對象,如果是同步刷盤它被賦值為GroupCommitService,如果是異步刷盤它被賦值為FlushRealTimeService;還有一個FlushCommitLogService的commitLogService對象,這個是將 TransientStorePoll 中的直接內存ByteBuffer,寫到FileChannel映射的磁盤文件中的服務。

// 異步、同步刷盤服務初始化
if (FlushDiskType.SYNC_FLUSH == defaultMessageStore.getMessageStoreConfig().getFlushDiskType()) {
    // 同步刷盤服務為 GroupCommitService
    this.flushCommitLogService = new GroupCommitService();
} else {
    // 異步刷盤服務為 FlushRealTimeService
    this.flushCommitLogService = new FlushRealTimeService();
}

// 定時將 transientStorePoll 中的直接內存 ByteBuffer,提交到內存映射 MappedByteBuffer 中
this.commitLogService = new CommitRealTimeService();
刷盤方法調用入口

putMessage()方法,將消息寫入內存的方式不同,調用的刷盤方式也不同。如果是asyncPutMessage()異步將消息寫入內存,submitFlushRequest()方法是刷盤入口。如果是putMessage()同步將消息寫入內存,handleDiskFlush()方法是刷盤入口。handleDiskFlush()和submitFlushRequest()都包含有同步刷盤和異步刷盤的方法。

// 異步的方式存放消息
public CompletableFuture<PutMessageResult> asyncPutMessage(final MessageExtBrokerInner msg) {

    // 異步存儲消息,提交刷盤請求
    CompletableFuture<PutMessageStatus> flushResultFuture = submitFlushRequest(result, putMessageResult, msg);
    CompletableFuture<PutMessageStatus> replicaResultFuture = submitReplicaRequest(result, putMessageResult, msg);
    // 根據刷盤結果副本結果,返回存放消息的結果
    return flushResultFuture.thenCombine(replicaResultFuture, (flushStatus, replicaStatus) -> {
        if (flushStatus != PutMessageStatus.PUT_OK) {
            putMessageResult.setPutMessageStatus(PutMessageStatus.FLUSH_DISK_TIMEOUT);
        }
        if (replicaStatus != PutMessageStatus.PUT_OK) {
            putMessageResult.setPutMessageStatus(replicaStatus);
        }
        return putMessageResult;
    });
}
// 同步方式存放消息
public PutMessageResult putMessage(final MessageExtBrokerInner msg) {

    // handle 硬盤刷新
    handleDiskFlush(result, putMessageResult, msg);
    // handle 高可用
    handleHA(result, putMessageResult, msg);
    // 返回存儲消息的結果
    return putMessageResult;
}

同步刷盤

一條消息調用一次刷盤服務,等待刷盤結果返回,然后再將結果返回;才能處理下一條刷盤消息。以handleDiskFlush()方法來介紹同步刷盤和異步刷盤,這里是區分刷盤方式的分水嶺。

/**
 * 一條消息進行刷盤
 * @param result 擴展到內存ByteBuffer的結果
 * @param putMessageResult 放入ByteBuffer這個過程的結果
 * @param messageExt 存放的消息
 */
public void handleDiskFlush(AppendMessageResult result, PutMessageResult putMessageResult, MessageExt messageExt) {
    // Synchronization flush 同步
    if (FlushDiskType.SYNC_FLUSH == this.defaultMessageStore.getMessageStoreConfig().getFlushDiskType()) {
        final GroupCommitService service = (GroupCommitService) this.flushCommitLogService;
        // 是否等待服務器將這一條消息存儲完畢再返回(等待刷盤完成),還是直接處理其他寫隊列requestsWrite里面的請求
        if (messageExt.isWaitStoreMsgOK()) {
            //刷盤請求
            GroupCommitRequest request = new GroupCommitRequest(result.getWroteOffset() + result.getWroteBytes());
            //放入寫請求隊列
            service.putRequest(request);
            // 同步等待獲取刷盤結果
            CompletableFuture<PutMessageStatus> flushOkFuture = request.future();
            PutMessageStatus flushStatus = null;
            try {
                // 5秒超市等待刷盤結果
                flushStatus = flushOkFuture.get(this.defaultMessageStore.getMessageStoreConfig().getSyncFlushTimeout(),
                        TimeUnit.MILLISECONDS);
            } catch (InterruptedException | ExecutionException | TimeoutException e) {
                //flushOK=false;
            }
            // 刷盤失敗,更新存放消息結果超時
            if (flushStatus != PutMessageStatus.PUT_OK) {
                log.error("do groupcommit, wait for flush failed, topic: " + messageExt.getTopic() + " tags: " + messageExt.getTags()
                    + " client address: " + messageExt.getBornHostString());
                putMessageResult.setPutMessageStatus(PutMessageStatus.FLUSH_DISK_TIMEOUT);
            }
        } else {
            // 喚醒處理刷盤請求寫磁盤線程,處理刷盤請求線程和提交刷盤請求之前的協調,通過CountDownLatch(1)操作,通過控制hasNotified狀態來實現寫隊列和讀隊列的交換
            service.wakeup();
        }
    }
    // 異步
    // Asynchronous flush
    else {
        if (!this.defaultMessageStore.getMessageStoreConfig().isTransientStorePoolEnable()) {
            flushCommitLogService.wakeup();
        } else {
            commitLogService.wakeup();
        }
    }
}

同步刷盤會創造一個刷盤請求,然后將請求放入處理寫刷盤請求的requestsWrite隊列,請求里面封裝了CompletableFuture對象用來記錄刷盤結果,利用CompletableFuturee的get方法同步等待獲取結果。flushStatus = flushOkFuture.get(this.defaultMessageStore.getMessageStoreConfig().getSyncFlushTimeout(),TimeUnit.MILLISECONDS);flushStatus為刷盤結果,默認等待5秒超時。

GroupCommitService為一個線程,用來定時處理requestsWrite隊列里面的寫刷盤請求,進行刷盤;它的requestsWrite和requestsRead隊列進行了讀寫分離,寫GroupCommitRequest請求到requestsWrite隊列,讀GroupCommitRequest請求從requestsRead讀取,讀取請求今夕寫盤操作。這兩個隊列,形成了化零為整,將一個個請求,劃分為一批,處理一批的GroupCommitRequest請求,然后requestsWrite和requestsRead隊列進行交換,requestsRead作為寫隊列,requestsWrite作為讀隊列,實現讀寫分離。從中使用CountDownLatch2來實現處理刷盤請求線程和提交刷盤請求之前的協調,通過控制hasNotified狀態來實現寫隊列和讀隊列的交換。

// 同步刷盤服務
class GroupCommitService extends FlushCommitLogService {
    // 兩個隊列,讀寫請求分離
    // 刷盤服務寫入請求隊列
    private volatile List<GroupCommitRequest> requestsWrite = new ArrayList<GroupCommitRequest>();
    // 刷盤服務讀取請求隊列
    private volatile List<GroupCommitRequest> requestsRead = new ArrayList<GroupCommitRequest>();
    // 將請求同步寫入requestsWrite
    public synchronized void putRequest(final GroupCommitRequest request) {
        synchronized (this.requestsWrite) {
            this.requestsWrite.add(request);
        }
        // 喚醒刷盤線程處理請求
        this.wakeup();
    }
    // 寫隊列和讀隊列交換
    private void swapRequests() {
        List<GroupCommitRequest> tmp = this.requestsWrite;
        this.requestsWrite = this.requestsRead;
        this.requestsRead = tmp;
    }

    private void doCommit() {
        // 上鎖讀請求隊列
        synchronized (this.requestsRead) {
            if (!this.requestsRead.isEmpty()) {
                // 每一個請求進行刷盤
                for (GroupCommitRequest req : this.requestsRead) {
                    // There may be a message in the next file, so a maximum of
                    // two times the flush
                    // 一個落盤請求,處理兩次,第一次為false,進行刷盤,一次刷盤的數據是多個offset,并不是只有當前這個offset的值,這個offset的值進行了刷盤,這個請求的第二次刷盤,這個offset已經已經落盤了,
                    // flushWhere這個值在flush方法已經更新變大,所以flushOK=true,跳出for循環,通知flushOKFuture已經完成。
                    boolean flushOK = false;
                    for (int i = 0; i < 2 && !flushOK; i++) {
                        // 是否已經刷過,false未刷,true已刷
                        flushOK = CommitLog.this.mappedFileQueue.getFlushedWhere() >= req.getNextOffset();
                        // false 刷盤
                        if (!flushOK) {
                            //0代碼立刻刷盤,不管緩存中消息有多少
                            CommitLog.this.mappedFileQueue.flush(0);
                        }
                    }
                    // flushOK:true,返回ok,已經刷過盤了,不用再刷盤;false:刷盤中,返回超時
                    // 喚醒等待刷盤結果的線程
                    req.wakeupCustomer(flushOK ? PutMessageStatus.PUT_OK : PutMessageStatus.FLUSH_DISK_TIMEOUT);
                }
                // 更新checkpoint的刷盤commitlog的最后刷盤時間,但是只寫寫到了checkpoint的內存ByteBuffer,并沒有刷盤
                long storeTimestamp = CommitLog.this.mappedFileQueue.getStoreTimestamp();
                if (storeTimestamp > 0) {
                    CommitLog.this.defaultMessageStore.getStoreCheckpoint().setPhysicMsgTimestamp(storeTimestamp);
                }
                // 清空隊列
                this.requestsRead.clear();
            } else {
                // Because of individual messages is set to not sync flush, it
                // will come to this process
                // 因為個別的消息不是同步刷盤的,所以它回到這里進行處理
                CommitLog.this.mappedFileQueue.flush(0);
            }
        }
    }

    public void run() {
        CommitLog.log.info(this.getServiceName() + " service started");
        // 線程是否停止
        while (!this.isStopped()) {
            try {
                // 設置hasNotified為false,未被通知,然后交換寫對隊列和讀隊列,重置waitPoint為(1),休息200ms,出事化為10ms,finally設置hasNotified為未被通知,交換寫對隊列和讀隊列
                this.waitForRunning(10);
                // 進行刷盤服務處理,一次處理一批請求,單個請求返回給等待刷盤服務結果的線程
                this.doCommit();
            } catch (Exception e) {
                CommitLog.log.warn(this.getServiceName() + " service has exception. ", e);
            }
        }
        // 處理非正常停機,sleep10ms,交換寫請求隊列和讀請求隊列,等待數據處理
        // Under normal circumstances shutdown, wait for the arrival of the
        // request, and then flush
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            CommitLog.log.warn("GroupCommitService Exception, ", e);
        }

        synchronized (this) {
            this.swapRequests();
        }
        // 進行請求處理
        this.doCommit();

        CommitLog.log.info(this.getServiceName() + " service end");
    }

    @Override
    protected void onWaitEnd() {
        // 寫隊列和讀隊列交換
        this.swapRequests();
    }

    @Override
    public String getServiceName() {
        return GroupCommitService.class.getSimpleName();
    }
    // 5 分鐘
    @Override
    public long getJointime() {
        return 1000 * 60 * 5;
    }
}
處理刷盤請求線程和提交刷盤請求之前的協調
# org.apache.rocketmq.common.ServiceThread
// 喚醒處理刷盤請求寫磁盤線程,處理刷盤請求線程和提交刷盤請求之前的協調,通過控制hasNotified狀態來實現寫隊列和讀隊列的交換
public void wakeup() {
    // hasNotified默認值是false,未被喚醒,這個操作之后喚醒了,處理刷盤請求
    if (hasNotified.compareAndSet(false, true)) {
        // waitPoint默認是1,然后其他線程處理
        waitPoint.countDown(); // notify
    }
}

/**
 * 設置hasNotified為false,未被通知,然后交換寫對隊列和讀隊列,重置waitPoint為(1),休息200ms,finally設置hasNotified為未被通知,交換寫對隊列和讀隊列
 * @param interval 200ms
 */
protected void waitForRunning(long interval) {
    // compareAndSet(except,update);如果真實值value==except,設置value值為update,返回true;如果真實值value !=except,真實值不變,返回false;
    // 如果hasNotified真實值為true,那么設置真實值為false,返回true;hasNotified真實值為false,那就返回false,真實值不變
    // 如果已經通知了,那就狀態變為未通知,如果是同步刷盤任務,交換寫請求隊列和讀請求隊列
    if (hasNotified.compareAndSet(true, false)) {
        // 同步刷盤:寫隊列和讀隊列交換
        this.onWaitEnd();
        return;
    }
    // 重置countDownLatch對象,等待接受刷盤請求的線程寫入請求到requestsRead,寫完后,waitPoint.countDown,喚醒處理刷盤請求的線程,開始刷盤
    //entry to wait
    waitPoint.reset();

    try {
        // 等待interval毫秒
        waitPoint.await(interval, TimeUnit.MILLISECONDS);
    } catch (InterruptedException e) {
        log.error("Interrupted", e);
    } finally {
        // 設置是否通知為false
        hasNotified.set(false);
        this.onWaitEnd();
    }
}
// 等待這個方法的步驟完成。比如:同步刷盤:寫隊列和讀隊列交換
protected void onWaitEnd() {
}

異步刷盤

異步刷盤根據是否開啟TransientStorePool暫存池,來區分是否有commit操作。開啟TransientStorePool會將writerBuffer中的數據commit到FileChannel中(fileChannel.write(writerBuffer)),然后再將FileChannel中的數據通過flush操作(fileChannel.force())到磁盤中;
如果為開啟TransientStorePool,就不會有commit操作,直接flush(MappedByteBuffer.force())到磁盤中。

// 異步刷盤
// Asynchronous flush
if (!this.defaultMessageStore.getMessageStoreConfig().isTransientStorePoolEnable()) {
    //執行flush操作
    flushCommitLogService.wakeup();
} else {
    //執行commit操作,然后喚醒執行flush操作
    commitLogService.wakeup();
}
CommitRealTimeService

定時將 transientStorePool 中的直接內存 ByteBuffer,提交到FileChannel中,然后喚醒刷盤操作。

// 定時將 transientStorePoll 中的直接內存 ByteBuffer,提交到FileChannel中
class CommitRealTimeService extends FlushCommitLogService {

    private long lastCommitTimestamp = 0;

    @Override
    public String getServiceName() {
        return CommitRealTimeService.class.getSimpleName();
    }

    @Override
    public void run() {
        CommitLog.log.info(this.getServiceName() + " service started");
        // 刷盤線程是否停止
        while (!this.isStopped()) {
            // writerBuffer寫數據到FileChannel時間間隔200ms
            int interval = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getCommitIntervalCommitLog();
            // writerBuffer寫數據到FileChannel頁數大小4
            int commitDataLeastPages = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getCommitCommitLogLeastPages();

            // writerBuffer寫數據到FileChannel跨度時間間隔200ms
            int commitDataThoroughInterval =
                CommitLog.this.defaultMessageStore.getMessageStoreConfig().getCommitCommitLogThoroughInterval();
            // 開始時間
            long begin = System.currentTimeMillis();
            // 觸發commit機制有兩種方式:1.commit時間超過了兩次commit時間間隔,然后只要有數據就進行提交 2.commit數據頁數大于默認設置的4頁
            // 本次commit時間>上次commit時間+兩次commit時間間隔,則進行commit,不用關心commit頁數的大小,設置commitDataLeastPages=0
            if (begin >= (this.lastCommitTimestamp + commitDataThoroughInterval)) {
                this.lastCommitTimestamp = begin;
                commitDataLeastPages = 0;
            }

            try {
                // result=false,表示提交了數據,多與上次提交的位置;表示此次有數據提交;result=true,表示沒有新的數據被提交
                boolean result = CommitLog.this.mappedFileQueue.commit(commitDataLeastPages);
                long end = System.currentTimeMillis();
                // result = false means some data committed.表示此次有數據提交,然后進行刷盤
                if (!result) {
                    this.lastCommitTimestamp = end; // result = false means some data committed.
                    //now wake up flush thread.
                    // 喚起刷盤線程,進行刷盤
                    flushCommitLogService.wakeup();
                }

                if (end - begin > 500) {
                    log.info("Commit data to file costs {} ms", end - begin);
                }
                // 暫停200ms,再運行
                this.waitForRunning(interval);
            } catch (Throwable e) {
                CommitLog.log.error(this.getServiceName() + " service has exception. ", e);
            }
        }

        boolean result = false;
        // 正常關機,循環10次,進行10次的有數據就提交的操作
        for (int i = 0; i < RETRY_TIMES_OVER && !result; i++) {
            result = CommitLog.this.mappedFileQueue.commit(0);
            CommitLog.log.info(this.getServiceName() + " service shutdown, retry " + (i + 1) + " times " + (result ? "OK" : "Not OK"));
        }
        CommitLog.log.info(this.getServiceName() + " service end");
    }
}
FlushRealTimeService

異步刷盤服務

class FlushRealTimeService extends FlushCommitLogService {
    private long lastFlushTimestamp = 0;
    // 刷盤次數
    private long printTimes = 0;

    public void run() {
        CommitLog.log.info(this.getServiceName() + " service started");

        while (!this.isStopped()) {
            // 默認值為false,表示await方法等待,如果為true,表示使用Thread.sleep方法等待
            boolean flushCommitLogTimed = CommitLog.this.defaultMessageStore.getMessageStoreConfig().isFlushCommitLogTimed();
            // 刷盤任務時間間隔,多久刷一次盤500ms
            int interval = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushIntervalCommitLog();
            // 一次刷寫任務至少包含頁數,如果待刷寫數據不足,小于該參數配置的值,將忽略本次刷寫任務,默認4頁
            int flushPhysicQueueLeastPages = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushCommitLogLeastPages();
            // 兩次真實刷寫任務最大跨度,默認10s
            int flushPhysicQueueThoroughInterval =
                CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushCommitLogThoroughInterval();

            // 打印記錄日志標志
            boolean printFlushProgress = false;

            // Print flush progress
            long currentTimeMillis = System.currentTimeMillis();
            // 觸發刷盤機制有兩種方式:1.刷盤時間超過了兩次刷盤時間間隔,然后只要有數據就進行提交 2.commit數據頁數大于默認設置的4頁
            // 本次刷盤時間>上次刷盤時間+兩次刷盤時間間隔,則進行刷盤,不用關心刷盤頁數的大小,設置commitDataLeastPages=0
            if (currentTimeMillis >= (this.lastFlushTimestamp + flushPhysicQueueThoroughInterval)) {
                this.lastFlushTimestamp = currentTimeMillis;
                flushPhysicQueueLeastPages = 0;
                // 每間隔10次記錄一次刷盤日志
                printFlushProgress = (printTimes++ % 10) == 0;
            }

            try {
                // 刷盤之前,進行線程sleep
                if (flushCommitLogTimed) {
                    Thread.sleep(interval);
                } else {
                    this.waitForRunning(interval);
                }
                // 打印記錄日志
                if (printFlushProgress) {
                    this.printFlushProgress();
                }
                // 刷盤開始時間
                long begin = System.currentTimeMillis();
                // 刷盤
                CommitLog.this.mappedFileQueue.flush(flushPhysicQueueLeastPages);
                long storeTimestamp = CommitLog.this.mappedFileQueue.getStoreTimestamp();
                // 更新checkpoint最后刷盤時間
                if (storeTimestamp > 0) {
                    CommitLog.this.defaultMessageStore.getStoreCheckpoint().setPhysicMsgTimestamp(storeTimestamp);
                }
                long past = System.currentTimeMillis() - begin;
                if (past > 500) {
                    log.info("Flush data to disk costs {} ms", past);
                }
            } catch (Throwable e) {
                CommitLog.log.warn(this.getServiceName() + " service has exception. ", e);
                this.printFlushProgress();
            }
        }
        // while循環結束,正常關機,保證所有的數據刷寫到磁盤
        // Normal shutdown, to ensure that all the flush before exit
        boolean result = false;
        for (int i = 0; i < RETRY_TIMES_OVER && !result; i++) {
            result = CommitLog.this.mappedFileQueue.flush(0);
            CommitLog.log.info(this.getServiceName() + " service shutdown, retry " + (i + 1) + " times " + (result ? "OK" : "Not OK"));
        }
        // 打印日志
        this.printFlushProgress();

        CommitLog.log.info(this.getServiceName() + " service end");
    }

    @Override
    public String getServiceName() {
        return FlushRealTimeService.class.getSimpleName();
    }

    private void printFlushProgress() {
        // CommitLog.log.info("how much disk fall behind memory, "
        // + CommitLog.this.mappedFileQueue.howMuchFallBehind());
    }

    @Override
    public long getJointime() {
        return 1000 * 60 * 5;
    }
}   

刷盤是否開啟TransientStorePool的區別

這里講一下刷盤是否開啟TransientStorePool的區別。

image.png
不開啟TransientStorePool:

MappedByteBuffer是直接內存,它暫時存儲了message消息,MappedFile.mapp()方法做好MappedByteBuffer對象直接內存和落盤文件的映射關系,然后flush()方法執行MappedByteBuffer.force():強制將ByteBuffer中的任何內容的改變寫入到磁盤文件。

開啟TransientStorePool:

MappedFile的writerBuffer為直接開辟的內存,然后MappedFile的初始化操作,做好FileChannel和磁盤文件的映射,commit()方法實質是執行fileChannel.write(writerBuffer),將writerBuffer的數據寫入到FileChannel映射的磁盤文件,flush操作執行FileChannel.force():將映射文件中的數據強制刷新到磁盤。

TransientStorePool的作用

TransientStorePool 相當于在內存層面做了讀寫分離,寫走內存磁盤,讀走pagecache,同時最大程度消除了page cache的鎖競爭,降低了毛刺。它還使用了鎖機制,避免直接內存被交換到swap分區。
參考:https://github.com/apache/rocketmq/issues/2466

FileChannel.force VS MappedByteBuffer.force區別

This method is only guaranteed to force changes that were made to this channel's file via the methods defined in this class. It may or may not force changes that were made by modifying the content of a{@link MappedByteBuffer <i>mapped byte buffer</i>} obtained by invoking the {@link #map map} method. Invoking the {@link MappedByteBuffer#force force} method of the mapped byte buffer will force changes made to the buffer's content to be written.

image.png

FileChannel和MappedByteBuffer都是NIO模塊的類,ByteBuffer直接內存映射到磁盤文件通過FileChannel。
FileChannel.force()只會將FileChannel類中方法使FileChannel發生改變的內容強制刷新到存儲設備文件中。
MappedByteBuffer.force()會將Map類中方法使ByteBuffer發生改變的內容強制刷新到存儲設備文件中。

來源:http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/classes/java/nio/channels/FileChannel.java

發送消息的方式可以分為:同步、異步、oneway
消息寫入ByteBuffer的處理方式分為:同步、異步
刷盤的處理方式分為:同步、異步
三個處理方式互不干擾,發送消息的為同步,寫入ByteBuffer可以為異步的方式,刷盤可以為同步的方式。最后,消息發送端會以同步的方式等待寫入ByteBuffer、刷盤成功的結果。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374