分庫分表【Sharding-JDBC】入門與項目實戰(zhàn)

最近項目中不少表的數(shù)據(jù)量越來越大,并且導致了一些數(shù)據(jù)庫的性能問題婴栽。因此想借助一些分庫分表的中間件,實現(xiàn)自動化分庫分表實現(xiàn)辈末。調研下來愚争,發(fā)現(xiàn)Sharding-JDBC目前成熟度最高并且應用最廣的Java分庫分表的客戶端組件。本文主要介紹一些Sharding-JDBC核心概念以及生產環(huán)境下的實戰(zhàn)指南挤聘,旨在幫助組內成員快速了解Sharding-JDBC并且能夠快速將其使用起來轰枝。Sharding-JDBC官方文檔

核心概念

在使用Sharding-JDBC之前,一定是先理解清楚下面幾個核心概念檬洞。

邏輯表

水平拆分的數(shù)據(jù)庫(表)的相同邏輯和數(shù)據(jù)結構表的總稱狸膏。例:訂單數(shù)據(jù)根據(jù)主鍵尾數(shù)拆分為10張表,分別是t_order_0t_order_9添怔,他們的邏輯表名為t_order湾戳。

真實表

在分片的數(shù)據(jù)庫中真實存在的物理表。即上個示例中的t_order_0t_order_9广料。

數(shù)據(jù)節(jié)點

數(shù)據(jù)分片的最小單元砾脑。由數(shù)據(jù)源名稱和數(shù)據(jù)表組成,例:ds_0.t_order_0艾杏。

綁定表

指分片規(guī)則一致的主表和子表韧衣。例如:t_order表和t_order_item表,均按照order_id分片购桑,則此兩張表互為綁定表關系畅铭。綁定表之間的多表關聯(lián)查詢不會出現(xiàn)笛卡爾積關聯(lián),關聯(lián)查詢效率將大大提升勃蜘。舉例說明,如果SQL為:

SELECT i.* FROM t_order o JOIN t_order_item i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);

假設t_ordert_order_item對應的真實表各有2個硕噩,那么真實表就有t_order_0t_order_1缭贡、t_order_item_0炉擅、t_order_item_1辉懒。在不配置綁定表關系時,假設分片鍵order_id將數(shù)值10路由至第0片谍失,將數(shù)值11路由至第1片眶俩,那么路由后的SQL應該為4條,它們呈現(xiàn)為笛卡爾積:

SELECT i.* FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
SELECT i.* FROM t_order_0 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
SELECT i.* FROM t_order_1 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
SELECT i.* FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);

在配置綁定表關系后快鱼,路由的SQL應該為2條:

SELECT i.* FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
SELECT i.* FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);

廣播表

指所有的分片數(shù)據(jù)源中都存在的表颠印,表結構和表中的數(shù)據(jù)在每個數(shù)據(jù)庫中均完全一致喇嘱。適用于數(shù)據(jù)量不大且需要與海量數(shù)據(jù)的表進行關聯(lián)查詢的場景捞烟,例如:字典表。

數(shù)據(jù)分片

分片鍵

用于分片的數(shù)據(jù)庫字段,是將數(shù)據(jù)庫(表)水平拆分的關鍵字段柒莉。例:將訂單表中的訂單主鍵的尾數(shù)取模分片,則訂單主鍵為分片字段沽翔。 SQL 中如果無分片字段兢孝,將執(zhí)行全路由,性能較差仅偎。 除了對單分片字段的支持跨蟹,Sharding-JDBC 也支持根據(jù)多個字段進行分片。

分片算法

通過分片算法將數(shù)據(jù)分片橘沥,支持通過=窗轩、>=、<=座咆、>痢艺、<、BETWEEN和IN分片介陶。 分片算法需要應用方開發(fā)者自行實現(xiàn)堤舒,可實現(xiàn)的靈活度非常高。

目前提供4種分片算法哺呜。 由于分片算法和業(yè)務實現(xiàn)緊密相關舌缤,因此并未提供內置分片算法,而是通過分片策略將各種場景提煉出來某残,提供更高層級的抽象国撵,并提供接口讓應用開發(fā)者自行實現(xiàn)分片算法。

