肝了三晚,終于吃透了Druid連接池

前言

作為一個java程序員载慈,數據庫的JDBC幾乎每天都在做,數據庫連接池Druid每天也在使用珍手,但可能用起來太簡單了(spring中引入依賴即可)办铡,往往忽略了連接池的意義和優(yōu)化

本文從源碼的角度分析Druid的常用配置及原理

連接

當我們程序需要訪問數據庫時辞做,需要創(chuàng)建一個本地到數據庫服務的網絡連接,此時本地代碼就相當于一個數據庫的客戶端寡具,可以通過這個連接去訪問數據秤茅、執(zhí)行sql,如下

Driver driver = new com.mysql.cj.jdbc.Driver();
// 創(chuàng)建連接
Connection con = driver.connect(JDBC_URL, props);
Statement statement = con.createStatement();
ResultSet resultSet = statement.executeQuery("show tables");
while (resultSet.next()) {
    System.out.println(resultSet.getString(1));
}
con.close();

池化技術

由于我們的代碼需要不斷與數據庫交互讀取數據晒杈,如果每次請求數據都創(chuàng)建一個連接的話嫂伞,網絡開銷是很大的,也會導致我們的程序比較慢拯钻,同時連接如果太多也會給數據庫造成壓力

為了解決這個問題帖努,就有了池化技術,把創(chuàng)建好的連接放在池里粪般,用時去池里獲取拼余,節(jié)省了創(chuàng)建連接的時間,也可以通過配置來限定池的最大連接數等

池化技術

連接池最常用的工具基本就是阿里的Druid了亩歹,簡單使用如下

// druid 數據源
DruidDataSource druidDataSource = new DruidDataSource();
// 數據源配置
druidDataSource.setUrl(JDBC_URL);
druidDataSource.setUsername(USERNAME);
druidDataSource.setPassword(PASSWORD);
// 初始化
druidDataSource.init();
// 獲取表名
Connection con = druidDataSource.getConnection();
Statement statement = con.createStatement();
ResultSet resultSet = statement.executeQuery("show tables");
while (resultSet.next()) {
    System.out.println(resultSet.getString(1));
}
con.close();

可以看到使用了Druid匙监,獲取連接不再是直接使用驅動創(chuàng)建連接,而是通過DruidDataSource對象獲取連接

DruidDataSource

接下來就分析DruidDataSource的源碼小作,從三個方面入手:配置亭姥、存儲、線程

配置

首先作為一個連接池工具顾稀,首先要支持重要參數的可配置达罗,以下只列舉一部分常用的配置和其簡單含義,后面的源碼分析會實際的分析每個配置的作用

  • maxActive 最大連接數
  • initialSize 初始化連接數
  • minIdle 最小空閑數
  • keepAlive 是否保持連接
  • asyncInit 是否異步初始化
  • timeBetweenEvictionRunsMillis 回收連接任務運行的頻率
  • minEvictableIdleTimeMillis 最小閑置時間静秆,連接閑置時間小于這個時間不會被回收粮揉,大于有可能被回收
  • maxEvictableIdleTimeMillis 最大閑置時間,連接閑置時間超過這個數是一定被回收的
  • validationQuery 測試是否有效的sql
  • phyTimeoutMillis 連接物理超時時間

有很多配置都是和其它配置配合使用的抚笔,所以很多配置單獨拿出來說它的作用沒有意義扶认,還是要結合代碼看一下

存儲

DruidDataSource作為一個連接池,內部一定會有一個容器來存儲連接殊橙,這應該是最重要的屬性

private volatile DruidConnectionHolder[] connections; // 當前的所有連接

connections存儲的就是所有的數據庫連接對象辐宾,并封裝了一個連接的持有對象DruidConnectionHolder,在持有物理連接的同時蛀柴,也記錄了一些連接的其它屬性螃概,比如:

  • connectTimeMillis 連接建立的時間
  • lastActiveTimeMillis 連接上一次被使用的時間

還有非常重要的一點,這個存儲連接的容器是有排序的鸽疾,每次使用連接都從最后拿,這就導致容器尾部的連接是最活躍的训貌,也就導致前面的連接閑置時間肯定是要高于后面的

計數

同時制肮,池內部有很多計數器來存儲當前各種維度的數量值

private int poolingCount = 0; // 可用連接數
private int activeCount = 0; // 正在使用連接數
private volatile long  discardCount = 0; // 丟棄連接數
private int notEmptyWaitThreadCount = 0; // 等待連接的線程數
線程

