SpringBoot整合MybatisPlus多數(shù)據(jù)源

相信在很多使用MybatisPlus框架的小伙伴都會遇到多數(shù)據(jù)源的配置問題泳秀,并且官網(wǎng)也給出了推薦使用多數(shù)據(jù)源 (dynamic-datasource-spring-boot-starter) 組件來實現(xiàn)阅爽。由于最近項目也在使用這個組件來實現(xiàn)多數(shù)據(jù)源切換疯淫,因此想了解一下該組件是如何運行的韩脑,經(jīng)過自己的調(diào)試宝踪,簡單記錄一下這個組件的實現(xiàn),也以便日后組件如果出問題了或者某些地方需要開次開發(fā)時有個參考。

簡單實現(xiàn)數(shù)據(jù)源切換

數(shù)據(jù)庫demo

本例子使用的是同一個MYSQL服務(wù)躺枕,不同數(shù)據(jù)庫來進行調(diào)試的,具體如圖所示

database01.jpg

SpringBoot demo

添加依賴

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.0</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
            <version>3.4.0</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.20</version>
            <scope>provided</scope>
        </dependency>

配置YML文件

spring:
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    hikari:
      minimum-idle: 5
      maximum-pool-size: 15
      idle-timeout: 30000
      max-lifetime: 1800000
      connection-timeout: 30000
      pool-name: OasisHikariCP
      connection-test-query: SELECT 1
    dynamic:
      primary: db1    #默認(rèn)主數(shù)據(jù)源
      datasource:
        db1:                 #配置主數(shù)據(jù)源
          url: jdbc:mysql://ip:3306/demo_user?useSSL=true&requireSSL=false&serverTimezone=UTC&characterEncoding=UTF-8
          username: root
          password: root
          driver-class-name: com.mysql.cj.jdbc.Driver
        db2:                #配置其他數(shù)據(jù)源
          url: jdbc:mysql://ip:3306/demo_class?useSSL=true&requireSSL=false&serverTimezone=UTC&characterEncoding=UTF-8
          username: root
          password: root
          driver-class-name: com.mysql.cj.jdbc.Driver

MybaitsPlus

實體層

UserEntity.class

/**
 * description: DB1中的實體
 * date: 2021/7/13 13:38 <br>
 * author: Neal <br>
 * version: 1.0 <br>
 */
@TableName("user_t")
public class UserEntity {

    private long id;

    private String userName;

    private String userSex;

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getUserSex() {
        return userSex;
    }

    public void setUserSex(String userSex) {
        this.userSex = userSex;
    }
}

ClassEntity.class

/**
 * description: DB2中的實體
 * date: 2021/7/13 13:40 <br>
 * author: Neal <br>
 * version: 1.0 <br>
 */
@TableName("class_t")
public class ClassEntity {

    private String name;

    private String number;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getNumber() {
        return number;
    }

    public void setNumber(String number) {
        this.number = number;
    }
}

mapper層

UserMapper.class(使用默認(rèn)數(shù)據(jù)源)

/**
 * description: UserMapper <br>
 * date: 2021/7/13 13:41 <br>
 * author: Neal <br>
 * version: 1.0 <br>
 */
public interface UserMapper extends BaseMapper<UserEntity> {
}

ClassMapper.class(使用另外一個數(shù)據(jù)源)

/**
 * description: ClassMapper <br>
 * date: 2021/7/13 13:41 <br>
 * author: Neal <br>
 * version: 1.0 <br>
 */
@DS("db2")     //使用另外一個數(shù)據(jù)源
public interface ClassMapper extends BaseMapper<ClassEntity> {
}

單元測試

test01.jpg

結(jié)果已經(jīng)是可以完美運行多數(shù)據(jù)源。

源碼解析

在我們搞項目中拐云,不僅要學(xué)會用這些組件罢猪,更重要的是 要知其所以然,知道他是如何實現(xiàn)的叉瘩,其實原理也就是網(wǎng)上能搜到的基于切面的代理處理方式膳帕,但是其中有些內(nèi)容還是值得去學(xué)習(xí)。

自動裝配

首先我們從 dynamic-datasource組件的自動裝配開始

step1.jpg

接下來讓我們來看一下 這個自動裝配類薇缅,所裝配的Bean

