Spring学习笔记(五): Spring 事务管理

本章节讲述企业应用中最为重要的内容之一,就是数据库的事务管理。

全部章节传送门:
Spring学习笔记(一):Spring IoC 容器
Spring学习笔记(二):Spring Bean 装配
Spring学习笔记(三): Spring 面向切面
Spring学习笔记(四): Spring 数据库编程
Spring学习笔记(五): Spring 事务管理

事务及其特性

事务(Transaction),一般是指要做的或所做的事情。在计算机术语中是指访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。在企业级应用程序开发中,事务管理是必不可少的技术,用来确保数据的完整性和一致性。

事务的四个特性(ACID):

  • 原子性(Atomicity):事务是一个原子操作,由一系列动作组成。事务的原子性确保动作要么全部完成,要么完全不起作用。

  • 一致性(Consistency):一旦事务完成(不管成功还是失败),系统必须确保它所建模的业务处于一致的状态,而不会是部分完成部分失败。在现实中的数据不应该被破坏。

  • 隔离性(Isolation):可能有许多事务会同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。

  • 持久性(Durability):一旦事务完成,无论发生什么系统错误,它的结果都不应该受到影响,这样就能从任何系统崩溃中恢复过来。通常情况下,事务的结果被写到持久化存储器中。

Spring 数据库事务管理器的设计

在 Spring 中数据库事务是通过 PlatformTransactionMananger 进行管理的, Spring事务管理涉及的接口的联系如下:

spring-transaction.jpg

其中最重要的部分是事务管理器,而事务管理器的实现和平台相关,本文使用 MyBatis 框架中使用比较多的 DataSourceTransactionManager,其它也大同小异。

<!-- 事务管理器配置数据源事务 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource" />
</bean>

声明式事务

Spring 事务分为编程式事务和声明式事务,编程式事务需要开发者自己的代码实现,当业务量很大时会很痛苦,目前已经很少使用,本文主要介绍声明式事务。

声明式事务是一种约定型的事务,当你的业务方法不发生异常时,Spring 就会让事务管理器提交事务,而发生异常时则让事务管理器回滚事务。

Transactional 的配置项

Transactional 的配置项如下表。

配置项 含义 备注
value 定义事务管理器 一个Bean,需要实现 PlatformTransactionManager接口
propagation 传播行为 传播行为是方法之间调用的问题,默认值是Propagation.REQUIRED
isolation 隔离级别 默认值取数据库的隔离级别
readOnly 读写或只读事务 默认读写,即false
timeout 超时时间 单位为秒,超时会引发异常
rollbackFor 导致事务回滚的异常类数组 只有当方法产生的异常包含在其中时,才回滚事务
rollbackForClassName 导致事务回滚的异常类名字数组 同rollBackFor,只是使用类名称定义
noRollbackFor 不会导致事务回滚的异常类数组 当方法产生的异常包含在其中时,会继续提交事务
noRollbackForClassName 不会导致事务回滚的异常类名字数组 同上,只是使用类名称

其中便较难理解的是 isolation 和 propagation,下面会详细介绍。

另外,声明式事务可以通过 XML 或者注解进行配置,比较常用的是注解配置,需要在配置文件中开启,然后使用 @Transactional 注解。

<!--使用注解定义事务 -->
<tx:annotation-driven transaction-manager="transactionManager" />

隔离级别

隔离级别定义一个事务可能受其他并发事务活动影响的程度。

在一个典型的应用程序中,多个事务同时运行,经常会为了完成他们的工作而操作同一个数据。并发虽然是必需的,但是会导致以下问题:

  • 脏读(Dirty read)-- 脏读发生在一个事务读取了被另一个事务改写但尚未提交的数据时。如果这些改变在稍后被回滚了,那么第一个事务读取的数据就会是无效的。
  • 不可重复读(Nonrepeatable read)-- 不可重复读发生在一个事务执行相同的查询两次或两次以上,但每次查询结果都不相同时。这通常是由于另一个并发事务在两次查询之间更新了数据。
  • 幻影读(Phantom reads)-- 幻影读和不可重复读相似。当一个事务(T1)读取几行记录后,另一个并发事务(T2)插入了一些记录时,幻影读就发生了。在后来的查询中,第一个事务(T1)就会发现一些原来没有的额外记录。