DruidDataSource中有幾個線程冒窍,在初始化方法init被創(chuàng)建并運行,它們分別承擔不同的工作

public void init() throws SQLException {
    // ...
    createAndLogThread(); // 開啟負責日志統(tǒng)計的線程
    createAndStartCreatorThread(); // 開啟負責創(chuàng)建連接的線程
    createAndStartDestroyThread(); // 開啟負責負責銷毀連接的線程
    // ...
}

實際上豺鼻,DruidDataSource就是依靠這些線程來維護整個線程池中連接的創(chuàng)建和銷毀任務综液,它們可以看做是線程池的維護人員

小結

所以Druid池簡單來說就是一個連接的容器(connections),可配的參數儒飒,狀態(tài)/計數的存儲組成的一個類谬莹,在初始化方法中會創(chuàng)建多個線程,這些線程在連接池的生命周期一直運行并監(jiān)控這當前線程池的狀態(tài)桩了,并根據配置和計數數據在需要的時候在容器中創(chuàng)建/銷毀線程

Druid

連接池中這幾個線程是可以被替代的附帽,如果我們設置了調度器,則可以按我們自己的方式去調度創(chuàng)建銷毀連接的任務井誉,這屬于比較高級的用法了蕉扮,本文不做探討

線程源碼分析

協(xié)調

線程池內部運行的兩個主要線程:創(chuàng)建連接的線程和銷毀連接的線程,池外部還有我們用戶代碼中想要獲取連接的線程(在此統(tǒng)一稱之為用戶線程)

各個線程可能都要訪問和修改各種計數和連接容器颗圣,為了達到線程安全喳钟,DruidDataSource內部提供了一個統(tǒng)一的ReentrantLock鎖

protected ReentrantLock lock;

各線程也少不了溝通,比如某用戶線程想獲取連接在岂,如何通知創(chuàng)建線程去創(chuàng)建連接奔则,創(chuàng)建線程創(chuàng)建完連接有如何告知用戶線程,為解決這個問題蔽午,DruidDataSource內部提了兩個主要的Condition

protected Condition notEmpty;
protected Condition empty;

其中empty代表空條件易茬,創(chuàng)建線程通過empty.await()即可等待空信號,而用戶線程通過empty.signal()即可發(fā)送空信號給創(chuàng)建線程祠丝,此時用戶線程notEmpty.await()開始等待非空條件疾呻,而創(chuàng)建線程一般會創(chuàng)建連接,創(chuàng)建完成后通過notEmpty.signal()通知線程創(chuàng)建完畢

Condition

創(chuàng)建連接的線程

CreateConnectionThread是專門負責創(chuàng)建連接的写半,可以說DruidDataSource中的連接基本都是由它負責實際創(chuàng)建的(也會有特例岸蜗,比如默認情況下initialSize設置的連接數是在init方法中直接創(chuàng)建的)

大部分情況下CreateConnectionThread是在empty條件上等待空信號,即empty.wait()叠蝇,當得到信號時再創(chuàng)建連接

接下來就看一下CreateConnectionThread的源碼

public class CreateConnectionThread extends Thread {

    public CreateConnectionThread(String name){
        super(name);
        // 設置守護線程
        this.setDaemon(true);
    }

