数据库中间件 Sharding-JDBC 源码分析 —— SQL 解析之 SQL 解析

1. 概述

上篇文章《词法解析》分享了词法解析器 Lexer 是如何解析 SQL 里的词法。本文分享 SQL 解析引擎是如何解析与理解 SQL 的。因为本文建立在《词法解析》之上,你需要阅读它后再开始这段旅程。

Parser 有两个组件:

  • SQLParsingEngine :SQL 解析引擎
  • SQLParser :SQL 解析器

SQLParsingEngine 调用 SQLParserFactory 生成 SQLParser,SQLParser 调用 LexerEngine(封装了 Lexer) 解析 SQL 词法。

2. SQLParsingEngine

SQLParsingEngine,SQL 解析引擎。其parse()方法作为 SQL 解析入口,本身不带复杂逻辑,通过调用 SQL 对应的 SQLParser 进行 SQL 解析。
核心代码如下:

// SQLParsingEngine.java
public SQLStatement parse() {
        LexerEngine lexerEngine = LexerEngineFactory.newInstance(dbType, sql);
        lexerEngine.nextToken();
        return SQLParserFactory.newInstance(dbType, lexerEngine.getCurrentToken().getType(), shardingRule, lexerEngine).parse();
    }

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class LexerEngineFactory {
    
    /**
     * Create lexical analysis engine instance.
     * 
     * @param dbType database type
     * @param sql SQL
     * @return lexical analysis engine instance
     */
    public static LexerEngine newInstance(final DatabaseType dbType, final String sql) {
        switch (dbType) {
            case H2:
            case MySQL:
                return new LexerEngine(new MySQLLexer(sql));
            case Oracle:
                return new LexerEngine(new OracleLexer(sql));
            case SQLServer:
                return new LexerEngine(new SQLServerLexer(sql));
            case PostgreSQL:
                return new LexerEngine(new PostgreSQLLexer(sql));
            default:
                throw new UnsupportedOperationException(String.format("Cannot support database [%s].", dbType));
        }
    }
}

主要流程为:

  1. 根据 db 类型和 sql 语句,生成对应的Lexer,并作为创建LexerEngine的构造参数。目前支持的 db 类型为 H2、MySQL、Oracle、SQLServer、PostgreSQL。
  2. 调用lexerEngine.nextToken()方法,生成第一个 Token。以查询语句为例,第一个 Token 的词法字面量为“select”,其类型为DefaultKeyword#SELECT
  3. 根据第一个 Token 的类型,以及 db 类型,获取对应的 SQLParse,如MySQLSelectParser
// SQLParserFactory.java
 public static SQLParser newInstance(final DatabaseType dbType, final TokenType tokenType, final ShardingRule shardingRule, final LexerEngine lexerEngine) {
        if (!(tokenType instanceof DefaultKeyword)) {
            throw new SQLParsingUnsupportedException(tokenType);
        }
        switch ((DefaultKeyword) tokenType)  {
            case SELECT:
                return SelectParserFactory.newInstance(dbType, shardingRule, lexerEngine);
            case INSERT:
                return InsertParserFactory.newInstance(dbType, shardingRule, lexerEngine);
            case UPDATE:
                return UpdateParserFactory.newInstance(dbType, shardingRule, lexerEngine);
            case DELETE:
                return DeleteParserFactory.newInstance(dbType, shardingRule, lexerEngine);
            case CREATE:
                return CreateParserFactory.newInstance(dbType, shardingRule, lexerEngine);
            case ALTER:
                return AlterParserFactory.newInstance(dbType, shardingRule, lexerEngine);
            case DROP:
                return DropParserFactory.newInstance(dbType, shardingRule, lexerEngine);
            case TRUNCATE:
                return TruncateParserFactory.newInstance(dbType, shardingRule, lexerEngine);
            case SET:
            case COMMIT:
            case ROLLBACK:
            case SAVEPOINT:
            case BEGIN:
                return TCLParserFactory.newInstance(dbType, shardingRule, lexerEngine);
            default:
                throw new SQLParsingUnsupportedException(lexerEngine.getCurrentToken().getType());
        }
    }