@Slf4j
@Configuration
//啟動SpringBoot 自動裝配 DynamicDataSourceProperties外部化配置
@EnableConfigurationProperties(DynamicDataSourceProperties.class)
//聲明裝配加載順序,在 DataSourceAutoConfiguration 之前加載
@AutoConfigureBefore(value = DataSourceAutoConfiguration.class, name = "com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure")
//當(dāng)自動裝配時危彩,引入并自動裝配下列三個自動裝配類
@Import(value = {DruidDynamicDataSourceConfiguration.class, DynamicDataSourceCreatorAutoConfiguration.class, DynamicDataSourceHealthCheckConfiguration.class})
//自動裝配加載條件 當(dāng) spring.datasource.dynamic = true時 進行自動裝配的加載,默認(rèn)缺省為true
@ConditionalOnProperty(prefix = DynamicDataSourceProperties.PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true)
public class DynamicDataSourceAutoConfiguration implements InitializingBean {

    //注入外部化配置
    private final DynamicDataSourceProperties properties;
    
    
    private final List<DynamicDataSourcePropertiesCustomizer> dataSourcePropertiesCustomizers;

    //構(gòu)造函數(shù)注入
    public DynamicDataSourceAutoConfiguration(
            DynamicDataSourceProperties properties,
            ObjectProvider<List<DynamicDataSourcePropertiesCustomizer>> dataSourcePropertiesCustomizers) {
        this.properties = properties;
        this.dataSourcePropertiesCustomizers = dataSourcePropertiesCustomizers.getIfAvailable();
    }

    //多數(shù)據(jù)源加載接口泳桦,默認(rèn)的實現(xiàn)為從yml信息中加載所有數(shù)據(jù)源 
    @Bean
    public DynamicDataSourceProvider ymlDynamicDataSourceProvider() {
        return new YmlDynamicDataSourceProvider(properties.getDatasource());
    }

    //實現(xiàn)DataSource JAVA JNDI 后期Spring 容器中 所有的數(shù)據(jù)庫連接都從該實現(xiàn)Bean 中獲取
    @Bean
    @ConditionalOnMissingBean
    public DataSource dataSource() {
        DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource();
        dataSource.setPrimary(properties.getPrimary());
        dataSource.setStrict(properties.getStrict());
        dataSource.setStrategy(properties.getStrategy());
        dataSource.setP6spy(properties.getP6spy());
        dataSource.setSeata(properties.getSeata());
        return dataSource;
    }

    //設(shè)置動態(tài)數(shù)據(jù)源轉(zhuǎn)換切換配置器
    @Role(value = BeanDefinition.ROLE_INFRASTRUCTURE)
    @Bean
    public Advisor dynamicDatasourceAnnotationAdvisor(DsProcessor dsProcessor) {
        DynamicDataSourceAnnotationInterceptor interceptor = new DynamicDataSourceAnnotationInterceptor(properties.isAllowedPublicOnly(), dsProcessor);
        DynamicDataSourceAnnotationAdvisor advisor = new DynamicDataSourceAnnotationAdvisor(interceptor);
        advisor.setOrder(properties.getOrder());
        return advisor;
    }

    //數(shù)據(jù)庫事務(wù)的切面配置類
    @Role(value = BeanDefinition.ROLE_INFRASTRUCTURE)
    @ConditionalOnProperty(prefix = DynamicDataSourceProperties.PREFIX, name = "seata", havingValue = "false", matchIfMissing = true)
    @Bean
    public Advisor dynamicTransactionAdvisor() {
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression("@annotation(com.baomidou.dynamic.datasource.annotation.DSTransactional)");
        return new DefaultPointcutAdvisor(pointcut, new DynamicLocalTransactionAdvisor());
    }

    //DynamicDataSourceAnnotationInterceptor 切面配置器中所需要的執(zhí)行鏈汤徽,
    //主要用來確定使用哪個數(shù)據(jù)源
    @Bean
    @ConditionalOnMissingBean
    public DsProcessor dsProcessor(BeanFactory beanFactory) {
        DsHeaderProcessor headerProcessor = new DsHeaderProcessor();
        DsSessionProcessor sessionProcessor = new DsSessionProcessor();
        DsSpelExpressionProcessor spelExpressionProcessor = new DsSpelExpressionProcessor();
        spelExpressionProcessor.setBeanResolver(new BeanFactoryResolver(beanFactory));
        headerProcessor.setNextProcessor(sessionProcessor);
        sessionProcessor.setNextProcessor(spelExpressionProcessor);
        return headerProcessor;
    }