    public void run() {
        initedLatch.countDown();

        long lastDiscardCount = 0;
        int errorCount = 0;
        // 線程一直運行著
        for (;;) {
            // 一.判斷是否需要創(chuàng)建連接
            // 獲取鎖
            lock.lockInterruptibly();

            // 當前被丟棄的連接數
            long discardCount = DruidDataSource.this.discardCount;
            // 對比上一次記錄被丟棄的連接數璃岳,看看是否有變化
            boolean discardChanged = discardCount - lastDiscardCount > 0;
            lastDiscardCount = discardCount;

            try {
                // 標志是否需要等待空信號
                boolean emptyWait = true;

                // 存在異常,當前池連接數為0悔捶,且沒有新丟棄的連接
                if (createError != null
                        && poolingCount == 0
                        && !discardChanged) {
                    emptyWait = false;
                }

                // 如果設置了異步初始化铃慷,且當前創(chuàng)建的連接數少于設置初始連接數,則跳過等待直接創(chuàng)建連接
                if (emptyWait
                        && asyncInit && createCount < initialSize) {
                    emptyWait = false;
                }

                // 如果沒有跳過等待蜕该,并不是實際的去等待犁柜,而是還有一層判斷
                if (emptyWait) {
                    // 有三種情況可以跳過這一步的等待
                    // 1.等待使用連接的線程數大于當前可用連接數
                    // 2.設置了keeplive=true且當前池的總連接數小于設置最小連接數
                    // 3.連續(xù)失敗isFailContinuous(這一項先忽略)
                    // 跳過這一步等待并不代表可以直接創(chuàng)建,還要進行下一步的是否到達最大設置數量的判斷
                    if (poolingCount >= notEmptyWaitThreadCount //
                            && (!(keepAlive && activeCount + poolingCount < minIdle))
                            && !isFailContinuous()
                    ) {
                        // 等待空信號
                        empty.await();
                    }

                    // 如果當前連接數量已超過設置最大數量堂淡,則等待空信號馋缅,否則就可以去創(chuàng)建連接了
                    if (activeCount + poolingCount >= maxActive) {
                        empty.await();
                        // 等待到了空信號扒腕,并不是直接創(chuàng)建連接,而是重新判斷一次是否需要等待萤悴,因為連接數是絕對不能超越maxActive的瘾腰,所以為了安全,必須重新判斷一次
                        continue;
                    }
                }

            } catch (InterruptedException e) {
                //...
            } finally {
                // 釋放鎖
                lock.unlock();
            }

            // 二.開始創(chuàng)建連接
            PhysicalConnectionInfo connection = null;

            try {
                // 創(chuàng)建物理連接
                connection = createPhysicalConnection();
            } catch (SQLException e) {
                //...
            }

            // 加入連接池的連接列表覆履,即connections
            boolean result = put(connection);

            // 如果連接池關閉蹋盆,創(chuàng)建連接線程也停止
            if (closing || closed) {
                break;
            }
        }
    }
}

代碼看起來還是比較復雜,簡單總結一下:
<特殊情況>
創(chuàng)建連接的線程有兩種特殊情況硝全,這兩種情況主要是異步初始化化和處理異常栖雾,這種情況下直接跳過等待,也不需考慮maxActive柳沙,直接創(chuàng)建連接岩灭,這種情況相對特殊暫不做考慮
<常規(guī)情況>
大部分情況下,創(chuàng)建連接的線程要根據minIdle,maxActive等配置以及線程池的狀態(tài)來判斷是否需要等待赂鲤,如果不需要等待也會創(chuàng)建連接

常規(guī)情況下有三種條件噪径,滿意任意一種就可以不需等待直接創(chuàng)建連接,但還有個大前提就是池中的連接總數不能超過maxActive設置的數量

三種條件分別是

  • 當等待使用連接的線程數(notEmptyWaitThreadCount)大于池中可用連接數(poolingCount)数初,即供不應求時
  • 當線程池設置保持連接(keepAlive=true)找爱,且當前池中的總連接數(activeCount + poolingCount)小于設置最小連接數(minIdle),即池中沒有保持足夠的最小連接數時
  • isFailContinuous 連續(xù)失敗時

三種條件如果都不滿足泡孩,則在empty條件上等待索要連接的信號车摄,得到信號則創(chuàng)建連接(還需要判斷最大連接數)

如果三個條件滿足任意一個,但連接數已到達maxActive仑鸥,依然在empty條件上等待信號吮播,得到信號重新再判斷一次,是為了確保連接數不超過最大配置

畫個圖梳理一下

CreateConnectionThread.run()

用一句話總結一下:

CreateConnectionThread負責給線程池創(chuàng)建連接眼俊,當線程池中供不應求意狠、最小保持連接數不足、連續(xù)錯誤時線程會主動創(chuàng)建連接疮胖,否則就會休息節(jié)省體力环戈,得到需求信號再創(chuàng)建連接,創(chuàng)建完成后重新開始審視創(chuàng)建的工作, ps:整個過程確保連接數不能超出設定范圍

銷毀連接的線程

與CreateConnectionThread對應澎灸,DestroyConnectionThread承擔銷毀連接的任務院塞,主要根據配置的參數和當前的技術器,銷毀掉需要銷毀的連接

public class DestroyConnectionThread extends Thread {

    public DestroyConnectionThread(String name) {
        super(name);
        // 設置守護線程
        this.setDaemon(true);
    }