在理想状态下,事务之间将完全隔离,从而可以防止这些问题发生。然而,完全隔离会影响性能,因为隔离经常牵扯到锁定在数据库中的记录(而且有时是锁定完整的数据表)。侵占性的锁定会阻碍并发,要求事务相互等待来完成工作。

考虑到完全隔离会影响性能,而且并不是所有应用程序都要求完全隔离,所以有时可以在事务隔离方面灵活处理。因此,就会有好几个隔离级别。

隔离级别 含义
ISOLATION_DEFAULT 使用数据库默认的隔离级别。
ISOLATION_READ_UNCOMMITTED 允许读取尚未提交的更改。可能导致脏读、幻影读或不可重复读
ISOLATION_READ_COMMITTED 允许从已经提交的并发事务读取。可防止脏读,但幻影读和不可重复读仍可能会发生。
ISOLATION_REPEATABLE_READ 对相同字段的多次读取的结果是一致的,除非数据被当前事务本身改变。可防止脏读和不可重复读,但幻影读仍可能发生
ISOLATION_SERIALIZABLE 完全服从ACID的隔离级别,确保不发生脏读、不可重复读和幻影读。这在所有隔离级别中也是最慢的,因为它通常是通过完全锁定当前事务所涉及的数据表来完成的。

在实际工作中,注解 @Transactional 经常使用默认值ISOLATION_DEFAULT,因为不同数据库的隔离级别可能不同,比如 MYSQL 可以支持4个隔离级别,而 Oracle 只能支持 ISOLATION_READ_COMMITTED 和 ISOLATION_SERIALIZABLE,默认为前者。

传播行为

传播行为是指方法之间的调用事务策略问题,一共包含7种。

传播行为 含义
PROPAGATION_REQUIRED 支持当前事务,如果当前没有事务,就新建一个事务。这是Spring的默认传播行为。
PROPAGATION_SUPPORTS 支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY 支持当前事务,如果当前没有事务,就抛出异常
PROPAGATION_REQUIRES_NEW 新建事务,如果当前存在事务,把当前事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。
PROPAGATION_NOT_SUPPORTED 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起,执行当前逻辑,结束后恢复上下文的事务。
PROPAGATION_NEVER 以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED 表示如果当前正有一个事务在进行中,则该方法应当运行在一个嵌套式事务中。被嵌套的事务可以独立于封装事务进行提交或回滚。如果封装事务不存在,行为就像PROPAGATION_REQUIRED一样

在 Spring+MyBatis中使用事务

创建 Maven 项目,添加依赖。

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.wyk</groupId>
    <artifactId>springtransactiondemo</artifactId>
    <version>1.0-SNAPSHOT</version>


    <properties>
        <spring.version>4.3.2.RELEASE</spring.version>
        <mybatis.version>3.4.0</mybatis.version>
    </properties>

    <dependencies>
        <!-- spring核心包 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <!-- mybatis核心包 -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>${mybatis.version}</version>
        </dependency>

        <!-- mybatis/spring包 -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>1.3.0</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.25</version>
        </dependency>
        <dependency>
            <groupId>commons-dbcp</groupId>
            <artifactId>commons-dbcp</artifactId>
            <version>1.4</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>

    </dependencies>

    <!--将xml文件加入maven构建 -->
    <build>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
                <filtering>true</filtering>
            </resource>
        </resources>
    </build>
</project>

添加 Spring 配置文件。

