通过LCN实现分布式事务

简介

为什么要使用分布式事务,就不详细介绍了,具体可以看CAP定律BASE理论和分布式事务解决方案,好了话不多说,一个字:

LCN介绍

官方宣称:LCN并不生产事务,LCN只是本地事务的协调工
TX-LCN定位于一款事务协调性框架,框架其本身并不操作事务,而是基于对事务的协调从而达到事务一致性的效果。

LCN解决方案

在一个分布式系统下存在多个模块协调来完成一次业务。那么就存在一次业务事务下可能横跨多种数据源节点的可能。TX-LCN将可以解决这样的问题。

例如存在服务模块A 、B、 C。A模块是mysql作为数据源的服务,B模块是基于redis作为数据源的服务,C模块是基于mongo作为数据源的服务。若需要解决他们的事务一致性就需要针对不同的节点采用不同的方案,并且统一协调完成分布式事务的处理。


image.png

若采用TX-LCN分布式事务框架,则可以将A模块采用LCN模式、B/C采用TCC模式就能完美解决。

事务控制原理

TX-LCN由两大模块组成, TxClient、TxManager,TxClient作为模块的依赖框架,提供TX-LCN的标准支持,TxManager作为分布式事务的控制放。事务发起方或者参与反都由TxClient端来控制。


image.png

核心步骤

创建事务组
是指在事务发起方开始执行业务代码之前先调用TxManager创建事务组对象,然后拿到事务标示GroupId的过程。

加入事务组
添加事务组是指参与方在执行完业务方法以后,将该模块的事务信息通知给TxManager的操作。

通知事务组
是指在发起方执行完业务代码以后,将发起方执行结果状态通知给TxManager,TxManager将根据事务最终状态和事务组的信息来通知相应的参与模块提交或回滚事务,并返回结果给事务发起方。

实战之-TxManager

LCN官方文档中的快速开始,说实话,还真快速不了,首先咱们先看下官方文档怎么说的。

建中间件数据库,

名称为: tx-manager,并创建表。

