數(shù)據(jù)庫中間件 Sharding-JDBC 源碼分析 —— SQL 路由

1. 概述

本文分享分表分庫路由相關(guān)的實(shí)現(xiàn)辆飘。涉及內(nèi)容如下:

  • SQL 路由器
  • 路由引擎
  • SQL 路由結(jié)果

SQL 路由大體流程如下:


第二個步驟其實(shí)是 SQL 的解析過程份殿,在上一篇<<SQL 解析>>已經(jīng)介紹了。所以具滴,嚴(yán)格來說,SQL 解析過程是在路由階段里的。

2. SQL 路由器

SQLRouter毙籽,SQL 路由器接口,共有兩種實(shí)現(xiàn):

  • DatabaseHintSQLRouter:通過提示且僅路由至數(shù)據(jù)庫的SQL路由器
  • ParsingSQLRouter:需要解析的SQL路由器

它們實(shí)現(xiàn) #parse() 進(jìn)行 SQL 解析毡庆, #route() 進(jìn)行 SQL 路由坑赡。

2.1 DatabaseHintSQLRouter

DatabaseHintSQLRouter,基于數(shù)據(jù)庫提示的路由引擎么抗。路由器工廠 SQLRouterFactory 創(chuàng)建路由器時毅否,判斷到使用數(shù)據(jù)庫提示( Hint ) 時,創(chuàng)建 DatabaseHintSQLRouter蝇刀。

    // DatabaseHintRoutingEngine.java
    public static SQLRouter createSQLRouter(final ShardingContext shardingContext) {
       return HintManagerHolder.isDatabaseShardingOnly() ? new DatabaseHintSQLRouter(shardingContext) : new ParsingSQLRouter(shardingContext);
    }

先來看下 HintManagerHolder螟加、HintManager 部分相關(guān)的代碼:

public final class HintManagerHolder {
    
    public static final String DB_TABLE_NAME = "DB_TABLE_NAME";
    
    public static final String DB_COLUMN_NAME = "DB_COLUMN_NAME";
    
    private static final ThreadLocal<HintManager> HINT_MANAGER_HOLDER = new ThreadLocal<>();
    
    /**
     * 判斷是否當(dāng)前只分庫.
     * 
     * @return database sharding only or not
     */
    public static boolean isDatabaseShardingOnly() {
        return null != HINT_MANAGER_HOLDER.get() && HINT_MANAGER_HOLDER.get().isDatabaseShardingOnly();
    }
    
    /**
     * 清理線索分片管理器的本地線程持有者.
     */
    public static void clear() {
        HINT_MANAGER_HOLDER.remove();
    }

    /**
     * Get hint manager in current thread.
     * 
     * @return hint manager in current thread
     */
    public static HintManager get() {
        return HINT_MANAGER_HOLDER.get();
    }
}

public final class HintManager implements AutoCloseable {
    
    private final Map<ShardingKey, ShardingValue> databaseShardingValues = new HashMap<>();
   
    @Getter
    private boolean databaseShardingOnly;
    
    /**
     * 獲取線索分片管理器實(shí)例.
     *
     * @return  {@code HintManager} instance
     */
    public static HintManager getInstance() {
        HintManager result = new HintManager();
        HintManagerHolder.setHintManager(result);
        return result;
    }
    
    /**
     * 設(shè)置分庫分片值.
     *
     * <p>該方法適用于只分庫的場景</p>
     *
     * @param value sharding value
     */
    public void setDatabaseShardingValue(final Comparable<?> value) {
        databaseShardingOnly = true;
        addDatabaseShardingValue(HintManagerHolder.DB_TABLE_NAME, HintManagerHolder.DB_COLUMN_NAME, value);
    }
    
}

那么如果要使用 DatabaseHintSQLRouter,我們只需要 HintManager.getInstance().setDatabaseShardingValue(庫分片值) 即可吞琐。這里有兩點(diǎn)要注意下:

  • HintManager#getInstance()捆探,每次獲取到的都是新的 HintManager,多次賦值需要小心站粟。
  • HintManager#close()黍图,使用完需要去清理,避免下個請求讀到遺漏的線程變量奴烙。