    public void run() {
        initedLatch.countDown();
        // 不斷執(zhí)行
        for (;;) {
            try {
                //...
                // 根據配置timeBetweenEvictionRunsMillis決定銷毀任務執(zhí)行的間隔
                if (timeBetweenEvictionRunsMillis > 0) {
                    Thread.sleep(timeBetweenEvictionRunsMillis);
                } else {
                    Thread.sleep(1000);
                }
                //...
                // 執(zhí)行銷毀任務
                destroyTask.run();
            } catch (InterruptedException e) {
                break;
            }
        }
    }

}

銷毀連接的任務實時性要求并不是太高性昭,所以可能會隔一段時間才去計算并銷毀一次拦止,這個間隔的時間就是配置timeBetweenEvictionRunsMillis

其中DestroyTask的run方法定義如下

public void run() {
    shrink(true, keepAlive);

    if (isRemoveAbandoned()) {
        removeAbandoned();
    }
}

主要調用的方法即shrink,意指收縮線程池糜颠,重點看一下這個方法:

public void shrink(boolean checkTime, boolean keepAlive) {
    // 獲取鎖
    lock.lockInterruptibly();
    
    // 是否需要補充
    boolean needFill = false;
    // 驅逐的數量
    int evictCount = 0;
    // 需要贝葱梗活的數量
    int keepAliveCount = 0;
    int fatalErrorIncrement = fatalErrorCount - fatalErrorCountLastShrink;
    fatalErrorCountLastShrink = fatalErrorCount;
    
    try {
        // 未初始化完成不執(zhí)行
        if (!inited) {
            return;
        }

        // 池中可用連接數超出最小連接數的數量
        final int checkCount = poolingCount - minIdle;
        final long currentTimeMillis = System.currentTimeMillis();
        // 循環(huán)池中可用連接
        for (int i = 0; i < poolingCount; ++i) {
            DruidConnectionHolder connection = connections[i];

            // 異常的處理艺玲,暫不做考慮
            if ((onFatalError || fatalErrorIncrement > 0) && (lastFatalErrorTimeMillis > connection.connectTimeMillis))  {
                keepAliveConnections[keepAliveCount++] = connection;
                continue;
            }
            
            // 如果檢查時間括蝠,銷毀線程傳入的是true
            if (checkTime) {
                // 如果設置了物聯連接超時時間
                if (phyTimeoutMillis > 0) {
                    // 當前連接連接時間過過了超時時間鞠抑,加入要待回收集合中
                    long phyConnectTimeMillis = currentTimeMillis - connection.connectTimeMillis;
                    if (phyConnectTimeMillis > phyTimeoutMillis) {
                        evictConnections[evictCount++] = connection;
                        continue;
                    }
                }

                // 計算當前連接已閑置的時間
                long idleMillis = currentTimeMillis - connection.lastActiveTimeMillis;

                // 如果連接閑置時間比較短,則可不被回收忌警,可以直接跳出循環(huán)搁拙,因為連接池是尾部更活躍,后面的肯定更短不需要判斷了
                if (idleMillis < minEvictableIdleTimeMillis
                        && idleMillis < keepAliveBetweenTimeMillis
                ) {
                    break;
                }

                // 如果連接閑置時間超出了設置的 最小閑置時間
                if (idleMillis >= minEvictableIdleTimeMillis) {
                    // 如果當前連接的位置在checkCount以內法绵,則加入待回收集合
                    if (checkTime && i < checkCount) {
                        evictConnections[evictCount++] = connection;
                        continue;
                    // 否則如果已超出最大閑置時間箕速,也要加入待回收集合  
                    } else if (idleMillis > maxEvictableIdleTimeMillis) {
                        evictConnections[evictCount++] = connection;
                        continue;
                    }
                }

                // 如果閑置時間超出保活檢測時間朋譬,且設置了keepAlive盐茎,則加入待驗證保活的集合中
                if (keepAlive && idleMillis >= keepAliveBetweenTimeMillis) {
                    keepAliveConnections[keepAliveCount++] = connection;
                }
            } else {
                //...
            }
        }

        // 要刪除的連接總數徙赢,實際上keepAliveCount只是有可能被刪除字柠,還沒有最終定論,這里做法是先刪除掉狡赐,如果驗證連接可用后續(xù)再加回來即可
        int removeCount = evictCount + keepAliveCount;
        if (removeCount > 0) {
            // 刪除連接池中的廢棄連接窑业,由于廢棄的連接一定是前removeCount個連接,所以直接使用復制即可刪除
            System.arraycopy(connections, removeCount, connections, 0, poolingCount - removeCount);
            Arrays.fill(connections, poolingCount - removeCount, poolingCount, null);
            // 當前可用連接數變小
            poolingCount -= removeCount;
        }
        keepAliveCheckCount += keepAliveCount;

        // 如果設置了闭硖耄活常柄,且總連接數小于最小連接數,則需要補充
        if (keepAlive && poolingCount + activeCount < minIdle) {
            needFill = true;
        }
    } finally {
        lock.unlock();
    }

    // 如果有要回收的連接
    if (evictCount > 0) {
        // 循環(huán)
        for (int i = 0; i < evictCount; ++i) {
            DruidConnectionHolder item = evictConnections[i];
            Connection connection = item.getConnection();
            // 關閉連接
            JdbcUtils.close(connection);
            destroyCountUpdater.incrementAndGet(this);
        }
        // 清空需要回收的連接集合
        Arrays.fill(evictConnections, null);
    }

    // 如果有要進行辈罄蓿活的連接
    if (keepAliveCount > 0) {
        // 循環(huán)要蔽髋耍活的連接
        for (int i = keepAliveCount - 1; i >= 0; --i) {
            DruidConnectionHolder holer = keepAliveConnections[i];
            Connection connection = holer.getConnection();
            holer.incrementKeepAliveCheckCount();

            boolean validate = false;
            try {
                // 驗證鏈接是否有效,此時要用到配置的validationQuery來驗證連接的有效性哨颂,如果沒設置喷市,就默認有效
                this.validateConnection(connection);
                validate = true;
            } catch (Throwable error) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("keepAliveErr", error);
                }
            }

            boolean discard = !validate;
            // 如果連接有效
            if (validate) {
                holer.lastKeepTimeMillis = System.currentTimeMillis();
                // 重新加入連接池最左側
                boolean putOk = put(holer, 0L, true);
                if (!putOk) {
                    discard = true;
                }
            }

            // 如果連接無效
            if (discard) {
                try {
                    // 關閉連接
                    connection.close();
                } catch (Exception e) {
                    // skip
                }

                lock.lock();
                try {
                    // 記錄被丟棄的連接數+1
                    discardCount++;
                    // 如果且總連接數小于最小連接數,發(fā)出空信號
                    if (activeCount + poolingCount <= minIdle) {
                        emptySignal();
                    }
                } finally {
                    lock.unlock();
                }
            }
        }
        this.getDataSourceStat().addKeepAliveCheckCount(keepAliveCount);
        // 清空需要迸剌铮活的連接集合
        Arrays.fill(keepAliveConnections, null);
    }

    // 如果需要補充
    if (needFill) {
        lock.lock();
        try {
            // 計算需要補充的數量东抹,createTaskCount是使用自定義調度時的邏輯,暫時忽略
            int fillCount = minIdle - (activeCount + poolingCount + createTaskCount);
            // 發(fā)出空信號
            for (int i = 0; i < fillCount; ++i) {
                emptySignal();
            }
        } finally {
            lock.unlock();
        }
    } else if (onFatalError || fatalErrorIncrement > 0) {
        // 異常處理 忽略..
    }
}

