spring-boot+mybatis+druid+atomikos(支持分布式事務)

1.多數(shù)據(jù)源事務管控

????之前寫了一篇基于注解來動態(tài)切換數(shù)據(jù)源的demo琉历,但是那個demo是不支持多數(shù)據(jù)源的事務的且改,也就是說在執(zhí)行多數(shù)據(jù)源數(shù)據(jù)改動操作的時候幽纷,如果其中某個數(shù)據(jù)源發(fā)生異常渴杆,之前操作的數(shù)據(jù)源的事務已經(jīng)提交不會回滾挠羔,只有發(fā)生異常的數(shù)據(jù)庫才會回滾事務墓陈,這就導致了事務的不一致性恶守,這里我參考了網(wǎng)上的大量文章,在基于注解動態(tài)切換數(shù)據(jù)源的基礎上改進贡必,使用atomikos分布式事務來對多個數(shù)據(jù)源資源的事務進行管控兔港,從而實現(xiàn)多數(shù)據(jù)源的事務一致性


2 項目搭建

  • 2.1 pom依賴和yml配置
  • 2.1.1 pom.xml

pom依賴和上篇文章差不多,只是額外添加了atomikos的依賴仔拟,以及apache下的commons-lang3的jar包依賴衫樊,mysql的版本需要使用6.0.6,版本高了利花,啟動會報錯

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <!--<parent>-->
        <!--<groupId>org.springframework.boot</groupId>-->
        <!--<artifactId>spring-boot-starter-parent</artifactId>-->
        <!--<version>2.2.1.RELEASE</version>-->
        <!--<relativePath/> &lt;!&ndash; lookup parent from repository &ndash;&gt;-->
    <!--</parent>-->
    <groupId>com.sccl</groupId>
    <artifactId>data_source_change</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>data_source_change</name>
    <description>Demo project for Spring Boot</description>

    <!--不繼承spring-boot-starter-parent科侈,使用依賴管理-->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.1.6.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!-- SpringBoot Web容器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- SpringBoot 攔截器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <!-- SpringBoot集成mybatis框架 -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>
        <!--可以不使用mybatis-plus中的會話工廠來完成數(shù)據(jù)源切換了,故而注釋此依賴-->
        <!--<dependency>-->
            <!--<groupId>com.baomidou</groupId>-->
            <!--<artifactId>mybatis-plus-boot-starter</artifactId>-->
            <!--<version>3.1.0</version>-->
        <!--</dependency>-->
        <!--阿里數(shù)據(jù)庫連接池 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <!-- Mysql驅(qū)動包 這里請使用6.0.6版本的mysql,版本高了會報錯-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>6.0.6</version>
            <!--<scope>runtime</scope>-->
        </dependency>
        <!--atomikos分布式事務-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jta-atomikos</artifactId>
        </dependency>
        <!--防止@ConfigurationProperties屬性注入爆錯炒事,引入此依賴-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
  • 2.1.2 yml文件

yml文件中臀栈,主要改動是將type改成xa類型的數(shù)據(jù)源,這樣才能讓多個數(shù)據(jù)源資源被atomoikos管理

server:
  port: 8099
  servlet:
    context-path: /data
spring:
  datasource:
    druid:
      # 注意(名稱不支持大寫和下劃線可用中橫線 比如 錯誤 的命名(slave_**, slaveTwo))
      master: #主庫(數(shù)據(jù)源-1)
        type: com.alibaba.druid.pool.xa.DruidXADataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/chapter05-1?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowMultiQueries=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai
        username: root
        password: 123456
      slave: #從庫(數(shù)據(jù)源-2)
        open: true
        type: com.alibaba.druid.pool.xa.DruidXADataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/chapter05-2?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowMultiQueries=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai
        username: root
        password: 123456
    #jta相關參數(shù)配置
  jta:
    log-dir: classpath:tx-logs
    transaction-manager-id: txManager
#mybatis的配置在會話工廠里面配置挠乳,在這里配置會報錯
#mybatis:
#  type-aliases-package: com.sccl.data_source_change.*.domain #包別名
#  mapper-locations: classpath*:mybatis/**/*.xml #掃描mapper映射文件
  • 2.2 項目代碼重構

項目目錄:


項目目錄
  • 2.2.1 自定義注解和切面

代碼基本沒改動

DataSource自定義注解

package com.sccl.data_source_change.aspectj.annotation;


import com.sccl.data_source_change.enumConst.DataSourceEnum;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**自定義多數(shù)據(jù)源切換注解
 * Create by wangbin
 * 2019-11-18-15:25
 */