Hint 方式主要使用場景:

  1. 分片字段不存在SQL助被、數(shù)據(jù)庫表結(jié)構(gòu)中,而存在于外部業(yè)務(wù)邏輯切诀。
  2. 強(qiáng)制在主庫進(jìn)行某些數(shù)據(jù)操作揩环。

2.2 ParsingSQLRouter

在我們平常的業(yè)務(wù)場景中,使用最多的是分片字段會在 SQL幅虑、數(shù)據(jù)庫表結(jié)構(gòu)中丰滑,其將采用 ParsingSQLRouter 進(jìn)行 SQL 的解析和路由。

ParsingSQLRouter 使用 SQLParsingEngine 解析SQL(上篇文章已經(jīng)介紹)翘单。

ParsingSQLRouter 在路由時吨枉,會根據(jù)表情況使用 SimpleRoutingEngine 或 CartesianRoutingEngine 進(jìn)行路由ParsingSQLRouter#route(parameters, sqlStatement):

private RoutingResult route(final List<Object> parameters, final SQLStatement sqlStatement) {
        Collection<String> tableNames = sqlStatement.getTables().getTableNames();
        RoutingEngine routingEngine;
        if (sqlStatement instanceof DDLStatement) {
            // DDL 表定義語言蹦渣,如 CREATE、ALTER 等操作
            routingEngine = new DDLRoutingEngine(shardingRule, parameters, (DDLStatement) sqlStatement); 
        } else if (tableNames.isEmpty()) {
            routingEngine = new DatabaseAllRoutingEngine(shardingRule.getDataSourceMap());
        } else if (1 == tableNames.size() || shardingRule.isAllBindingTables(tableNames) || shardingRule.isAllInDefaultDataSource(tableNames)) {
            // 簡單路由引擎
            routingEngine = new SimpleRoutingEngine(shardingRule, parameters, tableNames.iterator().next(), sqlStatement);
        } else {
            // 混合路由引擎
            routingEngine = new ComplexRoutingEngine(shardingRule, parameters, tableNames, sqlStatement);
        }
        return routingEngine.route();
    }
  1. 當(dāng)是 DDL 語句時貌亭,采用DDLRoutingEngine進(jìn)行路由柬唯。
  2. 當(dāng)只有一個表名或者多表互為 BindingTable 關(guān)系時,就會采用SimpleRoutingEngine進(jìn)行路由圃庭。
  3. 其他情況(多庫多表情況)采用混合路由引擎ComplexRoutingEngine锄奢。

BindingTable 關(guān)系在 ShardingRule 的 tableRules 配置。配置該關(guān)系 TableRule 有如下需要遵守的規(guī)則:

  • 分片策略與算法相同
  • 數(shù)據(jù)源配置對象相同
  • 真實(shí)表數(shù)量相同

2.3 SimpleRoutingEngine

SimpleRoutingEngine剧腻,簡單路由引擎。


// SimpleRoutingEngine.java
public RoutingResult route() {
        // 1. 獲取分表規(guī)則
        TableRule tableRule = shardingRule.getTableRule(logicTableName);
        // 2. 獲取分庫值
        List<ShardingValue> databaseShardingValues = getDatabaseShardingValues(tableRule);
        // 3. 獲取分表值
        List<ShardingValue> tableShardingValues = getTableShardingValues(tableRule);
        // 4. 路由數(shù)據(jù)庫
        Collection<String> routedDataSources = routeDataSources(tableRule, databaseShardingValues);
        Collection<DataNode> routedDataNodes = new LinkedList<>();
        for (String each : routedDataSources) {
            // 5. 路由表
            routedDataNodes.addAll(routeTables(tableRule, each, tableShardingValues));
        }
        // 6. 生成路由結(jié)果 RoutingResult
        return generateRoutingResult(routedDataNodes);
    }

第一步书在,根據(jù) SQL 的邏輯表 logicTableName 獲取分表規(guī)則(客戶端配置的分片規(guī)則)tableRule栏账。我們看 TableRule 的組成:

public final class TableRule {
    