CREATE TABLE `t_tx_exception`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `group_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `unit_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `mod_id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `transaction_state` tinyint(4) NULL DEFAULT NULL,
  `registrar` tinyint(4) NULL DEFAULT NULL,
  `remark` varchar(4096) NULL DEFAULT  NULL,
  `ex_state` tinyint(4) NULL DEFAULT NULL COMMENT '0 未解决 1已解决',
  `create_time` datetime(0) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

TM服务搭建

image.png

官方源码仓库中的txlcn-tm直接打包,然后运行即可,TM中间件就算搭起来了,如果直接把txlcn-tm拎出来,打包是打不成功的,因为txlcn-tm 依赖了其他的txlcn的其他服务,打包的话需要把其他包也一起打进去,我现在是直接本地导入IDE运行的,是可以的。

image.png

TM服务中 application.properties配置

spring.application.name=TransactionManager
server.port=7970
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/tx-manager?characterEncoding=UTF-8&usessl=false
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.hibernate.ddl-auto=update

#Redis配置
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=

tx-lcn.manager.admin-key=lcn



#spring.application.name=TransactionManager
#server.port=7970
#
## JDBC 数据库配置
#spring.datasource.driver-class-name=com.mysql.jdbc.Driver
#spring.datasource.url=jdbc:mysql://127.0.0.1:3306/tx-manager?characterEncoding=UTF-8
#spring.datasource.username=root
#spring.datasource.password=123456
#
## 数据库方言
#spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
#
## 第一次运行可以设置为: create, 为TM创建持久化数据库表
#spring.jpa.hibernate.ddl-auto=validate
#
## TM监听IP. 默认为 127.0.0.1
#tx-lcn.manager.host=127.0.0.1
#
## TM监听Socket端口. 默认为 ${server.port} - 100
#tx-lcn.manager.port=8070
#
## 心跳检测时间(ms). 默认为 300000
#tx-lcn.manager.heart-time=300000
#
## 分布式事务执行总时间(ms). 默认为36000
#tx-lcn.manager.dtx-time=8000
#
## 参数延迟删除时间单位ms  默认为dtx-time值
#tx-lcn.message.netty.attr-delay-time=${tx-lcn.manager.dtx-time}
#
## 事务处理并发等级. 默认为机器逻辑核心数5倍
#tx-lcn.manager.concurrent-level=160
#
## TM后台登陆密码,默认值为codingapi
#tx-lcn.manager.admin-key=codingapi
#
## 分布式事务锁超时时间 默认为-1,当-1时会用tx-lcn.manager.dtx-time的时间
#tx-lcn.manager.dtx-lock-time=${tx-lcn.manager.dtx-time}
#
## 雪花算法的sequence位长度,默认为12位.
#tx-lcn.manager.seq-len=12
#
## 异常回调开关。开启时请制定ex-url
#tx-lcn.manager.ex-url-enabled=false
#
## 事务异常通知(任何http协议地址。未指定协议时,为TM提供内置功能接口)。默认是邮件通知
#tx-lcn.manager.ex-url=/provider/email-to/***@**.com

#注意(NOTE)
#(1) TxManager所有配置均有默认配置,请按需覆盖默认配置。
#(2) 特别注意 TxManager进程会监听两个端口号,一个为TxManager端口,另一个是事务消息端口。TxClient默认连接事务消息端口是8070, 所以,为保证TX-LCN基于默认配置运行良好,请设置TxManager端口号为8069 或者指定事务消息端口为8070
#(3) 分布式事务执行总时间 a 与 TxClient通讯最大等待时间 b、TxManager通讯最大等待时间 c、微服务间通讯时间 d、微服务调用链长度 e 几个时间存在着依赖关系。 a >= 2c + (b + c + d) * (e - 1), 特别地,b、c、d 一致时,a >= (3e-1)b。你也可以在此理论上适当在减小a的值,发生异常时能更快得到自动补偿,即 a >= (3e-1)b - Δ(原因)。 最后,调用链小于等于3时,将基于默认配置运行良好
#(4) 若用tx-lcn.manager.ex-url=/provider/email-to/xxx@xx.xxx 这个配置,配置管理员邮箱信息(如QQ邮箱):
#spring.mail.host=smtp.qq.com
#spring.mail.port=587
#spring.mail.username=xxxxx@**.com

启动成功后界面

image.png

TxManager进程会监听两个端口号,一个为TxManager 7970 端口,另一个是事务消息端口 8070。

实战之-TxClient

场景简介

AB两个服务,A(producer)服务调用B(consumer)服务完毕后,抛出异常,B服务能回滚,确保幂等。

AB环境:MybatisPlus,Erueka,Feign,这些具体就不细讲了

服务结构

image.png

producer 服务

Pom.xml

<?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">

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.glj</groupId>
    <artifactId>producer</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>producer</name>
    <modelVersion>4.0.0</modelVersion>
    <description>Demo project for Spring Boot</description>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <codingapi.txlcn.version>5.0.2.RELEASE</codingapi.txlcn.version>
    </properties>

    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        

        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>27.0-jre</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- mybatis-plus begin -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.0.6</version>
        </dependency>

        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.8.0</version>
            <exclusions>
                <exclusion>
                    <artifactId>org.mapstruct</artifactId>
                    <groupId>mapstruct</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.8.0</version>
        </dependency>

        <dependency>
            <groupId>com.codingapi.txlcn</groupId>
            <artifactId>txlcn-tc</artifactId>
            <version>${codingapi.txlcn.version}</version>
        </dependency>

        <dependency>
            <groupId>com.codingapi.txlcn</groupId>
            <artifactId>txlcn-txmsg-netty</artifactId>
            <version>${codingapi.txlcn.version}</version>
        </dependency>



    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Greenwich.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

</project>

application.yml

spring:
  application:
    name: spring-cloud-producer

  datasource:
    url: jdbc:mysql://127.0.0.1:3306/spring_cloud_app?serverTimezone=GMT%2B8&characterEncoding=utf-8
    password: root
    username: root
    driver-class-name: com.mysql.cj.jdbc.Driver

  redis:
    host: 127.0.0.1
    port: 6379

server:
  port: 8081

eureka:
  client:
    service-url:
      defaultZone: http://glj:glj@127.0.0.1:2100/eureka/
  instance:
    prefer-ip-address: true
    instance-id: ${spring.cloud.client.ipAddress}:${server.port}

#swagger
swagger2:
  enable: true

tx-lcn:
  client:
    manager-address: 127.0.0.1:8070

服务入口

package com.glj.producer;

import com.codingapi.txlcn.tc.config.EnableDistributedTransaction;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@EnableDistributedTransaction
public class ProducerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ProducerApplication.class, args);
    }

}

package com.glj.producer.business.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.codingapi.txlcn.tc.annotation.LcnTransaction;
import com.glj.producer.business.entity.UserPo;
import com.glj.producer.business.mapper.UserMapper;
import com.glj.producer.business.service.IUserService;
import com.glj.producer.client.ConsumerUserClinet;
import com.glj.producer.dto.ProducerRequst;
import com.google.common.base.Preconditions;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author gaoleijie
 * @since 2019-05-21
 */
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, UserPo> implements IUserService {
    @Autowired
    private ConsumerUserClinet consumerUserClinet;
    @Autowired
    private IUserService userService;

    /**
     * 使用分布式事务
     * @param requst
     * @return
     */
    @Override
    @LcnTransaction
    @Transactional
    public String aTob(ProducerRequst requst) {
        log.info("开始调用B服务");
        Boolean res =  consumerUserClinet.saveUser(requst.getNickName(),requst.getId());
        log.info("B服务return result{}",res);

        UserPo userPo = userService.getById(requst.getId());
        userPo.setUserName(requst.getNickName());
        userService.saveOrUpdate(userPo);

        Preconditions.checkArgument(StringUtils.isNotBlank(requst.getExFlag()),"exFlag is null");

         if(res) {
             return "scuess";
         } else {
             return  "false";
         }
    }

    /**
     * 未使用分布式事务
     * @param requst
     * @return
     */
    @Override
    @Transactional
    public String aTob1(ProducerRequst requst) {
        log.info("开始调用B服务");
        Boolean res =  consumerUserClinet.saveUser1(requst.getNickName(),requst.getId());
        log.info("B服务return result{}",res);

        UserPo userPo = userService.getById(requst.getId());
        userPo.setUserName(requst.getNickName());
        userService.saveOrUpdate(userPo);

        Preconditions.checkArgument(StringUtils.isNotBlank(requst.getExFlag()),"exFlag is null");

        if(res) {
            return "scuess";
        } else {
            return  "false";
        }
    }
}

package com.glj.producer.business.controller;


import com.glj.producer.business.service.IUserService;
import com.glj.producer.dto.ProducerRequst;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
 * <p>
 *  前端控制器
 * </p>
 *
 * @author gaoleijie
 * @since 2019-05-21
 */
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private IUserService userService;

    @ApiOperation(value = "A 服务调用B服务(使用分布式事务)")
    @PostMapping("/aTob")
    public String aTob(@RequestBody ProducerRequst requst){
        return userService.aTob(requst);
    }

    @ApiOperation(value = "A 服务调用B服务(未使用分布式事务)")
    @PostMapping("/aTob1")
    public String aTob1(@RequestBody ProducerRequst requst){
        return userService.aTob1(requst);
    }
}

其实就是这么简单,核心代码就三处
一处是服务入口加注解@EnableDistributedTransaction
一处是service方法上加注解 @LcnTransaction
另一处是application.yml 指定事务消息端口

consumer 服务

Pom.xml

<?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>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.glj</groupId>
    <artifactId>consumer</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>consumer</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <codingapi.txlcn.version>5.0.2.RELEASE</codingapi.txlcn.version>
        <springcloud.version>Greenwich.RELEASE</springcloud.version>
        <swagger-version>2.8.0</swagger-version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.0.6</version>
            <exclusions>
                <exclusion>
                    <groupId>org.apache.tomcat</groupId>
                    <artifactId>tomcat-jdbc</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </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>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>


        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <!-- 与swagger一起使用,需要注意-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.8.0</version>
            <exclusions>
                <exclusion>
                    <artifactId>org.mapstruct</artifactId>
                    <groupId>mapstruct</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>${swagger-version}</version>
        </dependency>

        <dependency>
            <groupId>com.codingapi.txlcn</groupId>
            <artifactId>txlcn-tc</artifactId>
            <version>${codingapi.txlcn.version}</version>
        </dependency>

        <dependency>
            <groupId>com.codingapi.txlcn</groupId>
            <artifactId>txlcn-txmsg-netty</artifactId>
            <version>${codingapi.txlcn.version}</version>
        </dependency>


        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>27.0-jre</version>
        </dependency>


    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${springcloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

</project>

服务入口

package com.glj.consumer;

import com.codingapi.txlcn.tc.config.EnableDistributedTransaction;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
@EnableDistributedTransaction
public class ConsumerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class, args);
    }

}
package com.glj.consumer.business.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.codingapi.txlcn.tc.annotation.LcnTransaction;
import com.glj.consumer.business.entity.SysUserPo;
import com.glj.consumer.business.mapper.SysUserMapper;
import com.glj.consumer.business.service.ISysUserService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author gaoleijie
 * @since 2019-05-21
 */
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUserPo> implements ISysUserService {
    /**
     * LCN实现分布式事务
     * @param nickName
     * @param Id
     * @return
     */
    @Override
    @Transactional
    @LcnTransaction
    public Boolean saveUser(String nickName,Long Id){
        SysUserPo user = this.getById(Id);
        user.setNickname(nickName);
        return this.saveOrUpdate(user);
    }

    /**
     * 未实现分布式事务
     * @param nickName
     * @param Id
     * @return
     */
    @Override
    @Transactional
    public Boolean saveUser1(String nickName,Long Id){
        SysUserPo user = this.getById(Id);
        user.setNickname(nickName);
        return this.saveOrUpdate(user);
    }
}

还是那句话,就这么简单,跟生产者同样三处

效果

image.png
image.png

结果表明使用分布式以后,异常后数据回滚
没有使用分布式,异常后,AB数据不一致

结束语

LCN只是解决分布式事务的第三方,目前阿里的GTS 也很成熟了,后期将继续为大家分解通过GTS怎么解决分布式事务问题。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342