精確分片算法

對應 PreciseShardingAlgorithm玻墅,用于處理使用單一鍵作為分片鍵的 = 與 IN 進行分片的場景介牙。需要配合 StandardShardingStrategy 使用。

范圍分片算法

對應 RangeShardingAlgorithm椭豫,用于處理使用單一鍵作為分片鍵的 BETWEEN AND耻瑟、>旨指、<、>=喳整、<=進行分片的場景谆构。需要配合 StandardShardingStrategy 使用。

復合分片算法

對應 ComplexKeysShardingAlgorithm框都,用于處理使用多鍵作為分片鍵進行分片的場景搬素,包含多個分片鍵的邏輯較復雜,需要應用開發(fā)者自行處理其中的復雜度魏保。需要配合 ComplexShardingStrategy 使用熬尺。

Hint分片算法

對應 HintShardingAlgorithm用于處理通過Hint指定分片值而非從SQL中提取分片值的場景谓罗。需要配合 HintShardingStrategy 使用粱哼。

分片策略

包含分片鍵和分片算法,由于分片算法的獨立性檩咱,將其獨立抽離揭措。真正可用于分片操作的是分片鍵 + 分片算法,也就是分片策略刻蚯。目前提供 5 種分片策略绊含。

標準分片策略

對應 StandardShardingStrategy。提供對 SQ L語句中的 =, >, <, >=, <=, IN 和 BETWEEN AND 的分片操作支持炊汹。 StandardShardingStrategy 只支持單分片鍵躬充,提供 PreciseShardingAlgorithmRangeShardingAlgorithm 兩個分片算法。 PreciseShardingAlgorithm 是必選的讨便,用于處理 = 和 IN 的分片充甚。 RangeShardingAlgorithm 是可選的,用于處理 BETWEEN AND, >, <, >=, <=分片器钟,如果不配置 RangeShardingAlgorithm津坑,SQL 中的 BETWEEN AND 將按照全庫路由處理。

復合分片策略

對應 ComplexShardingStrategy傲霸。復合分片策略疆瑰。提供對 SQL 語句中的 =, >, <, >=, <=, IN 和 BETWEEN AND 的分片操作支持。 ComplexShardingStrategy 支持多分片鍵昙啄,由于多分片鍵之間的關系復雜穆役,因此并未進行過多的封裝,而是直接將分片鍵值組合以及分片操作符透傳至分片算法梳凛,完全由應用開發(fā)者實現(xiàn)耿币,提供最大的靈活度。

行表達式分片策略

對應 InlineShardingStrategy韧拒。使用 Groovy 的表達式淹接,提供對 SQL 語句中的 = 和 IN的分片操作支持十性,只支持單分片鍵。 對于簡單的分片算法塑悼,可以通過簡單的配置使用劲适,從而避免繁瑣的Java代碼開發(fā),如: t_user_$->{u_id % 8} 表示 t_user 表根據(jù) u_id 模 8厢蒜,而分成 8 張表霞势,表名稱為 t_user_0t_user_7可以認為是精確分片算法的簡易實現(xiàn)

Hint分片策略

對應 HintShardingStrategy斑鸦。通過 Hint 指定分片值而非從 SQL 中提取分片值的方式進行分片的策略愕贡。

分布式主鍵

用于在分布式環(huán)境下,生成全局唯一的id巷屿。Sharding-JDBC 提供了內置的分布式主鍵生成器固以,例如 UUIDSNOWFLAKE攒庵。還抽離出分布式主鍵生成器的接口嘴纺,方便用戶自行實現(xiàn)自定義的自增主鍵生成器败晴。為了保證數(shù)據(jù)庫性能浓冒,主鍵id還必須趨勢遞增,避免造成頻繁的數(shù)據(jù)頁面分裂尖坤。

讀寫分離