    //Bean注入后所執(zhí)行的方法,本Demo中目前暫無使用
    @Override
    public void afterPropertiesSet() {
        if (!CollectionUtils.isEmpty(dataSourcePropertiesCustomizers)) {
            for (DynamicDataSourcePropertiesCustomizer customizer : dataSourcePropertiesCustomizers) {
                customizer.customize(properties);
            }
        }
    }

}

大體上的自動裝配已經(jīng)介紹完了灸撰,接下來我們逐個將重要的代碼段或者類來進行解釋

DynamicDataSourceCreatorAutoConfiguration 分析

這個類主要是進行數(shù)據(jù)源加載的 主要代碼如下

@Slf4j
@Configuration
@AllArgsConstructor
@EnableConfigurationProperties(DynamicDataSourceProperties.class)
public class DynamicDataSourceCreatorAutoConfiguration {

    //描述Bean的 注入順序
    public static final int JNDI_ORDER = 1000;
    public static final int DRUID_ORDER = 2000;
    public static final int HIKARI_ORDER = 3000;
    public static final int BEECP_ORDER = 4000;
    public static final int DBCP2_ORDER = 5000;
    public static final int DEFAULT_ORDER = 6000;

    private final DynamicDataSourceProperties properties;

    //默認(rèn)的數(shù)據(jù)源創(chuàng)造器
    @Primary
    @Bean
    @ConditionalOnMissingBean
    public DefaultDataSourceCreator dataSourceCreator(List<DataSourceCreator> dataSourceCreators) {
        DefaultDataSourceCreator defaultDataSourceCreator = new DefaultDataSourceCreator();
        defaultDataSourceCreator.setProperties(properties);
        defaultDataSourceCreator.setCreators(dataSourceCreators);
        return defaultDataSourceCreator;
    }

    //省略部分代碼

    /**
     * 存在Hikari數(shù)據(jù)源時, 加入創(chuàng)建器
     */
    @ConditionalOnClass(HikariDataSource.class)
    @Configuration
    public class HikariDataSourceCreatorConfiguration {
        @Bean
        @Order(HIKARI_ORDER)
        @ConditionalOnMissingBean
        public HikariDataSourceCreator hikariDataSourceCreator() {
            return new HikariDataSourceCreator(properties.getHikari());
        }
    }

    //省略部分代碼

}

當(dāng)Spring 容器注入 DefaultDataSourceCreator 實例后 谒府,接下來就被 DynamicDataSourceProvider 這個類所使用。

DynamicDataSourceProvider 分析

@Slf4j
@AllArgsConstructor
public class YmlDynamicDataSourceProvider extends AbstractDataSourceProvider {

    /**
     * 所有數(shù)據(jù)源
     */
    private final Map<String, DataSourceProperty> dataSourcePropertiesMap;

    //通過構(gòu)造函數(shù)注入所有的 數(shù)據(jù)源 然后調(diào)用該父類方法創(chuàng)建數(shù)據(jù)源集合
    @Override
    public Map<String, DataSource> loadDataSources() {
        return createDataSourceMap(dataSourcePropertiesMap);
    }
}

@Slf4j
public abstract class AbstractDataSourceProvider implements DynamicDataSourceProvider {

    //從Spring 容器中獲取注入好的 DefaultDataSourceCreator 
    @Autowired
    private DefaultDataSourceCreator defaultDataSourceCreator;

    //創(chuàng)建數(shù)據(jù)源集合
    protected Map<String, DataSource> createDataSourceMap(
            Map<String, DataSourceProperty> dataSourcePropertiesMap) {
        Map<String, DataSource> dataSourceMap = new HashMap<>(dataSourcePropertiesMap.size() * 2);
        for (Map.Entry<String, DataSourceProperty> item : dataSourcePropertiesMap.entrySet()) {
            DataSourceProperty dataSourceProperty = item.getValue();
            String poolName = dataSourceProperty.getPoolName();
            if (poolName == null || "".equals(poolName)) {
                poolName = item.getKey();
            }
            dataSourceProperty.setPoolName(poolName);
            dataSourceMap.put(poolName, defaultDataSourceCreator.createDataSource(dataSourceProperty));
        }
        return dataSourceMap;
    }
}