/**
 * 注解說明:
 * @author wangbin
 * @date 2019/11/18 15:36

源碼樣例:

 @Target(ElementType.METHOD)
 @Retention(RetentionPolicy.RUNTIME)
 @Documented
 @Inherited
 public @interface MthCache {
 String key();
 }

 @Target 注解

 功能:指明了修飾的這個注解的使用范圍权薯,即被描述的注解可以用在哪里。

 ElementType的取值包含以下幾種:

 TYPE:類睡扬,接口或者枚舉
 FIELD:域盟蚣,包含枚舉常量
 METHOD:方法
 PARAMETER:參數(shù)
 CONSTRUCTOR:構造方法
 LOCAL_VARIABLE:局部變量
 ANNOTATION_TYPE:注解類型
 PACKAGE:包
 =======================================================================================
 @Retention 注解

 功能:指明修飾的注解的生存周期,即會保留到哪個階段威蕉。

 RetentionPolicy的取值包含以下三種:

 SOURCE:源碼級別保留刁俭,編譯后即丟棄。
 CLASS:編譯級別保留韧涨,編譯后的class文件中存在牍戚,在jvm運行時丟棄,這是默認值虑粥。
 RUNTIME: 運行級別保留如孝,編譯后的class文件中存在,在jvm運行時保留娩贷,可以被反射調(diào)用第晰。

 ====================================================================================
 @Documented 注解

 功能:指明修飾的注解,可以被例如javadoc此類的工具文檔化,只負責標記茁瘦,沒有成員取值品抽。
 ========================================================================================
 @Inherited注解

 功能:允許子類繼承父類中的注解。

 注意L鹑邸:

 @interface意思是聲明一個注解圆恤,方法名對應參數(shù)名,返回值類型對應參數(shù)類型腔稀。
 */
 @Target(ElementType.METHOD) //此注解使用于方法上
 @Retention(RetentionPolicy.RUNTIME) //此注解的生命周期為:運行時盆昙,在編譯后的class文件中存在,在jvm運行時保留焊虏,可以被反射調(diào)用
public @interface DataSource {
    /**
     * 切換數(shù)據(jù)源值
     */
    DataSourceEnum value() default DataSourceEnum.MASTER;
}

DsAspect數(shù)據(jù)源動態(tài)切換的切面

package com.sccl.data_source_change.aspectj;

import com.sccl.data_source_change.aspectj.annotation.DataSource;
import com.sccl.data_source_change.datasource.DynamicDataSourceContextHolder;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * 多數(shù)據(jù)源處理切面
 * 事務管理:
 * 事務管理在開啟時淡喜,需要確定數(shù)據(jù)源,也就是說數(shù)據(jù)源切換要在事務開啟之前诵闭,
 * 我們可以使用Order來配置執(zhí)行順序炼团,在AOP實現(xiàn)類上加Order注解,
 * 就可以使數(shù)據(jù)源切換提前執(zhí)行涂圆,order值越小们镜,執(zhí)行順序越靠前。
 * Create by wangbin
 * 2019-11-18-15:55
 */
@Aspect
@Order(1) //order值越小润歉,執(zhí)行順序越靠前。<!-- 設置切換數(shù)據(jù)源的優(yōu)先級 -->
@Component
public class DsAspect {
    protected Logger logger = LoggerFactory.getLogger(getClass());
    /**
     * 所有添加了DataSource自定義注解的方法都進入切面
     */
    @Pointcut("@annotation(com.sccl.data_source_change.aspectj.annotation.DataSource)")
    public void dsPointCut() {

    }
    // 這里使用@Around颈抚,在調(diào)用目標方法前踩衩,進行aop攔截,通過解析注解上的值來切換數(shù)據(jù)源贩汉。
    // 在調(diào)用方法結束后驱富,清除數(shù)據(jù)源。
    // 也可以使用@Before和@After來編寫匹舞,原理一樣褐鸥,這里就不多說了。
    @Around("dsPointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        if (method.isAnnotationPresent(DataSource.class)) {
            //獲取方法上的注解
            DataSource dataSource = method.getAnnotation(DataSource.class);
            if (dataSource != null) {
                //切換數(shù)據(jù)源
                DynamicDataSourceContextHolder.setDB(dataSource.value().getName());
            }
        }
        try {
            return point.proceed();
        } finally {
            // 銷毀數(shù)據(jù)源 在執(zhí)行方法之后
            DynamicDataSourceContextHolder.clearDB();
        }
    }
}

  • 2.2.2 數(shù)據(jù)源枚舉

枚舉 DataSourceEnum

package com.sccl.data_source_change.enumConst;

/**
 * Create by wangbin
 * 2019-11-19-16:54
 */
public enum DataSourceEnum {
    MASTER("master"),
    SLAVE("slave");
    private String name;

    public String getName() {
        return name;
    }

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

     DataSourceEnum(String name) {
        this.name = name;
    }
}
  • 2.2.3 動態(tài)數(shù)據(jù)源和動態(tài)數(shù)據(jù)源環(huán)境變量

DynamicDataSource動態(tài)數(shù)據(jù)源

package com.sccl.data_source_change.datasource;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import javax.sql.DataSource;
import java.util.Map;

/** 動態(tài)數(shù)據(jù)源
 * Create by wangbin
 * 2019-11-18-16:06
 */
public class DynamicDataSource extends AbstractRoutingDataSource {

    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources){
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }

    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDB();
    }
}

DynamicDataSourceContextHolder動態(tài)數(shù)據(jù)源環(huán)境變量控制