提供一主多從的讀寫分離配置稳懒,可獨立使用,也可配合分庫分表使用慢味。

  • 同一線程且同一數(shù)據(jù)庫連接內场梆,如有寫入操作,以后的讀操作均從主庫讀取纯路,用于保證數(shù)據(jù)一致性
  • 基于Hint的強制主庫路由或油。
  • 主從模型中,事務中讀寫均用主庫驰唬。

執(zhí)行流程

Sharding-JDBC 的原理總結起來很簡單: 核心由 SQL解析 => 執(zhí)行器優(yōu)化 => SQL路由 => SQL改寫 => SQL執(zhí)行 => 結果歸并的流程組成顶岸。

Sharding-JDBC執(zhí)行流程

項目實戰(zhàn)

spring-boot項目實戰(zhàn)

引入依賴

<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
    <version>4.0.1</version>
</dependency>

數(shù)據(jù)源配置

如果使用sharding-jdbc-spring-boot-starter, 并且數(shù)據(jù)源以及數(shù)據(jù)分片都使用shardingsphere進行配置,對應的數(shù)據(jù)源會自動創(chuàng)建并注入到spring容器中叫编。

spring.shardingsphere.datasource.names=ds0,ds1

spring.shardingsphere.datasource.ds0.type=org.apache.commons.dbcp.BasicDataSource
spring.shardingsphere.datasource.ds0.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds0.url=jdbc:mysql://localhost:3306/ds0
spring.shardingsphere.datasource.ds0.username=root
spring.shardingsphere.datasource.ds0.password=

spring.shardingsphere.datasource.ds1.type=org.apache.commons.dbcp.BasicDataSource
spring.shardingsphere.datasource.ds1.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds1.url=jdbc:mysql://localhost:3306/ds1
spring.shardingsphere.datasource.ds1.username=root
spring.shardingsphere.datasource.ds1.password=

# 其它分片配置

但是在我們已有的項目中辖佣,數(shù)據(jù)源配置是單獨的。因此要禁用sharding-jdbc-spring-boot-starter里面的自動裝配搓逾,而是參考源碼自己重寫數(shù)據(jù)源配置卷谈。需要在啟動類上加上@SpringBootApplication(exclude = {org.apache.shardingsphere.shardingjdbc.spring.boot.SpringBootConfiguration.class})來排除。然后自定義配置類來裝配DataSource霞篡。

@Configuration
@Slf4j
@EnableConfigurationProperties({
        SpringBootShardingRuleConfigurationProperties.class,
        SpringBootMasterSlaveRuleConfigurationProperties.class, SpringBootEncryptRuleConfigurationProperties.class, SpringBootPropertiesConfigurationProperties.class})
@AutoConfigureBefore(DataSourceConfiguration.class)
public class DataSourceConfig implements ApplicationContextAware {

    @Autowired
    private SpringBootShardingRuleConfigurationProperties shardingRule;

    @Autowired
    private SpringBootPropertiesConfigurationProperties props;

    private ApplicationContext applicationContext;

    @Bean("shardingDataSource")
    @Conditional(ShardingRuleCondition.class)
    public DataSource shardingDataSource() throws SQLException {
        // 獲取其它方式配置的數(shù)據(jù)源
        Map<String, DruidDataSourceWrapper> beans = applicationContext.getBeansOfType(DruidDataSourceWrapper.class);
        Map<String, DataSource> dataSourceMap = new HashMap<>(4);
        beans.forEach(dataSourceMap::put);
        // 創(chuàng)建shardingDataSource
        return ShardingDataSourceFactory.createDataSource(dataSourceMap, new ShardingRuleConfigurationYamlSwapper().swap(shardingRule), props.getProps());
    }

    @Bean
    public SqlSessionFactory sqlSessionFactory() throws SQLException {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        // 將shardingDataSource設置到SqlSessionFactory中
        sqlSessionFactoryBean.setDataSource(shardingDataSource());
        // 其它設置
        return sqlSessionFactoryBean.getObject();
    }
}

分布式id生成器配置