DynamicDataSourceAnnotationAdvisor 分析

這個其實就是Spring AOP的切面配置器 主要代碼如下

public class DynamicDataSourceAnnotationAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware {

    //切面增強方法
    private final Advice advice;

    private final Pointcut pointcut;

   //構(gòu)造方法注入
    public DynamicDataSourceAnnotationAdvisor(@NonNull DynamicDataSourceAnnotationInterceptor dynamicDataSourceAnnotationInterceptor) {
        this.advice = dynamicDataSourceAnnotationInterceptor;
        this.pointcut = buildPointcut();
    }

    @Override
    public Pointcut getPointcut() {
        return this.pointcut;
    }

    
    @Override
    public Advice getAdvice() {
        return this.advice;
    }

    //省略部分代碼
    
    
    //當(dāng)有類或者方法中有 DS.class 注解時 進行 切面增強
    private Pointcut buildPointcut() {
        Pointcut cpc = new AnnotationMatchingPointcut(DS.class, true);
        Pointcut mpc = new AnnotationMethodPoint(DS.class);
        return new ComposablePointcut(cpc).union(mpc);
    }

    //省略部分代碼 
}

DynamicDataSourceAnnotationInterceptor 分析

該類為切面增強浮毯,即當(dāng)上面的DynamicDataSourceAnnotationAdvisor 攔截到類或者方法中有 DS.class 注解時 完疫,調(diào)用該增強類進行處理

public class DynamicDataSourceAnnotationInterceptor implements MethodInterceptor {

    /**
     * The identification of SPEL.
     */
    private static final String DYNAMIC_PREFIX = "#";

    private final DataSourceClassResolver dataSourceClassResolver;
    private final DsProcessor dsProcessor;

    public DynamicDataSourceAnnotationInterceptor(Boolean allowedPublicOnly, DsProcessor dsProcessor) {
        dataSourceClassResolver = new DataSourceClassResolver(allowedPublicOnly);
        this.dsProcessor = dsProcessor;
    }

    //AOP攔截后進行 切面增強方法
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        //選擇數(shù)據(jù)源
        String dsKey = determineDatasourceKey(invocation);
        //使用基于ThreadLocal的實現(xiàn)切換數(shù)據(jù)源
        DynamicDataSourceContextHolder.push(dsKey);
        try {
            return invocation.proceed();
        } finally {
            DynamicDataSourceContextHolder.poll();
        }
    }

    //通過調(diào)用 DsProcessor 來鏈?zhǔn)秸{(diào)用進行 數(shù)據(jù)源的確認(rèn)
    private String determineDatasourceKey(MethodInvocation invocation) {
        String key = dataSourceClassResolver.findDSKey(invocation.getMethod(), invocation.getThis());
        return (!key.isEmpty() && key.startsWith(DYNAMIC_PREFIX)) ? dsProcessor.determineDatasource(invocation, key) : key;
    }
}

DefaultPointcutAdvisor 分析

該切面增強為事務(wù)增強,設(shè)置此增強類后债蓝,不能與Spring 源事務(wù)或者 @Transactional 注解共用壳鹤。

@Slf4j
public class DynamicLocalTransactionAdvisor implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        if (!StringUtils.isEmpty(TransactionContext.getXID())) {
            return methodInvocation.proceed();
        }
        boolean state = true;
        Object o;
        String xid = UUID.randomUUID().toString();
        TransactionContext.bind(xid);
        try {
            o = methodInvocation.proceed();
        } catch (Exception e) {
            state = false;
            throw e;
        } finally {
            ConnectionFactory.notify(state);
            TransactionContext.remove();
        }
        return o;
    }
}

DynamicDataSourceContextHolder 核心切換類

public final class DynamicDataSourceContextHolder {

    /**
     * 為什么要用鏈表存儲(準(zhǔn)確的是棧)
     * <pre>
     * 為了支持嵌套切換,如ABC三個service都是不同的數(shù)據(jù)源
     * 其中A的某個業(yè)務(wù)要調(diào)B的方法惦蚊,B的方法需要調(diào)用C的方法。一級一級調(diào)用切換讯嫂,形成了鏈蹦锋。
     * 傳統(tǒng)的只設(shè)置當(dāng)前線程的方式不能滿足此業(yè)務(wù)需求,必須使用棧欧芽,后進先出莉掂。
     * </pre>
     */
    private static final ThreadLocal<Deque<String>> LOOKUP_KEY_HOLDER = new NamedThreadLocal<Deque<String>>("dynamic-datasource") {
        @Override
        protected Deque<String> initialValue() {
            return new ArrayDeque<>();
        }
    };