    // 邏輯表
    private final String logicTable;
    // 節(jié)點(diǎn)集合
    private final List<DataNode> actualDataNodes;
    // 數(shù)據(jù)庫分片策略
    private final ShardingStrategy databaseShardingStrategy;
    // 表分片策略
    private final ShardingStrategy tableShardingStrategy;
    // 自增主鍵字段
    private final String generateKeyColumn;
    // 自增器,默認(rèn)的自增器采用 snowflake
    private final KeyGenerator keyGenerator;
    
    private final String logicIndex;

}

第二步栈源,根據(jù) tableRule 獲取分庫值getDatabaseShardingValues(tableRule)

// SimpleRoutingEngine.java
private List<ShardingValue> getDatabaseShardingValues(final TableRule tableRule) {
        ShardingStrategy strategy = shardingRule.getDatabaseShardingStrategy(tableRule);
        return HintManagerHolder.isUseShardingHint() ? getDatabaseShardingValuesFromHint(strategy.getShardingColumns()) : getShardingValues(strategy.getShardingColumns());
    }

private List<ShardingValue> getShardingValues(final Collection<String> shardingColumns) {
        List<ShardingValue> result = new ArrayList<>(shardingColumns.size());
        for (String each : shardingColumns) {
            Optional<Condition> condition = sqlStatement.getConditions().find(new Column(each, logicTableName));
            if (condition.isPresent()) {
                result.add(condition.get().getShardingValue(parameters));
            }
        }
        return result;
    }

// ShardingRule.java
 public ShardingStrategy getDatabaseShardingStrategy(final TableRule tableRule) {
        return null == tableRule.getDatabaseShardingStrategy() ? defaultDatabaseShardingStrategy : tableRule.getDatabaseShardingStrategy();
    }

該方法會從TableRule獲取分庫策略挡爵,如果為空,則使用默認(rèn)的分庫策略(需要客戶端配置)甚垦,如果未配置默認(rèn)的分庫策略茶鹃,則使用NoneShardingStrategy,標(biāo)明不使用任何分片策略艰亮。

關(guān)于 ShardingStrategy 的介紹闭翩,可以參考我的另一篇文章《分庫分表中間件 Sharding-JDBC》
其中兩個比較重要的分片策略是 StandardShardingStrategy 和 ComplexShardingStrategy垃杖。前者針對是單個分片鍵男杈,后者針對的是多個分片鍵丈屹。

拿到 ShardingStrategy 之后调俘,繼續(xù)判斷,如果采用的時 Hint 分片方式旺垒,則從 Hint 中獲取分庫值彩库;如果非 Hint 方式,則根據(jù)分片鍵和邏輯表找到對應(yīng)的Condition先蒋,我們看到了《SQL解析》分享的Condition對象骇钦。之前我們提到過 Parser 半理解 SQL 的目的之一是:提煉分片上下文,此處即是該目的的體現(xiàn)竞漾。

接著眯搭,根據(jù)Condition的 ShardingOperator 屬性窥翩,來創(chuàng)建不同的ShardingValue:

// Condition.java
public ShardingValue getShardingValue(final List<Object> parameters) {
        List<Comparable<?>> conditionValues = getValues(parameters);
        switch (operator) {
            case EQUAL:
            case IN:
                return new ListShardingValue<>(column.getTableName(), column.getName(), conditionValues);
            case BETWEEN:
                return new RangeShardingValue<>(column.getTableName(), column.getName(), Range.range(conditionValues.get(0), BoundType.CLOSED, conditionValues.get(1), BoundType.CLOSED));
            default:
                throw new UnsupportedOperationException(operator.getExpression());
        }
    }

分片操作為 = 或者 in 時,創(chuàng)建ListShardingValue對象鳞仙。
分片操作為 Between 時寇蚊,創(chuàng)建RangeShardingValue對象。

  • ListShardingValue
public final class ListShardingValue<T extends Comparable<?>> implements ShardingValue {
    // 邏輯表
    private final String logicTableName;
    // 分片字段
    private final String columnName;
    // 分片字段的值棍好,集合
    private final Collection<T> values;
}
  • RangeShardingValue
public final class RangeShardingValue<T extends Comparable<?>> implements ShardingValue {
    
    private final String logicTableName;
    
    private final String columnName;
    // 分片字段的值仗岸,區(qū)間
    private final Range<T> valueRange;
}