Sharding-JDBC提供了UUID世蔗、SNOWFLAKE生成器端逼,還支持用戶實現(xiàn)自定義id生成器。比如可以實現(xiàn)了type為SEQ的分布式id生成器污淋,調用統(tǒng)一的分布式id服務獲取id裳食。

@Data
public class SeqShardingKeyGenerator implements ShardingKeyGenerator {

    private Properties properties = new Properties();

    @Override
    public String getType() {
        return "SEQ";
    }

    @Override
    public synchronized Comparable<?> generateKey() {
       // 獲取分布式id邏輯
    }
}

由于擴展ShardingKeyGenerator是通過JDK的serviceloader的SPI機制實現(xiàn)的,因此還需要在resources/META-INF/services目錄下配置org.apache.shardingsphere.spi.keygen.ShardingKeyGenerator文件芙沥。 文件內容就是SeqShardingKeyGenerator類的全路徑名诲祸。這樣使用的時候,指定分布式主鍵生成器的type為SEQ就好了而昨。

至此救氯,Sharding-JDBC就整合進spring-boot項目中了,后面就可以進行數(shù)據(jù)分片相關的配置了歌憨。

數(shù)據(jù)分片實戰(zhàn)

如果項目初期就能預估出表的數(shù)據(jù)量級着憨,當然可以一開始就按照這個預估值進行分庫分表處理。但是大多數(shù)情況下务嫡,我們一開始并不能準備預估出數(shù)量級甲抖。這時候通常的做法是:

  1. 線上數(shù)據(jù)某張表查詢性能開始下降,排查下來是因為數(shù)據(jù)量過大導致的心铃。
  2. 根據(jù)歷史數(shù)據(jù)量預估出未來的數(shù)據(jù)量級准谚,并結合具體業(yè)務場景確定分庫分表策略。
  3. 自動分庫分表代碼實現(xiàn)去扣。

下面就以一個具體事例柱衔,闡述具體數(shù)據(jù)分片實戰(zhàn)。比如有張表數(shù)據(jù)結構如下:

CREATE TABLE `hc_question_reply_record` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增ID',
  `reply_text` varchar(500) NOT NULL DEFAULT '' COMMENT '回復內容',
  `reply_wheel_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '回復時間',

  `ctime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創(chuàng)建時間',
  `mtime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
  PRIMARY KEY (`id`),
  INDEX `idx_reply_wheel_time` (`reply_wheel_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
  COMMENT='回復明細記錄';

分片方案確定

先查詢目前目標表月新增趨勢:

SELECT count(*), date_format(ctime, '%Y-%m') AS `日期`
FROM hc_question_reply_record
GROUP BY date_format(ctime, '%Y-%m');
month-increse.png

目前月新增在180w左右愉棱,預估未來達到300w(基本以2倍計算)以上唆铐。期望單表數(shù)據(jù)量不超過1000w,可使用reply_wheel_time作為分片鍵按季度歸檔奔滑。

分片配置

spring:
  # sharing-jdbc配置
  shardingsphere:
    # 數(shù)據(jù)源名稱
    datasource:
      names: defaultDataSource,slaveDataSource
    sharding:
      # 主從節(jié)點配置
      master-slave-rules:
        defaultDataSource:
          # maser數(shù)據(jù)源
          master-data-source-name: defaultDataSource
          # slave數(shù)據(jù)源
          slave-data-source-names: slaveDataSource
      tables:
        # hc_question_reply_record 分庫分表配置
        hc_question_reply_record:
          # 真實數(shù)據(jù)節(jié)點  hc_question_reply_record_2020_q1
          actual-data-nodes: defaultDataSource.hc_question_reply_record_$->{2020..2025}_q$->{1..4}
          # 表分片策略
          table-strategy:
            standard:
              # 分片鍵
              sharding-column: reply_wheel_time
              # 精確分片算法 全路徑名
              preciseAlgorithmClassName: com.xx.QuestionRecordPreciseShardingAlgorithm
              # 范圍分片算法艾岂,用于BETWEEN,可選朋其。王浴。該類需實現(xiàn)RangeShardingAlgorithm接口并提供無參數(shù)的構造器
              rangeAlgorithmClassName: com.xx.QuestionRecordRangeShardingAlgorithm

      # 默認分布式id生成器
      default-key-generator:
        type: SEQ
        column: id