// SelectParserFactory.java
 public static AbstractSelectParser newInstance(final DatabaseType dbType, final ShardingRule shardingRule, final LexerEngine lexerEngine) {
        switch (dbType) {
            case H2:
            case MySQL:
                return new MySQLSelectParser(shardingRule, lexerEngine);
            case Oracle:
                return new OracleSelectParser(shardingRule, lexerEngine);
            case SQLServer:
                return new SQLServerSelectParser(shardingRule, lexerEngine);
            case PostgreSQL:
                return new PostgreSQLSelectParser(shardingRule, lexerEngine);
            default:
                throw new UnsupportedOperationException(String.format("Cannot support database [%s].", dbType));
        }
    }

最后,调用SQLParser#parse方法,对 SQL 进行解析。下面,我们就以 MySQL 的查询语句为例,探讨其解析流程。

3. 查询 SQL (MySQL) 解析流程

查询 SQL 解析主流程如下:


// AbstractSelectParser.java
public final SelectStatement parse() {
        SelectStatement result = parseInternal();
        if (result.containsSubQuery()) {
            result = result.mergeSubQueryStatement();
        }
        // TODO move to rewrite
        appendDerivedColumns(result);
        appendDerivedOrderBy(result);
        return result;
    }

3.1 SelectStatement

SelectStatement,查询语句解析结果对象。

public final class SelectStatement extends DQLStatement {
    
    // 是否是“*”
    private boolean containStar;
    // 最后一个查询项下一个 Token 的开始位置
    private int selectListLastPosition;
    // 最后一个分组项下一个 Token 的开始位置
    private int groupByLastPosition;
    // 查询项
    private final Set<SelectItem> items = new HashSet<>();
    // 分组项
    private final List<OrderItem> groupByItems = new LinkedList<>();
    // 排序项
    private final List<OrderItem> orderByItems = new LinkedList<>();
    // 分页信息
    private Limit limit;
}

3.2 AbstractSQLStatement

增删改查解析结果对象的抽象父类

public abstract class AbstractSQLStatement implements SQLStatement {
    
    // SQL 类型
    private final SQLType type;
    // 表名
    private final Tables tables = new Tables();
    // 过滤条件。只有对路由结果有影响的条件,才添加进数组
    private final Conditions conditions = new Conditions();
    // SQL标记对象
    private final List<SQLToken> sqlTokens = new LinkedList<>();
}

这里需要注意的是,conditions属性存放的是对路由结果有影响的条件,即分片键的过滤条件。

3.3 SQLToken

SQLToken,SQL标记对象接口,记录着标记对象的起始位置。下面都是它的实现类:

说明
GeneratedKeyToken 自增主键标记对象
TableToken 表标记对象
ItemsToken 选择项标记对象
OffsetToken 分页偏移量标记对象
OrderByToken 排序标记对象
RowCountToken 分页长度标记对象

3.4 解析流程分析

我们以 MySQL 的查询语句为例,直接看AbstractSelectParser#parseInternal()的源码:

  // AbstractSelectParser.java
  private SelectStatement parseInternal() {
        SelectStatement result = new SelectStatement();
        lexerEngine.nextToken();
        parseInternal(result);
        return result;
    }

    // MySQLSelectParser.java
    @Override
    protected void parseInternal(final SelectStatement selectStatement) {
        parseDistinct();
        parseSelectOption();
        parseSelectList(selectStatement, getItems());
        parseFrom(selectStatement);
        parseWhere(getShardingRule(), selectStatement, getItems());
        parseGroupBy(selectStatement);
        parseHaving();
        parseOrderBy(selectStatement);
        parseLimit(selectStatement);
        parseSelectRest();
    }

该方法调用lexerEngine对 SQL 进行词法解析,并生产SelectStatement对象。

这里有一点我们需要注意,SQLParser 并不是等 Lexer 解析完词法( Token ),再根据词法去理解 SQL。而是,在理解 SQL 的过程中,调用 Lexer 进行分词。