<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                        http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
                        http://www.springframework.org/schema/aop
                        http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
                        http://www.springframework.org/schema/tx
                        http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
                        http://www.springframework.org/schema/context
                        http://www.springframework.org/schema/context/spring-context-4.0.xsd">
    <!-- 启用扫描机制,并指定扫描对应的包 -->
    <context:annotation-config />
    <context:component-scan base-package="com.wyk.springtransactiondemo.*" />
    <!-- 数据库连接池 -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
          destroy-method="close"><!--设置为close使Spring容器关闭同时数据源能够正常关闭,以免造成连接泄露  -->
        <property name="driverClassName" value="com.mysql.jdbc.Driver" />
        <property name="url" value="jdbc:mysql://localhost:3306/springstudy?characterEncoding=UTF8" />
        <property name="username" value="root" />
        <property name="password" value="wanyunkai123" />
        <property name="defaultReadOnly" value="false" /><!-- 设置为只读状态,配置读写分离时,读库可以设置为true -->
        <!-- 在连接池创建后,会初始化并维护一定数量的数据库安连接,当请求过多时,数据库会动态增加连接数,
        当请求过少时,连接池会减少连接数至一个最小空闲值 -->
        <property name="initialSize" value="5" /><!-- 在启动连接池初始创建的数据库连接,默认为0 -->
        <property name="maxActive" value="15" /><!-- 设置数据库同一时间的最大活跃连接默认为8,负数表示不闲置 -->
        <property name="maxIdle" value="10"/><!-- 在连接池空闲时的最大连接数,超过的会被释放,默认为8,负数表示不闲置 -->
        <property name="minIdle" value="2" /><!-- 空闲时的最小连接数,低于这个数量会创建新连接,默认为0 -->
        <property name="maxWait" value="10000" /><!-- 连接被用完时等待归还的最大等待时间,单位毫秒,超出时间抛异常,默认为无限等待 -->
    </bean>
    <!-- 集成 Mybatis -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="configLocation" value="classpath:mybatis-config.xml" />
    </bean>
    <!-- 事务管理器配置数据源事务 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>

    <!--使用注解定义事务 -->
    <tx:annotation-driven transaction-manager="transactionManager" />

    <!-- 采用自动扫描方式创建 mapper bean -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.wyk.springtransactiondemo" />
        <property name="SqlSessionFactory" ref="sqlSessionFactory" />
        <property name="annotationClass" value="org.springframework.stereotype.Repository" />
    </bean>
</beans>

添加 MyBatis 配置文件。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!-- 指定映射器路径 -->
    <mappers>
        <mapper resource="com/wyk/springtransactiondemo/dao/RoleMapper.xml" />
    </mappers>
</configuration>

添加 log4j 配置文件。

log4j.rootLogger=DEBUG,stdout
log4j.logger.org.springframework=DEBUG
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d [%t] %-5p [%c] - %m%n

创建 Role 实体类。

package com.wyk.springmybatisdemo.domain;

public class Role {
    private Long id;
    private String roleName;
    private String note;

    public Long getId() {
        return id;
    }

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

    public String getRoleName() {
        return roleName;
    }

    public void setRoleName(String roleName) {
        this.roleName = roleName;
    }

    public String getNote() {
        return note;
    }

    public void setNote(String note) {
        this.note = note;
    }
}

创建映射接口。

package com.wyk.springtransactiondemo.dao;

import com.wyk.springtransactiondemo.domain.Role;
import org.springframework.stereotype.Repository;

@Repository
public interface RoleMapper {
    public int insertRole(Role role);
}

添加映射文件。