第三步,獲取表分片值借笙。跟第二步是類似的扒怖,這里就不再贅述了。
第四步业稼,根據(jù)數(shù)據(jù)庫分片值和分片規(guī)則盗痒,進(jìn)行數(shù)據(jù)庫路由操作routeDataSources:

private Collection<String> routeDataSources(final TableRule tableRule, final List<ShardingValue> databaseShardingValues) {
        // 獲取實(shí)際節(jié)點(diǎn)的數(shù)據(jù)庫集合
        Collection<String> availableTargetDatabases = tableRule.getActualDatasourceNames();
        if (databaseShardingValues.isEmpty()) {
            return availableTargetDatabases;
        }
        // 根據(jù)數(shù)據(jù)庫分片值進(jìn)行分片操作
        Collection<String> result = shardingRule.getDatabaseShardingStrategy(tableRule).doSharding(availableTargetDatabases, databaseShardingValues);
        Preconditions.checkState(!result.isEmpty(), "no database route info");
        return result;
    }

主要的路由操作是在ShardingStrategy#doSharding方法,這里我們以單個分片鍵為例低散,其實(shí)現(xiàn)類為StandardShardingStrategy:

public final class StandardShardingStrategy implements ShardingStrategy {
    // 分片字段
    private final String shardingColumn;
    // 精確分片算法
    private final PreciseShardingAlgorithm preciseShardingAlgorithm;
    // 區(qū)間分片算法
    private final Optional<RangeShardingAlgorithm> rangeShardingAlgorithm;
    
    public StandardShardingStrategy(final String shardingColumn, final PreciseShardingAlgorithm preciseShardingAlgorithm) {
        this(shardingColumn, preciseShardingAlgorithm, null);
    }
    
    public StandardShardingStrategy(final String shardingColumn, final PreciseShardingAlgorithm preciseShardingAlgorithm, final RangeShardingAlgorithm rangeShardingAlgorithm) {
        this.shardingColumn = shardingColumn;
        this.preciseShardingAlgorithm = preciseShardingAlgorithm;
        this.rangeShardingAlgorithm = Optional.fromNullable(rangeShardingAlgorithm);
    }
    
    @Override
    public Collection<String> doSharding(final Collection<String> availableTargetNames, final Collection<ShardingValue> shardingValues) {
        // 獲取分片值(就一個)
        ShardingValue shardingValue = shardingValues.iterator().next();
        // 分片操作
        Collection<String> shardingResult = shardingValue instanceof ListShardingValue
                ? doSharding(availableTargetNames, (ListShardingValue) shardingValue) : doSharding(availableTargetNames, (RangeShardingValue) shardingValue);
        Collection<String> result = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
        result.addAll(shardingResult);
        return result;
    }

    private Collection<String> doSharding(final Collection<String> availableTargetNames, final ListShardingValue<?> shardingValue) {
        Collection<String> result = new LinkedList<>();
        for (PreciseShardingValue<?> each : transferToPreciseShardingValues(shardingValue)) {
            // 調(diào)用分片算法(精確/區(qū)間)的分片操作
            result.add(preciseShardingAlgorithm.doSharding(availableTargetNames, each));
        }
        return result;
    }
    ...
}

StandardShardingStrategy 分片的時候积糯,是調(diào)用其PreciseShardingAlgorithm 或者 RangeShardingAlgorithm 對象的 doSharding方法進(jìn)行分片的。而我們客戶端配置分片算法的時候谦纱,就是實(shí)現(xiàn)了以上算法接口的看成。

第五步,遍歷數(shù)據(jù)庫路由結(jié)果跨嘉,對每一個數(shù)據(jù)庫進(jìn)行表的分片路由routeTables:

// SimpleRoutingEngine.java
private Collection<DataNode> routeTables(final TableRule tableRule, final String routedDataSource, final List<ShardingValue> tableShardingValues) {
        Collection<String> availableTargetTables = tableRule.getActualTableNames(routedDataSource);
        Collection<String> routedTables = tableShardingValues.isEmpty() ? availableTargetTables
                : shardingRule.getTableShardingStrategy(tableRule).doSharding(availableTargetTables, tableShardingValues);
        Preconditions.checkState(!routedTables.isEmpty(), "no table route info");
        Collection<DataNode> result = new LinkedList<>();
        for (String each : routedTables) {
            result.add(new DataNode(routedDataSource, each));
        }
        return result;
    }