    private DynamicDataSourceContextHolder() {
    }

    /**
     * 獲得當(dāng)前線程數(shù)據(jù)源
     *
     * @return 數(shù)據(jù)源名稱
     */
    public static String peek() {
        return LOOKUP_KEY_HOLDER.get().peek();
    }

    /**
     * 設(shè)置當(dāng)前線程數(shù)據(jù)源
     * <p>
     * 如非必要不要手動調(diào)用,調(diào)用后確保最終清除
     * </p>
     *
     * @param ds 數(shù)據(jù)源名稱
     */
    public static String push(String ds) {
        String dataSourceStr = StringUtils.isEmpty(ds) ? "" : ds;
        LOOKUP_KEY_HOLDER.get().push(dataSourceStr);
        return dataSourceStr;
    }

    /**
     * 清空當(dāng)前線程數(shù)據(jù)源
     * <p>
     * 如果當(dāng)前線程是連續(xù)切換數(shù)據(jù)源 只會移除掉當(dāng)前線程的數(shù)據(jù)源名稱
     * </p>
     */
    public static void poll() {
        Deque<String> deque = LOOKUP_KEY_HOLDER.get();
        deque.poll();
        if (deque.isEmpty()) {
            LOOKUP_KEY_HOLDER.remove();
        }
    }

    /**
     * 強制清空本地線程
     * <p>
     * 防止內(nèi)存泄漏千扔,如手動調(diào)用了push可調(diào)用此方法確保清除
     * </p>
     */
    public static void clear() {
        LOOKUP_KEY_HOLDER.remove();
    }
}

大致核心的代碼已經(jīng)介紹完了憎妙,接下來我們逐步debugger,摸清其執(zhí)行流程。

數(shù)據(jù)源切換執(zhí)行流程

現(xiàn)在當(dāng)我們執(zhí)行上面的SpringBoot demo中的 調(diào)用注解 @DS("db2") 的 Mapper 查詢數(shù)據(jù)庫時曲楚,他的順序如下(只給出涉及到該組件的相關(guān)類)

  1. ClassMapper#selectList() : 執(zhí)行Mybatis查詢操作

  2. DynamicDataSourceAnnotationInterceptor#invoke() : Spring AOP 攔截到帶有 @DS("db2") 并執(zhí)行代理增強操作

  3. DataSourceClassResolver#findDSKey() : 查找有注解@DS() 的 類或方法厘唾,獲取對應(yīng)的數(shù)據(jù)源Key 值 也就是 db2。

  4. DynamicDataSourceContextHolder#push() : 設(shè)置當(dāng)前線程數(shù)據(jù)源

  5. DynamicRoutingDataSource#getConnection(): 調(diào)用父類方法獲取數(shù)據(jù)庫連接 這里兩種處理方式 如下所示

        public Connection getConnection() throws SQLException {
            String xid = TransactionContext.getXID();
            //無事務(wù)時 即當(dāng)前操作為 查詢
            if (StringUtils.isEmpty(xid)) {
                return determineDataSource().getConnection();
            } else {
                //有事物時 龙誊,先從 之前DynamicDataSourceContextHolder 中獲取數(shù)據(jù)源 先進先出原則
                String ds = DynamicDataSourceContextHolder.peek();
                ds = StringUtils.isEmpty(ds) ? "default" : ds;
                ConnectionProxy connection = ConnectionFactory.getConnection(ds);
                 //創(chuàng)建數(shù)據(jù)源
                return connection == null ? getConnectionProxy(ds, determineDataSource().getConnection()) : connection;
            }
        }
    
  1. DynamicRoutingDataSource#getDataSource 設(shè)置數(shù)據(jù)源

        public DataSource getDataSource(String ds) {
            //如果當(dāng)前無 數(shù)據(jù)源聲明 則使用默認(rèn)數(shù)據(jù)源
            if (StringUtils.isEmpty(ds)) {
                return determinePrimaryDataSource();
            } else if (!groupDataSources.isEmpty() && groupDataSources.containsKey(ds)) {
                log.debug("dynamic-datasource switch to the datasource named [{}]", ds);
                return groupDataSources.get(ds).determineDataSource();
            } else if (dataSourceMap.containsKey(ds)) {
                //如果當(dāng)前存在數(shù)據(jù)源則取出該數(shù)據(jù)源返回
                log.debug("dynamic-datasource switch to the datasource named [{}]", ds);
                return dataSourceMap.get(ds);
            }
            if (strict) {
                throw new CannotFindDataSourceException("dynamic-datasource could not find a datasource named" + ds);
            }
            return determinePrimaryDataSource();
        }
    
  1. 執(zhí)行剩余的數(shù)據(jù)庫操作至結(jié)束抚垃。