分片算法實現(xiàn)

  • 精確分片算法:QuestionRecordPreciseShardingAlgorithm

    public class QuestionRecordPreciseShardingAlgorithm implements PreciseShardingAlgorithm<Date> {
      /**
       * Sharding.
       *
       * @param availableTargetNames available data sources or tables's names
       * @param shardingValue        sharding value
       * @return sharding result for data source or table's name
       */
      @Override
      public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Date> shardingValue) {
          return ShardingUtils.quarterPreciseSharding(availableTargetNames, shardingValue);
      }
    }
    
  • 范圍分片算法:QuestionRecordRangeShardingAlgorithm

    public class QuestionRecordRangeShardingAlgorithm implements RangeShardingAlgorithm<Date> {
    
    
      /**
       * Sharding.
       *
       * @param availableTargetNames available data sources or tables's names
       * @param shardingValue        sharding value
       * @return sharding results for data sources or tables's names
       */
      @Override
      public Collection<String> doSharding(Collection<String> availableTargetNames, RangeShardingValue<Date> shardingValue) {
          return ShardingUtils.quarterRangeSharding(availableTargetNames, shardingValue);
      }
    }
    
  • 具體分片實現(xiàn)邏輯:ShardingUtils

    @UtilityClass
    public class ShardingUtils {
        public static final String QUARTER_SHARDING_PATTERN = "%s_%d_q%d";
    
        /**
        * logicTableName_{year}_q{quarter}
        * 按季度范圍分片
        * @param availableTargetNames 可用的真實表集合
        * @param shardingValue 分片值
        * @return
        */
        public Collection<String> quarterRangeSharding(Collection<String> availableTargetNames, RangeShardingValue<Date> shardingValue) {
            Range<Date> valueRange = shardingValue.getValueRange();
            Date lower = valueRange.lowerEndpoint();
            Date upper = valueRange.upperEndpoint();
            LocalDate lowerLocalDate =
                    lower.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
            LocalDate upperLocalDate =
                    upper.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
            int lowerYear = lowerLocalDate.getYear();
            int lowerMonth = lowerLocalDate.getMonthValue();
            int lowerQuarter = getQuarter(lowerMonth);
    
            int upperYear = upperLocalDate.getYear();
            int upperMonth = upperLocalDate.getMonthValue();
            int upperQuarter = getQuarter(upperMonth);
    
            String logicTableName = shardingValue.getLogicTableName();
            List<String> actualTableNames = new ArrayList<>();
    
            if (lowerYear == upperYear) {
                // 不跨年
                for (int quarter = lowerQuarter; quarter <= upperQuarter; quarter++) {
                    String actualTableName = String.format(QUARTER_SHARDING_PATTERN, logicTableName, lowerYear, quarter);
                    actualTableNames.add(actualTableName);
                }
            } else {
                // 跨年
                for (int quarter = lowerQuarter; quarter <= Constant.INT_4; quarter++) {
                    String actualTableName = String.format(QUARTER_SHARDING_PATTERN, logicTableName, lowerYear, quarter);
                    actualTableNames.add(actualTableName);
                }
    
                for (int year = lowerYear + 1; year < upperYear; year++) {
                    for (int quarter = 1; quarter <= Constant.INT_4; quarter++) {
                        String actualTableName = String.format(QUARTER_SHARDING_PATTERN, logicTableName, year, quarter);
                        actualTableNames.add(actualTableName);
                    }
                }
    
                for (int quarter = 1; quarter <= upperQuarter; quarter++) {
                    String actualTableName = String.format(QUARTER_SHARDING_PATTERN, logicTableName, upperYear, quarter);
                    actualTableNames.add(actualTableName);
                }
    
            }
    
            if (!availableTargetNames.containsAll(actualTableNames)) {
                throw new IllegalArgumentException("分片異常!shardingValue=" + shardingValue + "; availableTargetNames=" + availableTargetNames);
            }
            return actualTableNames;
        }
    
        /**
        * logicTableName_{year}_q{quarter}
        * 按季度精確分片
        * @param availableTargetNames 可用的真實表集合
        * @param shardingValue 分片值
        * @return
        */
        public static String quarterPreciseSharding(Collection<String> availableTargetNames, PreciseShardingValue<Date> shardingValue) {
            String logicTableName = shardingValue.getLogicTableName();
            Date value = shardingValue.getValue();
            LocalDate localDate =
                    value.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
            int year = localDate.getYear();
            int month = localDate.getMonthValue();
            int quarter = getQuarter(month);
            String actualTableName = String.format(QUARTER_SHARDING_PATTERN, logicTableName, year, quarter);
            if (availableTargetNames.contains(actualTableName)) {
                return actualTableName;
            }
            throw new IllegalArgumentException("分片異常令宿!shardingValue=" + shardingValue + "; availableTargetNames=" + availableTargetNames);
        }
    
        private static int getQuarter(int month) {
            if (month >= Constant.INT_1 && month <= Constant.INT_3) {
                return Constant.INT_1;
            } else if (month >= Constant.INT_4 && month <= Constant.INT_6) {
                return Constant.INT_2;
            } else if (month >= Constant.INT_7 && month <= Constant.INT_9) {
                return Constant.INT_3;
            } else if (month >= Constant.INT_10 && month <= Constant.INT_12) {
                return Constant.INT_4;
            } else {
                throw new IllegalArgumentException("month非法叼耙!month=" + month);
            }
        }
    }
    
    