3.4.1 #parseDistinct()

解析 DISTINCT、DISTINCTROW 谓语。
核心代码DistinctClauseParser#parse

  /**
   * Parse distinct.
   */
  public final void parse() {
        lexerEngine.skipAll(DefaultKeyword.ALL);
        Collection<Keyword> distinctKeywords = new LinkedList<>();
        distinctKeywords.add(DefaultKeyword.DISTINCT);
        distinctKeywords.addAll(Arrays.asList(getSynonymousKeywordsForDistinct()));
        lexerEngine.unsupportedIfEqual(distinctKeywords.toArray(new Keyword[distinctKeywords.size()]));
    }

  public class MySQLDistinctClauseParser extends DistinctClauseParser {
    
    public MySQLDistinctClauseParser(final LexerEngine lexerEngine) {
        super(lexerEngine);
    }
    
    @Override
    protected Keyword[] getSynonymousKeywordsForDistinct() {
        return new Keyword[] {MySQLKeyword.DISTINCTROW};
    }
}

此处 DISTINCT 和 DISTINCT(字段) 不同,它是针对查询结果做去重,即整行重复。举个例子:

mysql> SELECT item_id, order_id FROM t_order_item;
+---------+----------+
| item_id | order_id |
+---------+----------+
| 1 | 1 |
| 1 | 1 |
+---------+----------+
2 rows in set (0.03 sec)
mysql> SELECT DISTINCT item_id, order_id FROM t_order_item;
+---------+----------+
| item_id | order_id |
+---------+----------+
| 1 | 1 |
+---------+----------+
1 rows in set (0.02 sec)

3.4.2 #parseSelectList()

将 SQL 查询字段 按照逗号( , )切割成多个选择项( SelectItem)。核心代码如下SelectListClauseParser#parse

public void parse(final SelectStatement selectStatement, final List<SelectItem> items) {
        do {
            selectStatement.getItems().add(parseSelectItem(selectStatement));
        } while (lexerEngine.skipIfEqual(Symbol.COMMA));
        selectStatement.setSelectListLastPosition(lexerEngine.getCurrentToken().getEndPosition() - lexerEngine.getCurrentToken().getLiterals().length());
        items.addAll(selectStatement.getItems());
    }

private SelectItem parseSelectItem(final SelectStatement selectStatement) {
        lexerEngine.skipIfEqual(getSkippedKeywordsBeforeSelectItem());
        SelectItem result;
        if (isRowNumberSelectItem()) {
           // 是否是 ROW_NUMBER 关键字(SQLServer 才有)
            result = parseRowNumberSelectItem(selectStatement);       
        } else if (isStarSelectItem()) {
           // 是否是全表查询“*”
            selectStatement.setContainStar(true);
            result = parseStarSelectItem();
        } else if (isAggregationSelectItem()) {
           // 聚合函数查询,如 SUM、AVG 等
            result = parseAggregationSelectItem(selectStatement);
            parseRestSelectItem(selectStatement);
        } else {
            // 普通查询
            result = new CommonSelectItem(SQLUtil.getExactlyValue(parseCommonSelectItem(selectStatement) + parseRestSelectItem(selectStatement)), aliasExpressionParser.parseSelectItemAlias());
        }
        return result;
    }

该方法会解析 select 字面量后面的查询选项,并赋值SelectStatement#items

3.4.3 #parseFrom()

解析表以及表连接关系。如 JOIN ON、子查询,解析过程中获得的表名存入AbstractSQLStatement#tables属性中,以及表对应的标识对象TableToken存入AbstractSQLStatement#sqlTokens属性中。
核心代码为TableReferencesClauseParser#parseTableFactor:

protected final void parseTableFactor(final SQLStatement sqlStatement, final boolean isSingleTableOnly) {
        final int beginPosition = lexerEngine.getCurrentToken().getEndPosition() - lexerEngine.getCurrentToken().getLiterals().length();
        String literals = lexerEngine.getCurrentToken().getLiterals();
        lexerEngine.nextToken();
        if (lexerEngine.equalAny(Symbol.DOT)) {
            throw new UnsupportedOperationException("Cannot support SQL for `schema.table`");
        }
        // 获取表名
        String tableName = SQLUtil.getExactlyValue(literals);
        if (Strings.isNullOrEmpty(tableName)) {
            return;
        }
        // 解析别名
        Optional<String> alias = aliasExpressionParser.parseTableAlias();
        if (isSingleTableOnly || shardingRule.tryFindTableRule(tableName).isPresent() || shardingRule.findBindingTableRule(tableName).isPresent()
                || shardingRule.getDataSourceMap().containsKey(shardingRule.getDefaultDataSourceName())) {
            sqlStatement.getSqlTokens().add(new TableToken(beginPosition, literals));
            sqlStatement.getTables().add(new Table(tableName, alias));
        }
        // 解析联表查询
        parseJoinTable(sqlStatement);
        if (isSingleTableOnly && !sqlStatement.getTables().isSingleTable()) {
            throw new UnsupportedOperationException("Cannot support Multiple-Table.");
        }
    }

3.4.4 #parseWhere()

解析 WHERE 条件。将对路由结果有影响的条件,即分片键的过滤条件,存入AbstractSQLStatement#conditions中。
核心代码为WhereClauseParser#parseComparisonCondition

private void parseComparisonCondition(final ShardingRule shardingRule, final SQLStatement sqlStatement, final List<SelectItem> items) {
        lexerEngine.skipIfEqual(Symbol.LEFT_PAREN);
        SQLExpression left = basicExpressionParser.parse(sqlStatement);
        if (lexerEngine.skipIfEqual(Symbol.EQ)) {
            // 解析 = 条件
            parseEqualCondition(shardingRule, sqlStatement, left);
            lexerEngine.skipIfEqual(Symbol.RIGHT_PAREN);
            return;
        }
        if (lexerEngine.skipIfEqual(DefaultKeyword.IN)) {
            // 解析 in 条件
            parseInCondition(shardingRule, sqlStatement, left);
            lexerEngine.skipIfEqual(Symbol.RIGHT_PAREN);
            return;
        }
        if (lexerEngine.skipIfEqual(DefaultKeyword.BETWEEN)) {
            // 解析 Between And 条件,即区间条件
            parseBetweenCondition(shardingRule, sqlStatement, left);
            lexerEngine.skipIfEqual(Symbol.RIGHT_PAREN);
            return;
        }
        if (sqlStatement instanceof SelectStatement && isRowNumberCondition(items, left)) {
            // ROW_NUMBER 的查询语句(MySQL 没有)
            if (lexerEngine.skipIfEqual(Symbol.LT)) {
                parseRowCountCondition((SelectStatement) sqlStatement, false);
                return;
            }
            if (lexerEngine.skipIfEqual(Symbol.LT_EQ)) {
                parseRowCountCondition((SelectStatement) sqlStatement, true);
                return;
            }
            if (lexerEngine.skipIfEqual(Symbol.GT)) {
                parseOffsetCondition((SelectStatement) sqlStatement, false);
                return;
            }
            if (lexerEngine.skipIfEqual(Symbol.GT_EQ)) {
                parseOffsetCondition((SelectStatement) sqlStatement, true);
                return;
            }
        }
        // 其他条件查询,如<,<=,>,>=,!= 等
        List<Keyword> otherConditionOperators = new LinkedList<>(Arrays.asList(getCustomizedOtherConditionOperators()));
        otherConditionOperators.addAll(
                Arrays.asList(Symbol.LT, Symbol.LT_EQ, Symbol.GT, Symbol.GT_EQ, Symbol.LT_GT, Symbol.BANG_EQ, Symbol.BANG_GT, Symbol.BANG_LT, DefaultKeyword.LIKE, DefaultKeyword.IS));
        if (lexerEngine.skipIfEqual(otherConditionOperators.toArray(new Keyword[otherConditionOperators.size()]))) {
            lexerEngine.skipIfEqual(DefaultKeyword.NOT);
            parseOtherCondition(sqlStatement);
        }
        if (lexerEngine.skipIfEqual(DefaultKeyword.NOT)) {
            lexerEngine.nextToken();
            lexerEngine.skipIfEqual(Symbol.LEFT_PAREN);
            parseOtherCondition(sqlStatement);
            lexerEngine.skipIfEqual(Symbol.RIGHT_PAREN);
        }
        lexerEngine.skipIfEqual(Symbol.RIGHT_PAREN);
    }