<?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.wyk.springtransactiondemo.dao.RoleMapper">
    <insert id="insertRole" parameterType="com.wyk.springtransactiondemo.domain.Role">
        insert into t_role (role_name, note) values(#{roleName}, #{note})
    </insert>
</mapper>

添加一个主程序进行测试。

public class MainApp {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-cfg.xml");
        RoleListService roleListService = ctx.getBean(RoleListService.class);
        List<Role> roleList = new ArrayList<Role>();
        for(int i = 1; i <= 2; i++) {
            Role role = new Role();
            role.setRoleName("role_name_" + i);
            role.setNote("note_" + i);
            roleList.add(role);
        }
        int count = roleListService.insertRoleList(roleList);
        System.out.println(count);
    }
}

在其中插入了2个角色,由于 insertRoleList 会调用 insertRole,而 insertRole 标注了 REQUIRES_NEW ,所以每次调用会产生新的事务,在控制台会打印如下日志。从日志中会观察到每次调用 insertRole 会产生新的事务。

2019-03-23 12:05:12,916 [main] DEBUG [org.springframework.jdbc.datasource.DataSourceTransactionManager] - Suspending current transaction, creating new transaction with name [com.wyk.springtransactiondemo.service.impl.RoleServiceImpl.insertRole]
2019-03-23 12:05:12,919 [main] DEBUG [org.springframework.jdbc.datasource.DataSourceTransactionManager] - Acquired Connection [jdbc:mysql://localhost:3306/springstudy?characterEncoding=UTF8, UserName=root@localhost, MySQL Connector Java] for JDBC transaction
2019-03-23 12:05:12,920 [main] DEBUG [org.springframework.jdbc.datasource.DataSourceUtils] - Changing isolation level of JDBC Connection [jdbc:mysql://localhost:3306/springstudy?characterEncoding=UTF8, UserName=root@localhost, MySQL Connector Java] to 2
2019-03-23 12:05:12,923 [main] DEBUG [org.springframework.jdbc.datasource.DataSourceTransactionManager] - Switching JDBC Connection [jdbc:mysql://localhost:3306/springstudy?characterEncoding=UTF8, UserName=root@localhost, MySQL Connector Java] to manual commit
2019-03-23 12:05:12,933 [main] DEBUG [org.mybatis.spring.SqlSessionUtils] - Creating a new SqlSession

将 insertRole 的传播行为改为 NESTED,再次进行测试,可以看到如下日志。

2019-03-23 12:10:56,371 [main] DEBUG [org.springframework.jdbc.datasource.DataSourceTransactionManager] - Releasing transaction savepoint

说明数据库启用了保存点技术,由于不是所有数据库都支持保存点技术,所以在把传播行为设置为NESTED的时候,如果数据库不予支持,那么它会和 REQUIRES_NEW 一样创建新事务运行代码,以达到内部方法发生异常时并不回滚当前事务的目的。

@Transactional 的自调用失效问题

有的时候配置了注解 @Transactional,但是它会失效,这里需要注意一些细节。

注解 @Transactional 的底层实现时 Spring AOP 技术,而 Spring AOP 技术使用的是动态代理,这就意味着对于静态(static)方法和非 public 方法,注解 @Transactional 是失效的。还有一个更为隐秘,且及其容易犯错误的——自调用。

所谓自调用,就是一个类的方法调用自身另外一个方法的过程。

改写前面的 RoleServiceImpl 类。

@Service
public class RoleServiceImpl implements RoleService {
    @Autowired
    private RoleMapper roleMapper;

    @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_COMMITTED)
    public int insertRole(Role role) {
        return roleMapper.insertRole(role);
    }

    @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED)
    public int insertRoleList(List<Role> roleList) {
        int count = 0;
        for(Role role : roleList) {
            try {
                //调用自身
                insertRole(role);
                count++;
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }
}

当通过这个实现类去实现 RoleService 的时候,insertRole 方法并不会产生新的事务,也就是说 insertRole 上面的 @Transactional 注解失效了。

出现这个问题的根本原因是 AOP 的实现原理,自己调用自己的过程并不存在代理对象的调用,这样就不会产生 AOP 去为我们设置 @Transactional 配置的参数,就出现了自调用失效的问题。

为了解决自调用问题,一方面可以使用2个服务类,如前文所示;另一方面,可以直接从容器中获取 RoleService 的代理对象,如下所示。但这样会有一个弊端:就是从容器中获取代理对象有入侵之嫌。

@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED)
public int insertRoleList(List<Role> roleList) {
    int count = 0;
    //从容器中获取代理对象
    RoleService service = ctx.getBean(RoleService.class);
    for(Role role : roleList) {
        try {
            service.insertRole(role);
            count++;
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

典型错误用法的剖析

数据事务时企业应用中最关注的核心内容,这里介绍一些容易犯错的地方。

错误使用 Service

在 Spring MVC 中,经常使用 Controller 来调用 Service,假如我们想在一个 Controller 中插入两个角色,而且需要在同一个事务中处理,使用下面的代码。

@Controller
public class RoleController {
    @Autowired
    private RoleSerivce roleService;

    @Autowired
    private RoleListService roleListService;

    public void errorUseServices() {
        Role role1 = new Role();
        role1.setRoleName("role_name_1");
        role1.setNote("role_note_1");
        roleService.insertRole(role1);
        Role role2 = new Role();
        role2.setRoleName("role_name_2");
        role2.setNote("role_note_2");
        roleService.insertRole(role2);
    }
}

这里存在的问题是两个 insertRole 方法不在一个事务里,如果第一个插入成功了,第二个插入失败了,这样会使数据库数据不完全同时成功或者失败,可能产生严重的数据不一致问题。

过长时间占用事务

在企业生产中,数据库事务资源是最宝贵的资源之一,使用数据库事务之后要及时释放。对于一些不需要在数据库中完成的工作,尽量放到事务之外,尤其是一些使用文件、对外连接等消耗时间的操作。

@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_COMMITTED)
    public int insertRole(Role role) {
        int result = roleMapper.insertRole(role);
        //与数据库无关的操作
        doSomeThingForFile();
        return result;
    }

其中的 doSomeThingForFile() 方法是与数据库操作无关的方法,却占用了数据库事务资源。如果占用时间较长,在高并发下很容易发生系统宕机。

对于这些方法,建议放到 Controller 中操作,而不是 Service 中。

错误捕获异常

模拟一段购买商品的代码,其中 ProductService 是产品服务类, TransactionService 是记录交易信息,需求显然是产品减少库存和保存交易在同一个事务里。

@Autowired
private ProductService productService;

@Autowired
private TransactionService transactionService;

@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED)
public int doTransaction(TransactionBean trans) {
    int result = 0;
    try {
        //减少库存
        int result = productService.docreaseStock(trans.getProductId);
        //如果减少库存成功则保存记录
        if(result > 0) {
            transactionService.save(trans);
        }
    } catch (Exception ex) {
        log.info(ex);
    }
    return result;
}

这里的问题是由于开发者不了解 Spring 的事务约定,在方法里加入了自己的try...catch...语句,这样在发生异常的时候, Spring在数据库事务约定的流程中无法得到异常信息,就会提交事务,导致出现问题。

可以对代码进行如下修改。抛出一个运行异常,这样 Spring的事务流程可以捕获到这个异常,进行事务回滚。

@Autowired
private ProductService productService;

@Autowired
private TransactionService transactionService;

@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED)
public int doTransaction(TransactionBean trans) {
    int result = 0;
    try {
        //减少库存
        int result = productService.docreaseStock(trans.getProductId);
        //如果减少库存成功则保存记录
        if(result > 0) {
            transactionService.save(trans);
        }
    } catch (Exception ex) {
        log.info(ex);
        //自行抛出异常,让Spring获取异常
        throw new RuntimeException(ex);
    }
    return result;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,542评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,822评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,912评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,449评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,500评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,370评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,193评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,074评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,505评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,722评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,841评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,569评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,168评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,783评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,918评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,962评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,781评论 2 354