到這里,針對hc_question_reply_record表粒没,使用reply_wheel_time作為分片鍵筛婉,按照季度分片的處理就完成了。還有一點要注意的就是,分庫分表之后爽撒,查詢的時候最好都帶上分片鍵作為查詢條件入蛆,否則就會使用全庫路由,性能很低硕勿。 還有就是Sharing-JDBC對mysql的全文索引支持的不是很好哨毁,項目有使用到的地方也要注意一下≡次洌總結來說整個過程還是比較簡單的扼褪,后續(xù)碰到其它業(yè)務場景,相信大家按照這個思路肯定都能解決的粱栖。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末话浇,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子闹究,更是在濱河造成了極大的恐慌幔崖,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件渣淤,死亡現(xiàn)場離奇詭異赏寇,居然都是意外死亡,警方通過查閱死者的電腦和手機价认,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門嗅定,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人刻伊,你說我怎么就攤上這事露戒。” “怎么了捶箱?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長动漾。 經常有香客問我丁屎,道長,這世上最難降的妖魔是什么旱眯? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任晨川,我火速辦了婚禮,結果婚禮上删豺,老公的妹妹穿的比我還像新娘共虑。我一直安慰自己,他們只是感情好呀页,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布妈拌。 她就那樣靜靜地躺著,像睡著了一般蓬蝶。 火紅的嫁衣襯著肌膚如雪尘分。 梳的紋絲不亂的頭發(fā)上猜惋,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天,我揣著相機與錄音培愁,去河邊找鬼著摔。 笑死,一個胖子當著我的面吹牛定续,可吹牛的內容都是我干的谍咆。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼私股,長吁一口氣:“原來是場噩夢啊……” “哼卧波!你這毒婦竟也來了?” 一聲冷哼從身側響起庇茫,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤港粱,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后旦签,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體查坪,經...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年宁炫,在試婚紗的時候發(fā)現(xiàn)自己被綠了偿曙。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡羔巢,死狀恐怖望忆,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情竿秆,我是刑警寧澤启摄,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站幽钢,受9級特大地震影響歉备,放射性物質發(fā)生泄漏。R本人自食惡果不足惜匪燕,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一蕾羊、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧帽驯,春花似錦龟再、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春截碴,著一層夾襖步出監(jiān)牢的瞬間梳侨,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工日丹, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留走哺,地道東北人。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓哲虾,卻偏偏與公主長得像丙躏,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子束凑,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344