package com.sccl.data_source_change.datasource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** 當前線程數(shù)據(jù)源,負責管理數(shù)據(jù)源的環(huán)境變量
 * Create by wangbin
 * 2019-11-18-16:11
 */
public class DynamicDataSourceContextHolder {
    public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);
    /**
     * 使用ThreadLocal維護變量赐稽,ThreadLocal為每個使用該變量的線程提供獨立的變量副本叫榕,
     *  所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本姊舵。
     */
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
    /**
     * 設置數(shù)據(jù)源名
     */
    public static void setDB(String dbType){
        log.info("切換到{}數(shù)據(jù)源", dbType);
        CONTEXT_HOLDER.set(dbType);
    }
    /**
     * 獲取數(shù)據(jù)源名
     */
    public static String getDB(){
        return CONTEXT_HOLDER.get();
    }
    /**
     * 清理數(shù)據(jù)源名
     */
    public static void clearDB(){
        CONTEXT_HOLDER.remove();
    }
}

  • 2.2.4 數(shù)據(jù)源配置晰绎,分布式事務管理器,多數(shù)據(jù)源事務管理器括丁,重寫的mybatis會話工廠

數(shù)據(jù)源配置 DruidMutilConfig(項目代碼的主要改動位置)

在數(shù)據(jù)源配置中荞下,我們需要將master和slave數(shù)據(jù)庫的druid數(shù)據(jù)庫驅(qū)動換成xa的,同時要使用動態(tài)數(shù)據(jù)源來創(chuàng)建會話連接,這里和網(wǎng)上很多代碼的不同之處在于尖昏,網(wǎng)上很多文章都是給每個數(shù)據(jù)源單獨創(chuàng)建一個連接會話仰税,然后進行切換和事務管理,同時還需要分包抽诉,一個數(shù)據(jù)源就要分一個包陨簇,這里只使用動態(tài)數(shù)據(jù)源來創(chuàng)建會話連接,切換到哪個數(shù)據(jù)源的時候就用該數(shù)據(jù)源來獲取連接并管控事務很靈活掸鹅,

采坑經(jīng)歷:
1.在這個配置中加入事務后動態(tài)數(shù)據(jù)源沒法切換塞帐,需要重寫Transaction,讓我們能夠動態(tài)的根據(jù)DatabaseType獲取不同的Connection巍沙,并且要求不能影響整個事物的特性葵姥。
詳情參考了:
springboot+mybatis解決多數(shù)據(jù)源切換事務控制不生效的問題

2.在yml文件中配置的mybatis掃描xml包路徑和配置包別名,不起作用句携,所以在mybatis的會話工廠中配置的這兩個屬性榔幸,這里又有一個坑是mybatis的會話工廠不支持通配符配置包別名,所以參考了網(wǎng)上的文章矮嫉,寫一個類繼承mybatis的會話工廠重寫了配置包別名的方法削咆,然后使用這個類來配置包別名和xml路徑以及數(shù)據(jù)源和多數(shù)據(jù)源事務管理器
詳情參考了:
typeAliasesPackage支持通配符包路徑掃描

package com.sccl.data_source_change.config;

import com.sccl.data_source_change.datasource.DynamicDataSource;
import com.sccl.data_source_change.enumConst.DataSourceEnum;
import com.sccl.data_source_change.utils.PackagesSqlSessionFactoryBean;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.Environment;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.lang.Nullable;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

/**
 * druid 配置多數(shù)據(jù)源
 *
 * @author sccl
 */
@Configuration
@EnableTransactionManagement //開啟事務
@MapperScan("com.sccl.data_source_change.**.mapper")
public class DruidMutilConfig {


    @Bean(name = "masterDataSource")
    public DataSource masterDataSource(Environment env) {
        String sourceName = "master";
        Properties prop = build(env, "spring.datasource.druid.master.");
        AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
        //druid的數(shù)據(jù)庫驅(qū)動換成xa的
        xaDataSource.setXaDataSourceClassName("com.alibaba.druid.pool.xa.DruidXADataSource");
        xaDataSource.setUniqueResourceName(sourceName);
        xaDataSource.setPoolSize(5);
        xaDataSource.setXaProperties(prop);
        return xaDataSource;
    }

    @Bean(name = "slaveDataSource")
    public DataSource slaveDataSource(Environment env) {
        String sourceName = "slave";
        Properties prop = build(env, "spring.datasource.druid.slave.");
        AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
        //druid的數(shù)據(jù)庫驅(qū)動換成xa的
        xaDataSource.setXaDataSourceClassName("com.alibaba.druid.pool.xa.DruidXADataSource");
        xaDataSource.setUniqueResourceName(sourceName);
        xaDataSource.setPoolSize(5);
        xaDataSource.setXaProperties(prop);
        return xaDataSource;

    }