小結(jié)

大體上寫的略微混亂,但是只要我們知道其自動裝配時 ,實例化了哪些Bean,并且知道這些Bean 是干什么的 鹤树,合適調(diào)用的铣焊,根據(jù)執(zhí)行流程逐步Debugger調(diào)試,就可以明白dynamic-datasource組件是如何進行數(shù)據(jù)源切換的罕伯,在流程中我認(rèn)為比較經(jīng)典也是比較核心的地方已經(jīng)標(biāo)注出源碼曲伊。我們可以借鑒 DynamicDataSourceContextHolder 這個公共類的思想,擴展和優(yōu)化我們現(xiàn)有的項目中某些跨資源調(diào)用的問題追他。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末坟募,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子湿酸,更是在濱河造成了極大的恐慌婿屹,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,430評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件推溃,死亡現(xiàn)場離奇詭異昂利,居然都是意外死亡,警方通過查閱死者的電腦和手機铁坎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,406評論 3 398
  • 文/潘曉璐 我一進店門蜂奸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人硬萍,你說我怎么就攤上這事扩所。” “怎么了朴乖?”我有些...
    開封第一講書人閱讀 167,834評論 0 360
  • 文/不壞的土叔 我叫張陵祖屏,是天一觀的道長。 經(jīng)常有香客問我买羞,道長袁勺,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,543評論 1 296
  • 正文 為了忘掉前任畜普,我火速辦了婚禮期丰,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘吃挑。我一直安慰自己钝荡,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,547評論 6 397
  • 文/花漫 我一把揭開白布舶衬。 她就那樣靜靜地躺著埠通,像睡著了一般。 火紅的嫁衣襯著肌膚如雪逛犹。 梳的紋絲不亂的頭發(fā)上植阴,一...
    開封第一講書人閱讀 52,196評論 1 308
  • 那天蟹瘾,我揣著相機與錄音,去河邊找鬼掠手。 笑死憾朴,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的喷鸽。 我是一名探鬼主播众雷,決...
    沈念sama閱讀 40,776評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼做祝!你這毒婦竟也來了砾省?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,671評論 0 276
  • 序言:老撾萬榮一對情侶失蹤混槐,失蹤者是張志新(化名)和其女友劉穎编兄,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體声登,經(jīng)...
    沈念sama閱讀 46,221評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡狠鸳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,303評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了悯嗓。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片件舵。...
    茶點故事閱讀 40,444評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖脯厨,靈堂內(nèi)的尸體忽然破棺而出铅祸,到底是詐尸還是另有隱情,我是刑警寧澤合武,帶...
    沈念sama閱讀 36,134評論 5 350
  • 正文 年R本政府宣布临梗,位于F島的核電站,受9級特大地震影響稼跳,放射性物質(zhì)發(fā)生泄漏盟庞。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,810評論 3 333
  • 文/蒙蒙 一岂贩、第九天 我趴在偏房一處隱蔽的房頂上張望茫经。 院中可真熱鬧巷波,春花似錦萎津、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,285評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至垮耳,卻和暖如春颈渊,著一層夾襖步出監(jiān)牢的瞬間遂黍,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,399評論 1 272
  • 我被黑心中介騙來泰國打工俊嗽, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留雾家,地道東北人。 一個月前我還...
    沈念sama閱讀 48,837評論 3 376
  • 正文 我出身青樓绍豁,卻偏偏與公主長得像芯咧,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子竹揍,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,455評論 2 359

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