3.4.5 #parseGroupBy()

解析分组条件,实现上比较类似 #parseSelectList,会更加简单一些。
解析出来的分组信息存入SelectStatement#groupByItems属性中。
核心代码为GroupByClauseParser#parse:

 public final void parse(final SelectStatement selectStatement) {
        if (!lexerEngine.skipIfEqual(DefaultKeyword.GROUP)) {
            return;
        }
        lexerEngine.accept(DefaultKeyword.BY);
        while (true) {
            // 解析分组表达式,得到 OrderItem,并存入 SelectStatement#groupByItems 属性中
            addGroupByItem(basicExpressionParser.parse(selectStatement), selectStatement);
            if (!lexerEngine.equalAny(Symbol.COMMA)) {
                break;
            }
            lexerEngine.nextToken();
        }
        lexerEngine.skipAll(getSkippedKeywordAfterGroupBy());
        selectStatement.setGroupByLastPosition(lexerEngine.getCurrentToken().getEndPosition() - lexerEngine.getCurrentToken().getLiterals().length());
    }

3.4.6 #parseHaving()

目前 Sharding-JDBC 不支持 Having 条件。
核心代码为HavingClauseParser#parse

public void parse() {
        lexerEngine.unsupportedIfEqual(DefaultKeyword.HAVING);
    }
// lexerEngine.java
public void unsupportedIfEqual(final TokenType... tokenTypes) {
        if (equalAny(tokenTypes)) {
            throw new SQLParsingUnsupportedException(lexer.getCurrentToken().getType());
        }
    }

3.4.7 #parseOrderBy()

解析排序条件。实现逻辑类似 #parseGroupBy(),这里就跳过,有兴趣的同学可以去看看。

3.4.8 #parseLimit()

解析分页 Limit 条件。相对简单,这里就跳过,有兴趣的同学可以去看看。注意下,分成 3 种情况:

  • LIMIT row_count
  • LIMIT offset, row_count
  • LIMIT row_count OFFSET offset

解析出来的分页信息存入SelectStatement#limit属性中。

  • Limit
public final class Limit {
    
    // 数据库类型
    private final DatabaseType databaseType;
    // offset
    private LimitValue offset;
    // row
    private LimitValue rowCount;
    
}

当分页位置为非占位符,即为数字时,会生成 OffsetToken 和 RowCountToken。

3.4.9 appendDerived 等方法

因为 Sharding-JDBC 对表做了分片,在 AVG , GROUP BY , ORDER BY 需要对 SQL 进行一些改写,以达到能在内存里对结果做进一步处理,例如求平均值、分组、排序等。

3.4.9.1 #appendAvgDerivedColumns()

解决 AVG 查询。
核心代码为AbstractSelectParser#appendAvgDerivedColumns:

private void appendAvgDerivedColumns(final ItemsToken itemsToken, final SelectStatement selectStatement) {
        int derivedColumnOffset = 0;
        for (SelectItem each : selectStatement.getItems()) {
            if (!(each instanceof AggregationSelectItem) || AggregationType.AVG != ((AggregationSelectItem) each).getType()) {
                continue;
            }
            AggregationSelectItem avgItem = (AggregationSelectItem) each;
            // COUNT 字段
            String countAlias = String.format(DERIVED_COUNT_ALIAS, derivedColumnOffset);
            AggregationSelectItem countItem = new AggregationSelectItem(AggregationType.COUNT, avgItem.getInnerExpression(), Optional.of(countAlias));
            // SUM 字段
            String sumAlias = String.format(DERIVED_SUM_ALIAS, derivedColumnOffset);
            AggregationSelectItem sumItem = new AggregationSelectItem(AggregationType.SUM, avgItem.getInnerExpression(), Optional.of(sumAlias));
            // AggregationSelectItem 设置
            avgItem.getDerivedAggregationSelectItems().add(countItem);
            avgItem.getDerivedAggregationSelectItems().add(sumItem);
            // TODO 将AVG列替换成常数,避免数据库再计算无用的AVG函数
            itemsToken.getItems().add(countItem.getExpression() + " AS " + countAlias + " ");
            itemsToken.getItems().add(sumItem.getExpression() + " AS " + sumAlias + " ");
            derivedColumnOffset++;
        }
    }