    private Properties build(Environment env, String prefix) {

        Properties prop = new Properties();
        prop.put("url", env.getProperty(prefix + "url"));
        prop.put("username", env.getProperty(prefix + "username"));
        prop.put("password", env.getProperty(prefix + "password"));
        prop.put("driverClassName", env.getProperty(prefix + "driverClassName", ""));
        //這里只設置了簡單的幾個屬性,如果想做更多的配置可以繼續(xù)往下添加即可
        return prop;
    }

    /**
     * 動態(tài)數(shù)據(jù)源,在這繼續(xù)添加 DataSource Bean
     */
    @Bean(name = "dynamicDataSource")
    @Primary
    public DynamicDataSource dataSource(@Qualifier("masterDataSource") DataSource masterDataSource, @Nullable @Qualifier("slaveDataSource") DataSource slaveDataSource) {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceEnum.MASTER.getName(), masterDataSource);
        if (slaveDataSource != null){
            targetDataSources.put(DataSourceEnum.SLAVE.getName(), slaveDataSource);
        }
        // 還有數(shù)據(jù)源,在targetDataSources中繼續(xù)添加
        return new DynamicDataSource(masterDataSource, targetDataSources);
    }

    @Bean(name = "sqlSessionFactory")
    @Primary
    public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dataSource)
            throws Exception {
        //參照的別人的代碼說需要將會話工廠改成mybatis-plus的sql會話工廠蠢笋,
        //經(jīng)測試發(fā)現(xiàn)使用mybatis的會話工廠也可以運行拨齐,不會報錯
//        MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
        //使用了PackagesSqlSessionFactoryBean繼承SqlSessionFactoryBean,重寫了配置別名的方法
        PackagesSqlSessionFactoryBean bean = new PackagesSqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        //設置多數(shù)據(jù)源分布式事務
        bean.setTransactionFactory(new MultiDataSourceTransactionFactory());
        bean.setTypeAliasesPackage("com.sccl.data_source_change.*.domain");//通配符設置包別名
        bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mybatis/**/*.xml"));// 掃描指定目錄的xml
        return bean.getObject();
    }

    @Bean(name = "sqlSessionTemplate")
    @Primary
    public SqlSessionTemplate sqlSessionTemplate(
            @Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
}

多數(shù)據(jù)源事務管理器和相應的工廠(參考別人的代碼)
MultiDataSourceTransaction昨寞,MultiDataSourceTransactionFactory

MultiDataSourceTransaction

package com.sccl.data_source_change.config;

import com.alibaba.druid.support.logging.Log;
import com.alibaba.druid.support.logging.LogFactory;
import com.sccl.data_source_change.datasource.DynamicDataSource;
import com.sccl.data_source_change.datasource.DynamicDataSourceContextHolder;
import org.apache.ibatis.transaction.Transaction;
import org.springframework.jdbc.CannotGetJdbcConnectionException;
import org.springframework.jdbc.datasource.DataSourceUtils;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import static org.apache.commons.lang3.Validate.notNull;

/**
 * <P>多數(shù)據(jù)源切換瞻惋,支持事務</P>
 *
 * @author lishuangqi
 * @date 2019/5/16 15:09
 * @since
 */
public class MultiDataSourceTransaction implements Transaction {
    private static final Log LOGGER = LogFactory.getLog(MultiDataSourceTransaction.class);

    private final DataSource dataSource;

    private Connection mainConnection;

    private String mainDatabaseIdentification;

    private ConcurrentMap<String, Connection> otherConnectionMap;


    private boolean isConnectionTransactional;

    private boolean autoCommit;