表的分片和數(shù)據(jù)庫的分片的邏輯是一樣的川慌,分片完成之后,將每個分片結(jié)果封裝在DataNode對象中:

public class DataNode {
    
    private static final String DELIMITER = ".";
    // 數(shù)據(jù)庫
    private final String dataSourceName;
    // 表
    private final String tableName;

}

第六步祠乃,將 DataNode 集合封裝成路由結(jié)果RoutingResult:

private RoutingResult generateRoutingResult(final Collection<DataNode> routedDataNodes) {
        RoutingResult result = new RoutingResult();
        for (DataNode each : routedDataNodes) {
            result.getTableUnits().getTableUnits().add(new TableUnit(each.getDataSourceName(), logicTableName, each.getTableName()));
        }
        return result;
    }

RoutingResult 存放了TableUnits梦重,其是TableUnit的集合對象。該方法將分片出來的數(shù)據(jù)庫信息和表信息存入在TableUnit中亮瓷,供后續(xù)改寫 SQL 用琴拧。

2.4 ComplexRoutingEngine

ComplexRoutingEngine,混合多庫多表路由引擎嘱支。

public final class ComplexRoutingEngine implements RoutingEngine {
    
    private final ShardingRule shardingRule;
    
    private final List<Object> parameters;
    
    private final Collection<String> logicTables;
    
    private final SQLStatement sqlStatement;
    
    @Override
    public RoutingResult route() {
        Collection<RoutingResult> result = new ArrayList<>(logicTables.size());
        Collection<String> bindingTableNames = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
        // 遍歷邏輯表集合
        for (String each : logicTables) {
            Optional<TableRule> tableRule = shardingRule.tryFindTableRule(each);
            if (tableRule.isPresent()) {
                if (!bindingTableNames.contains(each)) {
                    // 有 BindingTable 關(guān)系的蚓胸,走普通路由引擎
                    result.add(new SimpleRoutingEngine(shardingRule, parameters, tableRule.get().getLogicTable(), sqlStatement).route());
                }
                Optional<BindingTableRule> bindingTableRule = shardingRule.findBindingTableRule(each);
                if (bindingTableRule.isPresent()) {
                    bindingTableNames.addAll(Lists.transform(bindingTableRule.get().getTableRules(), new Function<TableRule, String>() {
                        
                        @Override
                        public String apply(final TableRule input) {
                            return input.getLogicTable();
                        }
                    }));
                }
            }
        }
        log.trace("mixed tables sharding result: {}", result);
        if (result.isEmpty()) {
            throw new ShardingJdbcException("Cannot find table rule and default data source with logic tables: '%s'", logicTables);
        }
        if (1 == result.size()) {
            return result.iterator().next();
        }
        // 如果有多個路由結(jié)果,則走笛卡爾積的路由引擎
        return new CartesianRoutingEngine(result).route();
    }
}
  • ComplexRoutingEngine 計算每個邏輯表的簡單路由分片除师,路由結(jié)果交給 CartesianRoutingEngine 繼續(xù)路由形成笛卡爾積結(jié)果沛膳。


  • 由于在 ComplexRoutingEngine 路由前已經(jīng)判斷全部表互為 BindingTable 關(guān)系,因而不會出現(xiàn) result.size == 1汛聚,屬于防御性編程锹安。

  • 部分表互為 BindingTable 關(guān)系時,ComplexRoutingEngine 不重復(fù)計算分片。

2.5 CartesianRoutingEngine

CartesianRoutingEngine叹哭,笛卡爾積的庫表路由忍宋。