针对 AVG 聚合字段,增加推导字段,将 AVG 改写成 SUM 和 COUNT 查询,内存计算出 AVG = SUM / COUNT 结果。

3.4.9.2 #appendDerivedOrderColumns()

解决 GROUP BY , ORDER BY。
核心代码为AbstractSelectParser#appendDerivedOrderColumns:

private void appendDerivedOrderColumns(final ItemsToken itemsToken, final List<OrderItem> orderItems, final String aliasPattern, final SelectStatement selectStatement) {
        int derivedColumnOffset = 0;
        for (OrderItem each : orderItems) {
            if (!isContainsItem(each, selectStatement)) {
                String alias = String.format(aliasPattern, derivedColumnOffset++);
                each.setAlias(Optional.of(alias));
                itemsToken.getItems().add(each.getQualifiedName().get() + " AS " + alias + " ");
            }
        }
    }

private boolean isContainsItem(final OrderItem orderItem, final SelectStatement selectStatement) {
        if (selectStatement.isContainStar()) {
            return true;
        }
        for (SelectItem each : selectStatement.getItems()) {
            if (-1 != orderItem.getIndex()) {
                return true;
            }
            if (each.getAlias().isPresent() && orderItem.getAlias().isPresent() && each.getAlias().get().equalsIgnoreCase(orderItem.getAlias().get())) {
                return true;
            }
            if (!each.getAlias().isPresent() && orderItem.getQualifiedName().isPresent() && each.getExpression().equalsIgnoreCase(orderItem.getQualifiedName().get())) {
                return true;
            }
        }
        return false;
    }

针对 GROUP BY 或 ORDER BY 字段,增加推导字段。

如果该字段不在查询字段里,需要额外查询该字段,这样才能在内存里 GROUP BY 或 ORDER BY。

3.4.9.3 #appendDerivedOrderBy()

当无 Order By 条件时,使用 Group By 作为排序条件。
核心代码为AbstractSelectParser#appendDerivedOrderBy:

private void appendDerivedOrderBy(final SelectStatement selectStatement) {
        if (!selectStatement.getGroupByItems().isEmpty() && selectStatement.getOrderByItems().isEmpty()) {
            selectStatement.getOrderByItems().addAll(selectStatement.getGroupByItems());
            selectStatement.getSqlTokens().add(new OrderByToken(selectStatement.getGroupByLastPosition()));
        }
    }

3.4.10 ItemsToken

选择项标记对象,属于分片上下文信息,目前有 3 个情况会创建:

  1. AVG 查询额外 COUNT 和 SUM: #appendAvgDerivedColumns()
  2. GROUP BY 不在 查询字段,额外查询该字段 : #appendDerivedOrderColumns()
  3. ORDER BY 不在 查询字段,额外查询该字段 : #appendDerivedOrderColumns()
public final class ItemsToken implements SQLToken {
    /**
     * SQL 开始位置
     */
    private final int beginPosition;
    /**
     * 字段名数组
     */
    private final List<String> items = new LinkedList<>();
}

4. 结语

查询语句的 SQL 解析已经讲解完毕,其他的 INSERT,UPDATE,DELETE 就更简单了,感兴趣的同学可以自行去了解。那么,我们拿到 SQL 解析的结果SQLStatement,就可以进行下一步的路由操作了,于是下一篇,我们将讨论 Sharding-JDBC 的路由流程,尽请关注!

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

推荐阅读更多精彩内容