核心代碼依然相當復雜沃测,還是嘗試總結一下

(一) 銷毀任務實時性不高缭黔,銷毀線程執(zhí)行是一個定時任務,時間間隔可配

(二) 銷毀線程只考慮數目為poolingCount的池中可用連接蒂破,正在使用的連接不可能被銷毀(其實也已不在池中)

(三) 銷毀線程會從前往后循環(huán)查看所有的池中連接馏谨,主要判斷是否需要銷毀或者保活附迷,主要包含如下邏輯:

  • 循環(huán)前會提前計算當前可用連接超出最小限制連接的數量惧互,為checkCount哎媚,這個數量其實就是線程池中多余連接的數量,而且按照容器的排序喊儡,越前面的連接越不活躍拨与,所以前checkCount就是多余連接,但多余連接不一定會被移除艾猜,有可能因為閑置時間(說明剛用完不久)較短而被暫時保留
  • 如果當前連接閑置時間比較短买喧,不需要進行銷毀或保活測試匆赃,直接跳出循環(huán)淤毛,因為后面的連接活躍度更高
  • 如果連接閑置時間比較長,比如超過了設置的最大閑置時間算柳,或超過最小閑置時間且當前連接本身就是多余連接低淡,就會從池中移出至待銷毀的集合中
  • 如果連接閑置時間比較長,超過了彼蚕睿活測試的設定時間(且keepAlive)蔗蹋,就會從池中移出至待測試有效性的集合中
  • 待銷毀集合的連接后續(xù)會被直接關閉,待測試有效性集合的連接需要測試連接是否可用滥壕,如果不可用直接銷毀纸颜,通過校驗加回至連接池中
  • 由于銷毀了很多連接,可能導致keepAlive情況下最小連接數不夠了绎橘,所以需要通過empty.signal通知創(chuàng)建線程補充連接