public CartesianRoutingResult route() {
        CartesianRoutingResult result = new CartesianRoutingResult();
        // 根據(jù)路由結(jié)果獲取 dataSourceLogicTablesMap:key 為 數(shù)據(jù)庫名,value 為表名集合
        for (Entry<String, Set<String>> entry : getDataSourceLogicTablesMap().entrySet()) {
            // 獲得當(dāng)前數(shù)據(jù)源(庫)的實(shí)際表分組
            List<Set<String>> actualTableGroups = getActualTableGroups(entry.getKey(), entry.getValue());
            // 獲得當(dāng)前數(shù)據(jù)源(庫)的路由表單元分組
            List<Set<TableUnit>> tableUnitGroups = toTableUnitGroups(entry.getKey(), actualTableGroups);
            // 對路由表單元分組進(jìn)行笛卡爾積风罩,并合并到路由結(jié)果
            result.merge(entry.getKey(), getCartesianTableReferences(Sets.cartesianProduct(tableUnitGroups)));
        }
        log.trace("cartesian tables sharding result: {}", result);
        return result;
    }
  • 第一步讶踪,獲得同庫對應(yīng)的邏輯表集合,即 Entry<數(shù)據(jù)源(庫), Set<邏輯表>> entry泊交。

  • 第二步乳讥,遍歷數(shù)據(jù)源(庫),獲得當(dāng)前數(shù)據(jù)源(庫)的路由表單元分組廓俭。

  • 第三步云石,對路由表單元分組進(jìn)行笛卡爾積,并合并到路由結(jié)果研乒。

注意:同庫才可以進(jìn)行笛卡爾積汹忠。

3. 結(jié)語

由于篇幅關(guān)系,本文并未對笛卡爾積的路由展開說雹熬,感興趣的同學(xué)可以自行去了解宽菜。

經(jīng)過路由之后,得到的RoutingResult對象竿报,會在下一篇文章 SQL 改寫中用到它铅乡,盡請關(guān)注~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市烈菌,隨后出現(xiàn)的幾起案子阵幸,更是在濱河造成了極大的恐慌,老刑警劉巖芽世,帶你破解...
    沈念sama閱讀 218,525評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件挚赊,死亡現(xiàn)場離奇詭異,居然都是意外死亡济瓢,警方通過查閱死者的電腦和手機(jī)荠割,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,203評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來旺矾,“玉大人蔑鹦,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 164,862評論 0 354
  • 文/不壞的土叔 我叫張陵碗啄,是天一觀的道長淑履。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么雕崩? 我笑而不...
    開封第一講書人閱讀 58,728評論 1 294
  • 正文 為了忘掉前任魁索,我火速辦了婚禮,結(jié)果婚禮上盼铁,老公的妹妹穿的比我還像新娘粗蔚。我一直安慰自己,他們只是感情好饶火,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,743評論 6 392
  • 文/花漫 我一把揭開白布鹏控。 她就那樣靜靜地躺著,像睡著了一般肤寝。 火紅的嫁衣襯著肌膚如雪当辐。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,590評論 1 305
  • 那天鲤看,我揣著相機(jī)與錄音缘揪,去河邊找鬼。 笑死义桂,一個胖子當(dāng)著我的面吹牛找筝,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播慷吊,決...
    沈念sama閱讀 40,330評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼袖裕,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了溉瓶?” 一聲冷哼從身側(cè)響起陆赋,我...
    開封第一講書人閱讀 39,244評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎嚷闭,沒想到半個月后攒岛,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,693評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡胞锰,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,885評論 3 336
  • 正文 我和宋清朗相戀三年灾锯,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片嗅榕。...
    茶點(diǎn)故事閱讀 40,001評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡顺饮,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出凌那,到底是詐尸還是另有隱情兼雄,我是刑警寧澤,帶...
    沈念sama閱讀 35,723評論 5 346
  • 正文 年R本政府宣布帽蝶,位于F島的核電站赦肋,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜佃乘,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,343評論 3 330
  • 文/蒙蒙 一囱井、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧趣避,春花似錦庞呕、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,919評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至愁拭,卻和暖如春讲逛,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背敛苇。 一陣腳步聲響...
    開封第一講書人閱讀 33,042評論 1 270
  • 我被黑心中介騙來泰國打工妆绞, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人枫攀。 一個月前我還...
    沈念sama閱讀 48,191評論 3 370
  • 正文 我出身青樓括饶,卻偏偏與公主長得像,于是被迫代替她去往敵國和親来涨。 傳聞我的和親對象是個殘疾皇子图焰,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,955評論 2 355

推薦閱讀更多精彩內(nèi)容