    public MultiDataSourceTransaction(DataSource dataSource) {
        notNull(dataSource, "No DataSource specified");
        this.dataSource = dataSource;
        otherConnectionMap = new ConcurrentHashMap<>();
        mainDatabaseIdentification= DynamicDataSourceContextHolder.getDB();
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public Connection getConnection() throws SQLException {
        String databaseIdentification = DynamicDataSourceContextHolder.getDB();
        if (databaseIdentification.equals(mainDatabaseIdentification)) {
            if (mainConnection != null) return mainConnection;
            else {
                openMainConnection();
                mainDatabaseIdentification =databaseIdentification;
                return mainConnection;
            }
        } else {
            if (!otherConnectionMap.containsKey(databaseIdentification)) {
                try {
                    Connection conn = dataSource.getConnection();
                    otherConnectionMap.put(databaseIdentification, conn);
                } catch (SQLException ex) {
                    throw new CannotGetJdbcConnectionException("Could not get JDBC Connection", ex);
                }
            }
            return otherConnectionMap.get(databaseIdentification);
        }

    }


    private void openMainConnection() throws SQLException {
        this.mainConnection = DataSourceUtils.getConnection(this.dataSource);
        this.autoCommit = this.mainConnection.getAutoCommit();
        this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.mainConnection, this.dataSource);

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug(
                    "JDBC Connection ["
                            + this.mainConnection
                            + "] will"
                            + (this.isConnectionTransactional ? " " : " not ")
                            + "be managed by Spring");
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void commit() throws SQLException {
        if (this.mainConnection != null && !this.isConnectionTransactional && !this.autoCommit) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Committing JDBC Connection [" + this.mainConnection + "]");
            }
            this.mainConnection.commit();
            for (Connection connection : otherConnectionMap.values()) {
                connection.commit();
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void rollback() throws SQLException {
        if (this.mainConnection != null && !this.isConnectionTransactional && !this.autoCommit) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Rolling back JDBC Connection [" + this.mainConnection + "]");
            }
            this.mainConnection.rollback();
            for (Connection connection : otherConnectionMap.values()) {
                connection.rollback();
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void close() throws SQLException {
        DataSourceUtils.releaseConnection(this.mainConnection, this.dataSource);
        for (Connection connection : otherConnectionMap.values()) {
            DataSourceUtils.releaseConnection(connection, this.dataSource);
        }
    }

    @Override
    public Integer getTimeout() throws SQLException {
        return null;
    }
}

MultiDataSourceTransactionFactory

package com.sccl.data_source_change.config;



import org.apache.ibatis.session.TransactionIsolationLevel;
import org.apache.ibatis.transaction.Transaction;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;

import javax.sql.DataSource;

/**
 * <P>支持Service內(nèi)多數(shù)據(jù)源切換的Factory</P>
 *
 * @author lishuangqi
 * @date 2019/5/16 15:09
 * @since
 */
public class MultiDataSourceTransactionFactory extends SpringManagedTransactionFactory {
    @Override
    public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {
        return new MultiDataSourceTransaction(dataSource);
    }
}

XATransactionManagerConfig (分布式事務管理器)
上面重寫的多數(shù)據(jù)源事務管理器是為了讓我們能根據(jù)數(shù)據(jù)源的不同類型,動態(tài)獲取數(shù)據(jù)庫連接援岩,而不是從原來的緩存中獲取導致數(shù)據(jù)源沒法切換歼狼,這里配置的分布式事務管理器是為了讓多數(shù)據(jù)源操作發(fā)生異常時,讓多數(shù)據(jù)源的事務進行同步回滾享怀,由于之前配置的數(shù)據(jù)源都是換成了支持xa協(xié)議的羽峰,所以多數(shù)據(jù)源的資源都在atomikos的管控下了,能夠進行多數(shù)據(jù)源的事務回滾

package com.sccl.data_source_change.config;

import com.atomikos.icatch.jta.UserTransactionImp;
import com.atomikos.icatch.jta.UserTransactionManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.jta.JtaTransactionManager;

import javax.transaction.TransactionManager;
import javax.transaction.UserTransaction;

/** 分布式事務管理器
 * Create by wangbin
 * 2019-11-21-18:10
 */
@Configuration
public class XATransactionManagerConfig {
    @Bean(name = "userTransaction")
    public UserTransaction userTransaction() throws Throwable {
        UserTransactionImp userTransactionImp = new UserTransactionImp();
        userTransactionImp.setTransactionTimeout(10000);
        return userTransactionImp;
    }

    @Bean(name = "atomikosTransactionManager")
    public TransactionManager atomikosTransactionManager() throws Throwable {
        UserTransactionManager userTransactionManager = new UserTransactionManager();
        userTransactionManager.setForceShutdown(false);
        return userTransactionManager;
    }

    @Bean(name = "transactionManager")
    @DependsOn({ "userTransaction", "atomikosTransactionManager" })
    public PlatformTransactionManager transactionManager() throws Throwable {
        return new JtaTransactionManager(userTransaction(),atomikosTransactionManager());
    }
}

PackagesSqlSessionFactoryBean

這個類繼承mybatis的SqlSessionFactoryBean添瓷,重寫設置包別名的方法梅屉,使其支持通配符配置

package com.sccl.data_source_change.utils;

import org.apache.commons.lang3.StringUtils;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.util.ClassUtils;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/** 配置typeAliasesPackage支持通配符包路徑掃描
 * 通過繼承重寫包路徑讀取方式來實現(xiàn)支持通配符配置,以前的SqlSessionFactoryBean
 * 不支持通配符設置包別名仰坦,所以重寫該方法
 * Create by wangbin
 * 2019-11-25-17:18
 */
public class PackagesSqlSessionFactoryBean extends SqlSessionFactoryBean {
    private static final Logger logger = LoggerFactory.getLogger(PackagesSqlSessionFactoryBean.class);

    static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";

    @Override
    public void setTypeAliasesPackage(String typeAliasesPackage) {
        ResourcePatternResolver resolver = (ResourcePatternResolver) new PathMatchingResourcePatternResolver();
        MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resolver);
        typeAliasesPackage = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
                ClassUtils.convertClassNameToResourcePath(typeAliasesPackage) + "/" + DEFAULT_RESOURCE_PATTERN;

        //將加載多個絕對匹配的所有Resource
        //將首先通過ClassLoader.getResource("META-INF")加載非模式路徑部分
        //然后進行遍歷模式匹配
        try {
            List<String> result = new ArrayList<String>();
            Resource[] resources =  resolver.getResources(typeAliasesPackage);
            if(resources != null && resources.length > 0){
                MetadataReader metadataReader = null;
                for(Resource resource : resources){
                    if(resource.isReadable()){
                        metadataReader =  metadataReaderFactory.getMetadataReader(resource);
                        try {
                            result.add(Class.forName(metadataReader.getClassMetadata().getClassName()).getPackage().getName());
                        } catch (ClassNotFoundException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
            if(result.size() > 0) {
                super.setTypeAliasesPackage(StringUtils.join(result.toArray(), ","));
            }else{
                logger.warn("參數(shù)typeAliasesPackage:"+typeAliasesPackage+"履植,未找到任何包");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

  • 2.2.5 domain,controller,service,mapper,mapper.xml

這些都是常規(guī)的層了,只有一些小細節(jié)需要注意悄晃,這里為了方便直觀測試玫霎,特意建了master,slave的包分開凿滤,其實可以不用分開的,mapper.xml就也分了包


方便直觀測試庶近,特意分的包

mapper.xml也分包了

Domain 實體類

Book

import lombok.Data;

/**
 * Create by wangbin
 * 2019-08-07-0:55
 */
@Data
public class Book {
    private Integer id;
    private String name;
    private String author;
}

User

package com.sccl.data_source_change.slave.domain;

import lombok.Data;

/**
 * Create by wangbin
 * 2019-11-21-18:13
 */
@Data
public class User {
    private Integer id;
    private Integer age;
    private String gender;
    private String name;
}

Controller層

直接在controller層測試多庫讀取和多庫寫入

package com.sccl.data_source_change.controller;


import com.sccl.data_source_change.master.domain.Book;
import com.sccl.data_source_change.master.service.BookService;
import com.sccl.data_source_change.slave.domain.User;
import com.sccl.data_source_change.slave.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**在controller層中注入不同的mapper實例翁脆,操作不同的數(shù)據(jù)源
 * Create by wangbin
 * 2019-08-07-1:26
 */
@RestController
public class BookController {
    @Autowired
    private BookService bookService;
    @Autowired
    private UserService userService;

    @GetMapping("/test1")//測試查詢主從庫的數(shù)據(jù)
    public String test1(){
        List<Book> books1 = bookService.getAllBooks();
        List<Book> books2 = bookService.getAllBooks2();
        System.out.println("books1:"+books1);
        System.out.println("books2:"+books2);
        List<User> users1 = userService.getAllUsers();
        System.out.println("user1:"+users1);
        return "OK";
    }
    @Transactional
    @GetMapping("/test2")//測試主從雙庫寫入
    public String test2(){
        Book book = new Book();
        book.setName("羅賓遜");
        book.setAuthor("漂流記");
        int bookNumber = bookService.addBook(book);
        Book book2 = new Book();
        book2.setName("飛馳人生");
        book2.setAuthor("韓寒");
        int bookNumber2 = bookService.addBook2(book2);
        System.out.println("向master數(shù)據(jù)庫添加數(shù)據(jù):"+bookNumber);
        System.out.println("向slave數(shù)據(jù)庫添加數(shù)據(jù):"+bookNumber2);
        int number = 1/0;//自定義錯誤,查看事務是否回滾
        return "OK";
    }
    @Transactional
    @GetMapping("/test3")
    public String test3(){
        Book book = new Book();
        book.setName("master");
        book.setAuthor("master");
        int bookNumber = bookService.addBook(book);
        User user = new User();
        user.setAge(18);
        user.setGender("男");
        user.setName("slave");
        int userNumber = userService.addUser(user);
        int number = 1/0;
        return "OK";
    }
}

Service鼻种,ServiceImpl 服務層與實現(xiàn)類

BookService


import com.sccl.data_source_change.master.domain.Book;

import java.util.List;

/**
 * Create by wangbin
 * 2019-11-18-17:56
 */
public interface BookService  {
    List<Book> getAllBooks();
    List<Book> getAllBooks2();
    int addBook(Book book);
    int addBook2(Book book);

BookServiceImpl

package com.sccl.data_source_change.master.service;


import com.sccl.data_source_change.aspectj.annotation.DataSource;
import com.sccl.data_source_change.enumConst.DataSourceEnum;
import com.sccl.data_source_change.master.domain.Book;
import com.sccl.data_source_change.master.mapper.BookMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

/**
 * Create by wangbin
 * 2019-11-18-17:57
 */
@Transactional
@Service
public class BookServiceImpl  implements BookService {
    @Autowired
    private BookMapper bookMapper;
    @DataSource(value = DataSourceEnum.MASTER)
    @Override
    public List<Book> getAllBooks() {
        return bookMapper.getAllBooks();
    }
    @DataSource(value = DataSourceEnum.SLAVE)
    @Override
    public List<Book> getAllBooks2() {
        return bookMapper.getAllBooks();
    }
    @DataSource(value = DataSourceEnum.MASTER)
    @Override
    public int addBook(Book book) {
        return bookMapper.addBook(book);
    }
    @DataSource(value = DataSourceEnum.SLAVE)
    @Override
    public int addBook2(Book book) {
        return bookMapper.addBook(book);
    }
}

UserService

package com.sccl.data_source_change.slave.service;


import com.sccl.data_source_change.slave.domain.User;

import java.util.List;

/**
 * Create by wangbin
 * 2019-11-21-18:18
 */
public interface UserService {
    int addUser(User user);
    List<User> getAllUsers();
}

UserServiceImpl

package com.sccl.data_source_change.slave.service;


import com.sccl.data_source_change.aspectj.annotation.DataSource;
import com.sccl.data_source_change.enumConst.DataSourceEnum;
import com.sccl.data_source_change.slave.domain.User;
import com.sccl.data_source_change.slave.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

/**
 * Create by wangbin
 * 2019-11-21-18:19
 */
@Transactional
@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;
    @DataSource(value = DataSourceEnum.SLAVE)
    @Override
    public int addUser(User user) {
        return userMapper.addUser(user);
    }
    @DataSource(value = DataSourceEnum.SLAVE)
    @Override
    public List<User> getAllUsers() {
        return userMapper.getAllUsers();
    }
}

mapper層

BookMapper

package com.sccl.data_source_change.master.mapper;



import com.sccl.data_source_change.master.domain.Book;

import java.util.List;

/**
 * Create by wangbin
 * 2019-08-07-1:18
 */
public interface BookMapper {
    List<Book> getAllBooks();
    int addBook(Book book);
}

UserMapper

package com.sccl.data_source_change.slave.mapper;


import com.sccl.data_source_change.slave.domain.User;

import java.util.List;

/**
 * Create by wangbin
 * 2019-11-21-18:20
 */
public interface UserMapper {
    int addUser(User user);
    List<User> getAllUsers();
}

mapper.xml文件

BookMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.sccl.data_source_change.master.mapper.BookMapper">

    <select id="getAllBooks" resultType="Book">
        select * from book
    </select>
    <insert id="addBook" parameterType="Book">
        insert into book (name,author) values (#{name},#{author})
    </insert>
</mapper>

UserMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.sccl.data_source_change.slave.mapper.UserMapper">

    <select id="getAllUsers" resultType="com.sccl.data_source_change.slave.domain.User">
        select * from user
    </select>
    <insert id="addUser" parameterType="com.sccl.data_source_change.slave.domain.User">
        insert into user (age,gender,name) values (#{age},#{gender},#{name})
    </insert>
</mapper>

3 測試

3.1 測試雙庫讀取

訪問 http://localhost:8099/data/test1

BookConreoller測試代碼

BookServiceImpl測試代碼

UserServiceImpl測試代碼

斷點調(diào)試:
第一次訪問切換到master數(shù)據(jù)源

第一次訪問切換到master數(shù)據(jù)源

第二次訪問切換到slave數(shù)據(jù)源

第二次訪問切換到slave數(shù)據(jù)源

第三次訪問切換到slave數(shù)據(jù)源

第三次訪問切換到slave數(shù)據(jù)源

后臺打印結果:

后臺打印結果

前端頁面顯示結果:

前端頁面顯示結果

后臺數(shù)據(jù)庫:

master庫反番,book表數(shù)據(jù)

master庫,book表數(shù)據(jù)

slave庫叉钥,book表數(shù)據(jù)

slave庫罢缸,book表數(shù)據(jù)

slave庫,user表數(shù)據(jù)

slave庫投队,user表數(shù)據(jù)

測試結果:成功查詢了master庫中的book數(shù)據(jù)枫疆,slave庫中的book和user數(shù)據(jù)

小坑:這里要注意一下,要將@Transactional事務注解方法serviceImpl層敷鸦,不能放到controller層息楔,不然測試會發(fā)現(xiàn)在查詢book數(shù)據(jù)的時候,數(shù)據(jù)庫沒有切換扒披,但是查user數(shù)據(jù)的時候切換了值依,原因是在查book的時候,查詢不同庫中的book數(shù)據(jù)碟案,方法都寫在同一個bookService中的愿险,要在同一個service中用不同方法訪問不同的數(shù)據(jù)庫需要將事務控制的注解加到serviceImpl層,如果是controller層調(diào)用不同service中的方法訪問不同數(shù)據(jù)庫价说,可以直接將事務控制的注解加在controller層

3.2 測試雙庫寫入

訪問:http://localhost:8099/data/test2

1.先進行正常測試
BookController測試代碼
BookServiceImpl測試代碼

后臺打印結果:

雙庫寫入數(shù)據(jù)

后臺數(shù)據(jù)庫:

maser庫的book中成功添加一條數(shù)據(jù)

maser庫的book中成功添加一條數(shù)據(jù)

slave庫的book中成功添加一條數(shù)據(jù)

slave庫的book中成功添加一條數(shù)據(jù)

前端頁面結果:


前端頁面結果
2.進行異常測試拯啦,看事務是否同步回滾
放開注釋,制造異常熔任,查看事務是否回滾

前端結果:


異常出現(xiàn),回滾事務

后臺打印結果:


發(fā)生異常了

后臺數(shù)據(jù)庫: 查看數(shù)據(jù)是否有沒有加入進去唁情,事務有沒有同步回滾

maste庫中數(shù)據(jù)編號還是9疑苔,是之前正常測試加進去的數(shù)據(jù),可以看到book數(shù)據(jù)沒有加進去甸鸟,事務回滾了


image.png

slave庫中的數(shù)據(jù)編號還是5惦费,也是之前正常測試加入的數(shù)據(jù),book的數(shù)據(jù)也沒有加進去抢韭,事務也回滾了


image.png

測試結果:master,slave庫中的數(shù)據(jù)都沒有添加成功薪贫,事務都進行了同步回滾

3.進行異常測試,向不同庫的不同表添加數(shù)據(jù)
異常測試刻恭,多庫事務是否同步回滾
addBook
addUser

前端結果:


異常出現(xiàn)瞧省,事務回滾

后臺打印結果:


異常出現(xiàn)

后臺數(shù)據(jù)庫:

master庫中的book數(shù)據(jù)編號還是9扯夭,依舊是第一次正常測試添加的數(shù)據(jù),本次添加的數(shù)據(jù)進行了事務回滾


master庫事務回滾

slave庫中的user數(shù)據(jù)只有最早擁有的第一條數(shù)據(jù)鞍匾,很顯然新的數(shù)據(jù)也沒有加進去交洗,事務回滾了


slave庫數(shù)據(jù)回滾

測試結果:多庫操作異常發(fā)生,多庫事務同步回滾

4.進行正常測試橡淑,向不同庫的不同表添加數(shù)據(jù)
正常測試

前端結果:


沒發(fā)生異常构拳,返回ok

后臺打印結果:


沒發(fā)生異常,數(shù)據(jù)庫操作與切換成功

后臺數(shù)據(jù)庫結果:

master庫book表中新加了一條數(shù)據(jù)

master庫book表中新加了一條數(shù)據(jù)

slave庫user表中新加了一條數(shù)據(jù)

slave庫user表中新加了一條數(shù)據(jù)

測試結果:正常測試梁棠,向不同庫的不同表中添加數(shù)據(jù)成功

最終結論:采用Atomikos統(tǒng)一管控了多數(shù)據(jù)源資源操作的事務置森,通過重寫多數(shù)據(jù)源事務管理器使得在事務管控下能通過注解正常切換數(shù)據(jù)源,當多數(shù)據(jù)源操作出現(xiàn)異常時符糊,Atomikos會對管控的多數(shù)據(jù)源事務進行同步回滾凫海,未發(fā)生異常時,數(shù)據(jù)庫正常操作執(zhí)行

本Demo采用注解形式動態(tài)切換數(shù)據(jù)源濒蒋,并且可以管控分布式事務盐碱,可以不用分包,如果需要添加更多的數(shù)據(jù)源可以在枚舉和yml中簡單配置一下即可沪伙,比較靈活

本項目參照了很多網(wǎng)上的文章瓮顽,文章出處在文中已標注

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市围橡,隨后出現(xiàn)的幾起案子暖混,更是在濱河造成了極大的恐慌,老刑警劉巖翁授,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件拣播,死亡現(xiàn)場離奇詭異,居然都是意外死亡收擦,警方通過查閱死者的電腦和手機贮配,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來塞赂,“玉大人泪勒,你說我怎么就攤上這事⊙缁” “怎么了圆存?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長仇哆。 經(jīng)常有香客問我沦辙,道長,這世上最難降的妖魔是什么讹剔? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任油讯,我火速辦了婚禮详民,結果婚禮上,老公的妹妹穿的比我還像新娘撞羽。我一直安慰自己阐斜,他們只是感情好,可當我...
    茶點故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布诀紊。 她就那樣靜靜地躺著谒出,像睡著了一般葡幸。 火紅的嫁衣襯著肌膚如雪典蜕。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天枯途,我揣著相機與錄音碌宴,去河邊找鬼杀狡。 笑死,一個胖子當著我的面吹牛贰镣,可吹牛的內(nèi)容都是我干的呜象。 我是一名探鬼主播,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼碑隆,長吁一口氣:“原來是場噩夢啊……” “哼恭陡!你這毒婦竟也來了?” 一聲冷哼從身側響起上煤,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤休玩,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后劫狠,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體拴疤,經(jīng)...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年独泞,在試婚紗的時候發(fā)現(xiàn)自己被綠了呐矾。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,724評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡懦砂,死狀恐怖凫佛,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情孕惜,我是刑警寧澤,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布晨炕,位于F島的核電站衫画,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏瓮栗。R本人自食惡果不足惜削罩,卻給世界環(huán)境...
    茶點故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一瞄勾、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧弥激,春花似錦进陡、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至以蕴,卻和暖如春糙麦,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背丛肮。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工赡磅, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人宝与。 一個月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓焚廊,卻偏偏與公主長得像,于是被迫代替她去往敵國和親习劫。 傳聞我的和親對象是個殘疾皇子咆瘟,可洞房花燭夜當晚...
    茶點故事閱讀 43,627評論 2 350