再畫個示意圖

CreateConnectionThread

用戶線程

用戶線程主要是去池中獲取連接胁孙,上文也提到過,是從最后拿連接称鳞,重點方法takeLast

DruidConnectionHolder takeLast() throws InterruptedException, SQLException {
    try {
        while (poolingCount == 0) {
            // 發(fā)送空信號涮较,讓創(chuàng)建線程創(chuàng)建連接
            emptySignal(); // send signal to CreateThread create connection
            // 增加等待線程數
            notEmptyWaitThreadCount++;
            
            // 等待非空信號
            try {
                notEmpty.await(); // signal by recycle or creator
            } finally {
                notEmptyWaitThreadCount--;
            }
            //...
        }
    } catch (InterruptedException ie) {
        //...
    }

    // 有了可用連接
    // 可用連接減一,因為要拿出用了
    decrementPoolingCount();
    // 取出最后一個連接
    DruidConnectionHolder last = connections[poolingCount];
    connections[poolingCount] = null;
    // 返回
    return last;
}

邏輯就是取池中最后一個連接冈止,如果沒有通知創(chuàng)建線程創(chuàng)建連接

最后

費了好大勁狂票,基本捋明白了Druid連接池的重要代碼,感覺真的很復雜

總結一下Druid的優(yōu)點

  • 連接的創(chuàng)建銷毀異步執(zhí)行熙暴,保證效率
  • 連接池的固定最大連接數避免了連接的過度創(chuàng)建
  • 連接池中連接的存活時間可配置闺属,保證高并發(fā)下連接不會被回收,可重復利用
  • 連接池的敝苊梗活機制掂器,可以固定維持一定數量的連接長期保留在池中,還可以定時檢測連接的有效性俱箱,固定維持的連接可以在并發(fā)驟增的情況下提前預熱国瓮,避免一次性建立過多連接

其實還是有很多地方并沒有想太明白,而且很多結論也很難測試,如果有誤乃摹,歡迎指正

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末禁漓,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子孵睬,更是在濱河造成了極大的恐慌播歼,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件肪康,死亡現場離奇詭異荚恶,居然都是意外死亡,警方通過查閱死者的電腦和手機磷支,發(fā)現死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來食寡,“玉大人雾狈,你說我怎么就攤上這事〉种澹” “怎么了善榛?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長呻畸。 經常有香客問我移盆,道長,這世上最難降的妖魔是什么伤为? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任咒循,我火速辦了婚禮,結果婚禮上绞愚,老公的妹妹穿的比我還像新娘叙甸。我一直安慰自己,他們只是感情好位衩,可當我...
    茶點故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布裆蒸。 她就那樣靜靜地躺著,像睡著了一般糖驴。 火紅的嫁衣襯著肌膚如雪僚祷。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天贮缕,我揣著相機與錄音辙谜,去河邊找鬼。 笑死跷睦,一個胖子當著我的面吹牛筷弦,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼烂琴,長吁一口氣:“原來是場噩夢啊……” “哼爹殊!你這毒婦竟也來了?” 一聲冷哼從身側響起奸绷,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤梗夸,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后号醉,有當地人在樹林里發(fā)現了一具尸體反症,經...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年畔派,在試婚紗的時候發(fā)現自己被綠了铅碍。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出瓮具,到底是詐尸還是另有隱情组底,我是刑警寧澤,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏径密。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一躺孝、第九天 我趴在偏房一處隱蔽的房頂上張望享扔。 院中可真熱鬧,春花似錦括细、人聲如沸伪很。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽锉试。三九已至,卻和暖如春览濒,著一層夾襖步出監(jiān)牢的瞬間呆盖,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工贷笛, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留应又,地道東北人。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓乏苦,卻偏偏與公主長得像株扛,于是被迫代替她去往敵國和親尤筐。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,802評論 2 345

推薦閱讀更多精彩內容